cleanup
This commit is contained in:
parent
d0d4c543f2
commit
65ca631115
4 changed files with 200 additions and 375 deletions
|
|
@ -2,12 +2,10 @@ defmodule AprsWeb.MapLive.CallsignView do
|
|||
@moduledoc false
|
||||
use AprsWeb, :live_view
|
||||
|
||||
alias Aprs.EncodingUtils
|
||||
alias Aprs.Packets
|
||||
alias AprsWeb.Endpoint
|
||||
alias AprsWeb.MapLive.MapHelpers
|
||||
alias AprsWeb.MapLive.PacketUtils
|
||||
alias AprsWeb.TimeHelpers
|
||||
|
||||
@default_center %{lat: 39.0, lng: -98.0}
|
||||
@default_zoom 4
|
||||
|
|
@ -266,61 +264,30 @@ defmodule AprsWeb.MapLive.CallsignView do
|
|||
def handle_info(:replay_next_packet, socket), do: handle_replay_next_packet(socket)
|
||||
def handle_info(:cleanup_old_packets, socket), do: handle_cleanup_old_packets(socket)
|
||||
|
||||
def handle_info(%Phoenix.Socket.Broadcast{topic: "aprs_messages", event: "packet", payload: payload} = msg, socket) do
|
||||
handle_aprs_packet_broadcast(msg, payload, socket)
|
||||
def handle_info(%Phoenix.Socket.Broadcast{topic: "aprs_messages", event: "packet", payload: packet}, socket) do
|
||||
# Only process packets for the specific callsign being viewed
|
||||
if PacketUtils.generate_callsign(packet) == socket.assigns.callsign do
|
||||
handle_info_postgres_packet(packet, socket)
|
||||
else
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info(_msg, socket), do: {:noreply, socket}
|
||||
|
||||
defp handle_aprs_packet_broadcast(_msg, payload, socket) do
|
||||
sanitized_packet = EncodingUtils.sanitize_packet(payload)
|
||||
sanitized_packet = Map.put_new(sanitized_packet, :received_at, DateTime.utc_now())
|
||||
{lat, lng, _} = MapHelpers.get_coordinates(sanitized_packet)
|
||||
defp handle_info_postgres_packet(packet, socket) do
|
||||
key = System.unique_integer([:positive])
|
||||
updated_visible_packets = Map.put(socket.assigns.visible_packets, key, packet)
|
||||
socket = assign(socket, visible_packets: updated_visible_packets)
|
||||
|
||||
callsign_key =
|
||||
"#{sanitized_packet.base_callsign}#{if sanitized_packet.ssid, do: "-#{sanitized_packet.ssid}", else: ""}"
|
||||
packet_data = PacketUtils.build_packet_data(packet)
|
||||
|
||||
if MapHelpers.has_position_data?(sanitized_packet) and
|
||||
packet_matches_callsign?(sanitized_packet, socket.assigns.callsign) and
|
||||
MapHelpers.within_bounds?(%{lat: lat, lon: lng}, socket.assigns.map_bounds) and
|
||||
packet_within_time_threshold?(
|
||||
sanitized_packet,
|
||||
socket.assigns.packet_age_threshold
|
||||
) do
|
||||
packet_data = build_packet_data(sanitized_packet)
|
||||
socket =
|
||||
if packet_data,
|
||||
do: push_event(socket, "add_markers", %{markers: [packet_data]}),
|
||||
else: socket
|
||||
|
||||
# Remove any previous marker for this callsign
|
||||
socket =
|
||||
if Map.has_key?(socket.assigns.visible_packets, callsign_key) do
|
||||
push_event(socket, "remove_marker", %{id: callsign_key})
|
||||
else
|
||||
socket
|
||||
end
|
||||
|
||||
visible_packets = %{callsign_key => sanitized_packet}
|
||||
|
||||
last_known_position =
|
||||
if lat && lng, do: %{lat: lat, lng: lng}, else: socket.assigns.last_known_position
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> push_event("new_packet", packet_data)
|
||||
|> assign(
|
||||
visible_packets: visible_packets,
|
||||
last_known_position: last_known_position
|
||||
)
|
||||
|
||||
{:noreply, socket}
|
||||
else
|
||||
# Remove marker if it exists and is now out of bounds or expired
|
||||
if Map.has_key?(socket.assigns.visible_packets, callsign_key) do
|
||||
socket = push_event(socket, "remove_marker", %{id: callsign_key})
|
||||
visible_packets = %{}
|
||||
{:noreply, assign(socket, visible_packets: visible_packets)}
|
||||
else
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
defp handle_replay_next_packet(socket) do
|
||||
|
|
@ -338,20 +305,8 @@ defmodule AprsWeb.MapLive.CallsignView do
|
|||
socket.assigns.replay_index < length(socket.assigns.replay_packets)
|
||||
end
|
||||
|
||||
defp handle_replay_packet(nil, socket) do
|
||||
socket =
|
||||
assign(socket,
|
||||
replay_active: false,
|
||||
replay_paused: false,
|
||||
replay_timer_ref: nil,
|
||||
replay_index: 0
|
||||
)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
defp handle_replay_packet(packet, socket) do
|
||||
case build_packet_data(packet) do
|
||||
case PacketUtils.build_packet_data(packet) do
|
||||
nil -> handle_invalid_replay_packet(socket)
|
||||
packet_data -> handle_valid_replay_packet(packet, packet_data, socket)
|
||||
end
|
||||
|
|
@ -445,7 +400,7 @@ defmodule AprsWeb.MapLive.CallsignView do
|
|||
.callsign-header {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
left: 60px;
|
||||
z-index: 1000;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border: 2px solid rgba(0,0,0,0.2);
|
||||
|
|
@ -614,6 +569,11 @@ defmodule AprsWeb.MapLive.CallsignView do
|
|||
max-width: 300px;
|
||||
}
|
||||
|
||||
/* Hides the empty state dialog when not needed */
|
||||
.empty-state.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
margin: 0 0 8px 0;
|
||||
color: #333;
|
||||
|
|
@ -638,14 +598,15 @@ defmodule AprsWeb.MapLive.CallsignView do
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<%= if map_size(@visible_packets) == 0 and not @replay_active do %>
|
||||
<div class="empty-state">
|
||||
<h3>Loading Historical Data</h3>
|
||||
<p>
|
||||
Loading packet history for {@callsign}...
|
||||
</p>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class={[
|
||||
"empty-state",
|
||||
if(map_size(@visible_packets) > 0 or @replay_active, do: "hidden")
|
||||
]}>
|
||||
<h3>Loading Historical Data</h3>
|
||||
<p>
|
||||
Loading packet history for {@callsign}...
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="aprs-map"
|
||||
|
|
@ -749,7 +710,7 @@ defmodule AprsWeb.MapLive.CallsignView do
|
|||
unique_position_packets
|
||||
|> Enum.with_index()
|
||||
|> Enum.map(fn {packet, index} ->
|
||||
case build_packet_data(packet) do
|
||||
case PacketUtils.build_packet_data(packet) do
|
||||
nil ->
|
||||
nil
|
||||
|
||||
|
|
@ -794,7 +755,7 @@ defmodule AprsWeb.MapLive.CallsignView do
|
|||
socket =
|
||||
if latest_packet && !MapSet.member?(historical_callsigns, latest_packet_id) do
|
||||
# Push the latest marker as a regular marker (not historical)
|
||||
packet_data = build_packet_data(latest_packet)
|
||||
packet_data = PacketUtils.build_packet_data(latest_packet)
|
||||
|
||||
if packet_data do
|
||||
push_event(socket, "add_markers", %{markers: [packet_data]})
|
||||
|
|
@ -876,123 +837,6 @@ defmodule AprsWeb.MapLive.CallsignView do
|
|||
abs(lat1 - lat2) > 0.00001 || abs(lng1 - lng2) > 0.00001
|
||||
end
|
||||
|
||||
defp build_packet_data(packet) do
|
||||
{lat, lng, _} = MapHelpers.get_coordinates(packet)
|
||||
callsign = Map.get(packet, :base_callsign, Map.get(packet, "base_callsign", ""))
|
||||
|
||||
if lat != nil and lng != nil and callsign != "" and callsign != nil do
|
||||
build_packet_map(packet, lat, lng, packet.data_extended)
|
||||
end
|
||||
end
|
||||
|
||||
defp format_popup_timestamp(ts) do
|
||||
dt =
|
||||
cond do
|
||||
is_binary(ts) ->
|
||||
case DateTime.from_iso8601(ts) do
|
||||
{:ok, dt, _} -> dt
|
||||
_ -> DateTime.utc_now()
|
||||
end
|
||||
|
||||
is_integer(ts) ->
|
||||
DateTime.from_unix!(ts, :millisecond)
|
||||
|
||||
match?(%DateTime{}, ts) ->
|
||||
ts
|
||||
|
||||
match?(%NaiveDateTime{}, ts) ->
|
||||
DateTime.from_naive!(ts, "Etc/UTC")
|
||||
|
||||
true ->
|
||||
DateTime.utc_now()
|
||||
end
|
||||
|
||||
ago = TimeHelpers.time_ago_in_words(dt)
|
||||
abs_time = Calendar.strftime(dt, "%Y-%m-%d %H:%M UTC")
|
||||
{ago, abs_time}
|
||||
end
|
||||
|
||||
defp build_packet_map(packet, lat, lng, data_extended) do
|
||||
data_extended = data_extended || %{}
|
||||
callsign = PacketUtils.generate_callsign(packet)
|
||||
{symbol_table_id, symbol_code} = PacketUtils.get_symbol_info(packet)
|
||||
timestamp = PacketUtils.get_timestamp(packet)
|
||||
comment = PacketUtils.get_packet_field(packet, :comment, "")
|
||||
|
||||
{ago, abs_time} = format_popup_timestamp(timestamp)
|
||||
|
||||
popup = """
|
||||
<div class=\"aprs-popup\">
|
||||
<div class=\"aprs-callsign\"><strong><a href=\"/#{callsign}\">#{callsign}</a></strong></div>
|
||||
<div class=\"aprs-symbol-info\">Symbol: #{symbol_table_id}#{symbol_code}</div>
|
||||
#{if comment == "", do: "", else: "<div class=\\\"aprs-comment\\\">#{comment}</div>"}
|
||||
<div class=\"aprs-coords\">#{Float.round(PacketUtils.to_float(lat), 4)}, #{Float.round(PacketUtils.to_float(lng), 4)}</div>
|
||||
<div class=\"aprs-timestamp\"><div>#{ago}</div><div>#{abs_time}</div></div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
%{
|
||||
"id" => callsign,
|
||||
"callsign" => callsign,
|
||||
"base_callsign" => PacketUtils.get_packet_field(packet, :base_callsign, ""),
|
||||
"ssid" => PacketUtils.get_packet_field(packet, :ssid, 0),
|
||||
"lat" => PacketUtils.to_float(lat),
|
||||
"lng" => PacketUtils.to_float(lng),
|
||||
"data_type" => to_string(PacketUtils.get_packet_field(packet, :data_type, "unknown")),
|
||||
"path" => PacketUtils.get_packet_field(packet, :path, ""),
|
||||
"comment" => comment,
|
||||
"data_extended" => PacketUtils.convert_tuples_to_strings(data_extended || %{}),
|
||||
"symbol_table_id" => symbol_table_id,
|
||||
"symbol_code" => symbol_code,
|
||||
"symbol_description" => "Symbol: #{symbol_table_id}#{symbol_code}",
|
||||
"timestamp" => timestamp,
|
||||
"popup" => popup
|
||||
}
|
||||
end
|
||||
|
||||
defp packet_matches_callsign?(packet, target_callsign) do
|
||||
normalized_packet = normalize_packet_callsign(packet)
|
||||
normalized_target = normalize_target_callsign(target_callsign)
|
||||
exact_or_base_match?(normalized_packet, normalized_target, packet)
|
||||
end
|
||||
|
||||
defp normalize_packet_callsign(packet) do
|
||||
base_callsign = packet[:base_callsign] || packet.base_callsign || ""
|
||||
ssid = packet[:ssid] || packet.ssid
|
||||
|
||||
case_result =
|
||||
case ssid do
|
||||
nil -> base_callsign
|
||||
"" -> base_callsign
|
||||
"0" -> base_callsign
|
||||
_ -> "#{base_callsign}-#{ssid}"
|
||||
end
|
||||
|
||||
case_result
|
||||
|> String.upcase()
|
||||
|> String.trim()
|
||||
end
|
||||
|
||||
defp normalize_target_callsign(target_callsign) do
|
||||
target_callsign
|
||||
|> String.upcase()
|
||||
|> String.trim()
|
||||
end
|
||||
|
||||
defp exact_or_base_match?(normalized_packet, normalized_target, packet) do
|
||||
cond do
|
||||
normalized_packet == normalized_target ->
|
||||
true
|
||||
|
||||
not String.contains?(normalized_target, "-") ->
|
||||
base_callsign = packet[:base_callsign] || packet.base_callsign || ""
|
||||
String.upcase(String.trim(base_callsign)) == normalized_target
|
||||
|
||||
true ->
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
defp load_callsign_packets(socket, callsign) do
|
||||
latest_packet =
|
||||
%{callsign: callsign}
|
||||
|
|
@ -1042,7 +886,7 @@ defmodule AprsWeb.MapLive.CallsignView do
|
|||
defp maybe_push_latest_marker(socket, nil), do: socket
|
||||
|
||||
defp maybe_push_latest_marker(socket, packet) do
|
||||
packet_data = build_packet_data(packet)
|
||||
packet_data = PacketUtils.build_packet_data(packet)
|
||||
if packet_data, do: push_event(socket, "add_markers", %{markers: [packet_data]}), else: socket
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ defmodule AprsWeb.MapLive.Index do
|
|||
"""
|
||||
use AprsWeb, :live_view
|
||||
|
||||
import AprsWeb.TimeHelpers, only: [time_ago_in_words: 1]
|
||||
|
||||
alias AprsWeb.Endpoint
|
||||
alias AprsWeb.MapLive.MapHelpers
|
||||
alias AprsWeb.MapLive.PacketUtils
|
||||
|
|
@ -195,7 +197,7 @@ defmodule AprsWeb.MapLive.Index do
|
|||
|
||||
visible_packets_list =
|
||||
filtered_packets
|
||||
|> Enum.map(fn {_callsign, packet} -> build_packet_data(packet) end)
|
||||
|> Enum.map(fn {_callsign, packet} -> PacketUtils.build_packet_data(packet) end)
|
||||
|> Enum.filter(& &1)
|
||||
|
||||
socket = assign(socket, visible_packets: filtered_packets)
|
||||
|
|
@ -273,7 +275,7 @@ defmodule AprsWeb.MapLive.Index do
|
|||
{:noreply, socket}
|
||||
else
|
||||
# Navigate to the callsign-specific route
|
||||
{:noreply, push_navigate(socket, to: "/#{trimmed_callsign}")}
|
||||
{:noreply, push_navigate(socket, to: "/map/callsign/#{trimmed_callsign}")}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -307,7 +309,7 @@ defmodule AprsWeb.MapLive.Index do
|
|||
|
||||
@spec handle_bounds_update(map(), Socket.t()) :: {:noreply, Socket.t()}
|
||||
defp handle_bounds_update(bounds, socket) do
|
||||
# Update the map bounds from the client
|
||||
# Update the map bounds from the client, converting to atom keys
|
||||
map_bounds = %{
|
||||
north: bounds["north"],
|
||||
south: bounds["south"],
|
||||
|
|
@ -324,7 +326,6 @@ defmodule AprsWeb.MapLive.Index do
|
|||
# Only schedule a bounds update if the bounds have actually changed (with rounding)
|
||||
if compare_bounds(map_bounds, socket.assigns.map_bounds) do
|
||||
{:noreply, socket}
|
||||
# Cancel any pending bounds update timer
|
||||
else
|
||||
if socket.assigns[:bounds_update_timer] do
|
||||
Process.cancel_timer(socket.assigns.bounds_update_timer)
|
||||
|
|
@ -495,7 +496,7 @@ defmodule AprsWeb.MapLive.Index do
|
|||
else: System.unique_integer([:positive])
|
||||
|
||||
new_visible_packets = Map.put(socket.assigns.visible_packets, callsign_key, packet)
|
||||
marker_data = build_packet_data(packet)
|
||||
marker_data = PacketUtils.build_packet_data(packet)
|
||||
|
||||
socket =
|
||||
if marker_data do
|
||||
|
|
@ -903,38 +904,6 @@ defmodule AprsWeb.MapLive.Index do
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Links (Mobile) -->
|
||||
<div class="lg:hidden pt-4 border-t border-slate-200 space-y-3">
|
||||
<.link
|
||||
navigate={~p"/packets"}
|
||||
class="flex items-center space-x-2 text-sm text-blue-600 hover:text-blue-800 transition-colors"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
|
||||
/>
|
||||
</svg>
|
||||
<span>View All Packets</span>
|
||||
</.link>
|
||||
<.link
|
||||
navigate={~p"/badpackets"}
|
||||
class="flex items-center space-x-2 text-sm text-blue-600 hover:text-blue-800 transition-colors"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.082 16.5c-.77.833.192 2.5 1.732 2.5z"
|
||||
/>
|
||||
</svg>
|
||||
<span>View Bad Packets</span>
|
||||
</.link>
|
||||
</div>
|
||||
|
||||
<!-- Deployment Information -->
|
||||
<div class="pt-4 border-t border-slate-200 space-y-3">
|
||||
<div class="flex items-center space-x-2 text-sm text-slate-600">
|
||||
|
|
@ -1079,7 +1048,7 @@ defmodule AprsWeb.MapLive.Index do
|
|||
|
||||
packet_data_list =
|
||||
historical_packets
|
||||
|> Enum.group_by(&generate_callsign/1)
|
||||
|> Enum.group_by(&PacketUtils.generate_callsign/1)
|
||||
|> Enum.flat_map(fn {callsign, packets} ->
|
||||
sorted_packets =
|
||||
Enum.sort_by(
|
||||
|
|
@ -1120,7 +1089,7 @@ defmodule AprsWeb.MapLive.Index do
|
|||
end
|
||||
|
||||
defp build_historical_packet_data(packet, index, callsign) do
|
||||
case build_packet_data(packet) do
|
||||
case PacketUtils.build_packet_data(packet) do
|
||||
nil ->
|
||||
nil
|
||||
|
||||
|
|
@ -1208,7 +1177,7 @@ defmodule AprsWeb.MapLive.Index do
|
|||
packets = packets_module.get_packets_for_replay(packets_params)
|
||||
|
||||
# Sort packets by received_at timestamp to ensure chronological replay
|
||||
Enum.sort_by(packets, fn packet -> packet.received_at end)
|
||||
Enum.sort_by(packets, & &1.received_at)
|
||||
end
|
||||
|
||||
@spec load_historical_packets_for_bounds(Socket.t(), map()) :: Socket.t()
|
||||
|
|
@ -1258,146 +1227,6 @@ defmodule AprsWeb.MapLive.Index do
|
|||
end
|
||||
end
|
||||
|
||||
@spec build_packet_data(map() | struct()) :: map() | nil
|
||||
defp build_packet_data(packet) do
|
||||
{lat, lon, data_extended} = MapHelpers.get_coordinates(packet)
|
||||
callsign = Map.get(packet, :base_callsign, Map.get(packet, "base_callsign", ""))
|
||||
|
||||
# Only include packets with valid position data and a non-empty callsign
|
||||
if lat && lon && callsign != "" && callsign != nil do
|
||||
build_packet_map(packet, lat, lon, data_extended)
|
||||
end
|
||||
end
|
||||
|
||||
@spec build_packet_map(map() | struct(), number(), number(), map() | nil) :: map()
|
||||
defp build_packet_map(packet, lat, lon, data_extended) do
|
||||
data_extended = data_extended || %{}
|
||||
packet_info = extract_packet_info(packet, data_extended)
|
||||
popup = build_popup_content(packet, packet_info, lat, lon)
|
||||
|
||||
build_packet_result(packet, packet_info, lat, lon, popup)
|
||||
end
|
||||
|
||||
defp extract_packet_info(packet, data_extended) do
|
||||
%{
|
||||
callsign: PacketUtils.generate_callsign(packet),
|
||||
symbol_table_id: PacketUtils.get_packet_field(packet, :symbol_table_id, "/"),
|
||||
symbol_code: PacketUtils.get_packet_field(packet, :symbol_code, ">"),
|
||||
timestamp: PacketUtils.get_timestamp(packet),
|
||||
comment: PacketUtils.get_packet_field(packet, :comment, ""),
|
||||
safe_data_extended: PacketUtils.convert_tuples_to_strings(data_extended),
|
||||
is_weather_packet: PacketUtils.weather_packet?(packet)
|
||||
}
|
||||
end
|
||||
|
||||
defp build_popup_content(packet, packet_info, lat, lon) do
|
||||
if packet_info.is_weather_packet do
|
||||
build_weather_popup_html(packet, packet_info.callsign)
|
||||
else
|
||||
build_standard_popup_html(packet_info, lat, lon)
|
||||
end
|
||||
end
|
||||
|
||||
defp build_standard_popup_html(packet_info, lat, lon) do
|
||||
comment_html =
|
||||
if packet_info.comment == "",
|
||||
do: "",
|
||||
else: ~s(<div class="aprs-comment">#{packet_info.comment}</div>)
|
||||
|
||||
"""
|
||||
<div class="aprs-popup">
|
||||
<div class="aprs-callsign"><strong><a href="/map/callsign/#{packet_info.callsign}">#{packet_info.callsign}</a></strong></div>
|
||||
#{comment_html}
|
||||
<div class="aprs-coords">#{Float.round(PacketUtils.to_float(lat), 4)}, #{Float.round(PacketUtils.to_float(lon), 4)}</div>
|
||||
<div class="aprs-time">#{format_popup_timestamp(packet_info.timestamp)}</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp build_packet_result(packet, packet_info, lat, lon, popup) do
|
||||
%{
|
||||
"id" => packet_info.callsign,
|
||||
"callsign" => packet_info.callsign,
|
||||
"base_callsign" => PacketUtils.get_packet_field(packet, :base_callsign, ""),
|
||||
"ssid" => PacketUtils.get_packet_field(packet, :ssid, 0),
|
||||
"lat" => PacketUtils.to_float(lat),
|
||||
"lng" => PacketUtils.to_float(lon),
|
||||
"data_type" => to_string(PacketUtils.get_packet_field(packet, :data_type, "unknown")),
|
||||
"path" => PacketUtils.get_packet_field(packet, :path, ""),
|
||||
"comment" => packet_info.comment,
|
||||
"data_extended" => packet_info.safe_data_extended || %{},
|
||||
"symbol_table_id" => packet_info.symbol_table_id,
|
||||
"symbol_code" => packet_info.symbol_code,
|
||||
"symbol_description" => "Symbol: #{packet_info.symbol_table_id}#{packet_info.symbol_code}",
|
||||
"timestamp" => packet_info.timestamp,
|
||||
"popup" => popup
|
||||
}
|
||||
end
|
||||
|
||||
defp build_weather_popup_html(packet, callsign) do
|
||||
received_at = get_received_at(packet)
|
||||
timestamp_str = format_popup_timestamp(received_at)
|
||||
|
||||
"""
|
||||
<strong>#{callsign} - Weather Report</strong><br>
|
||||
<small>#{timestamp_str}</small>
|
||||
<hr>
|
||||
Temperature: #{PacketUtils.get_weather_field(packet, :temperature)}°F<br>
|
||||
Humidity: #{PacketUtils.get_weather_field(packet, :humidity)}%<br>
|
||||
Wind: #{PacketUtils.get_weather_field(packet, :wind_direction)}° at #{PacketUtils.get_weather_field(packet, :wind_speed)} mph, gusts to #{PacketUtils.get_weather_field(packet, :wind_gust)} mph<br>
|
||||
Pressure: #{PacketUtils.get_weather_field(packet, :pressure)} hPa<br>
|
||||
Rain (1h): #{PacketUtils.get_weather_field(packet, :rain_1h)} in.<br>
|
||||
Rain (24h): #{PacketUtils.get_weather_field(packet, :rain_24h)} in.<br>
|
||||
Rain (since midnight): #{PacketUtils.get_weather_field(packet, :rain_since_midnight)} in.<br>
|
||||
"""
|
||||
end
|
||||
|
||||
defp get_received_at(packet) do
|
||||
cond do
|
||||
Map.has_key?(packet, :received_at) -> packet.received_at
|
||||
Map.has_key?(packet, "received_at") -> packet["received_at"]
|
||||
true -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp format_popup_timestamp(ts) do
|
||||
cond do
|
||||
is_binary(ts) ->
|
||||
case DateTime.from_iso8601(ts) do
|
||||
{:ok, dt, _} -> Calendar.strftime(dt, "%Y-%m-%d %H:%M:%S UTC")
|
||||
_ -> ts
|
||||
end
|
||||
|
||||
is_integer(ts) ->
|
||||
ts
|
||||
|> DateTime.from_unix!(:millisecond)
|
||||
|> Calendar.strftime("%Y-%m-%d %H:%M:%S UTC")
|
||||
|
||||
match?(%DateTime{}, ts) ->
|
||||
Calendar.strftime(ts, "%Y-%m-%d %H:%M:%S UTC")
|
||||
|
||||
match?(%NaiveDateTime{}, ts) ->
|
||||
ts
|
||||
|> DateTime.from_naive!("Etc/UTC")
|
||||
|> Calendar.strftime("%Y-%m-%d %H:%M:%S UTC")
|
||||
|
||||
true ->
|
||||
to_string(ts)
|
||||
end
|
||||
end
|
||||
|
||||
@spec generate_callsign(map() | struct()) :: String.t()
|
||||
defp generate_callsign(packet) do
|
||||
base_callsign = Map.get(packet, :base_callsign, Map.get(packet, "base_callsign", ""))
|
||||
ssid = Map.get(packet, :ssid, Map.get(packet, "ssid", 0))
|
||||
|
||||
if ssid != 0 and ssid != "" and ssid != nil do
|
||||
"#{base_callsign}-#{ssid}"
|
||||
else
|
||||
base_callsign
|
||||
end
|
||||
end
|
||||
|
||||
# Get location from IP using ip-api.com
|
||||
@spec get_ip_location(String.t() | nil) :: {float(), float()} | nil
|
||||
defp get_ip_location(nil), do: nil
|
||||
|
|
@ -1469,7 +1298,4 @@ defmodule AprsWeb.MapLive.Index do
|
|||
round4.(Map.get(b1, key)) == round4.(Map.get(b2, key))
|
||||
end)
|
||||
end
|
||||
|
||||
# Use shared helper
|
||||
defp time_ago_in_words(datetime), do: AprsWeb.TimeHelpers.time_ago_in_words(datetime)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ defmodule AprsWeb.MapLive.PacketUtils do
|
|||
Shared utilities for extracting and processing packet data in map views.
|
||||
"""
|
||||
|
||||
alias AprsWeb.MapLive.MapHelpers
|
||||
alias AprsWeb.TimeHelpers
|
||||
|
||||
@doc """
|
||||
Safely extracts a value from a packet or data_extended map with fallback support.
|
||||
Checks both atom and string keys, and provides a default value.
|
||||
|
|
@ -134,4 +137,129 @@ defmodule AprsWeb.MapLive.PacketUtils do
|
|||
Map.get(data_extended, key) ||
|
||||
Map.get(data_extended, to_string(key)) || "N/A"
|
||||
end
|
||||
|
||||
def build_packet_data(packet) do
|
||||
{lat, lon, data_extended} = MapHelpers.get_coordinates(packet)
|
||||
callsign = generate_callsign(packet)
|
||||
|
||||
if lat && lon && callsign != "" && callsign != nil do
|
||||
build_packet_map(packet, lat, lon, data_extended)
|
||||
end
|
||||
end
|
||||
|
||||
defp build_packet_map(packet, lat, lon, data_extended) do
|
||||
data_extended = data_extended || %{}
|
||||
packet_info = extract_packet_info(packet, data_extended)
|
||||
popup = build_popup_content(packet, packet_info, lat, lon)
|
||||
|
||||
build_packet_result(packet, packet_info, lat, lon, popup)
|
||||
end
|
||||
|
||||
defp extract_packet_info(packet, data_extended) do
|
||||
%{
|
||||
callsign: generate_callsign(packet),
|
||||
symbol_table_id: get_packet_field(packet, :symbol_table_id, "/"),
|
||||
symbol_code: get_packet_field(packet, :symbol_code, ">"),
|
||||
timestamp: get_timestamp(packet),
|
||||
comment: get_packet_field(packet, :comment, ""),
|
||||
safe_data_extended: convert_tuples_to_strings(data_extended),
|
||||
is_weather_packet: weather_packet?(packet)
|
||||
}
|
||||
end
|
||||
|
||||
defp build_popup_content(packet, packet_info, lat, lon) do
|
||||
if packet_info.is_weather_packet do
|
||||
build_weather_popup_html(packet, packet_info.callsign)
|
||||
else
|
||||
build_standard_popup_html(packet_info, lat, lon)
|
||||
end
|
||||
end
|
||||
|
||||
defp build_standard_popup_html(packet_info, lat, lon) do
|
||||
comment_html =
|
||||
if packet_info.comment == "",
|
||||
do: "",
|
||||
else: ~s(<div class="aprs-comment">#{packet_info.comment}</div>)
|
||||
|
||||
timestamp_dt = TimeHelpers.to_datetime(packet_info.timestamp)
|
||||
|
||||
timestamp_html =
|
||||
if timestamp_dt do
|
||||
"""
|
||||
<div class="aprs-timestamp">
|
||||
<div>#{TimeHelpers.time_ago_in_words(timestamp_dt)}</div>
|
||||
<div class="text-slate-400">#{Calendar.strftime(timestamp_dt, "%Y-%m-%d %H:%M:%S UTC")}</div>
|
||||
</div>
|
||||
"""
|
||||
else
|
||||
""
|
||||
end
|
||||
|
||||
"""
|
||||
<div class="aprs-popup">
|
||||
<div class="aprs-callsign"><strong><a href="/map/callsign/#{packet_info.callsign}">#{packet_info.callsign}</a></strong></div>
|
||||
#{comment_html}
|
||||
<div class="aprs-coords">#{Float.round(to_float(lat), 4)}, #{Float.round(to_float(lon), 4)}</div>
|
||||
#{timestamp_html}
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp build_weather_popup_html(packet, callsign) do
|
||||
received_at = get_received_at(packet)
|
||||
timestamp_dt = TimeHelpers.to_datetime(received_at)
|
||||
|
||||
timestamp_html =
|
||||
if timestamp_dt do
|
||||
"""
|
||||
<div class="aprs-timestamp" style="font-size: 11px; color: #6b7280; padding-top: 0; padding-bottom: 4px;">
|
||||
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 12px; line-height: 1.4;">#{TimeHelpers.time_ago_in_words(timestamp_dt)}</div>
|
||||
<div style="font-family: monospace;">#{Calendar.strftime(timestamp_dt, "%Y-%m-%d %H:%M:%S UTC")}</div>
|
||||
</div>
|
||||
"""
|
||||
else
|
||||
""
|
||||
end
|
||||
|
||||
"""
|
||||
<strong>#{callsign} - Weather Report</strong>
|
||||
#{timestamp_html}
|
||||
<hr style="margin-top: 4px; margin-bottom: 4px;">
|
||||
Temperature: #{get_weather_field(packet, :temperature)}°F<br>
|
||||
Humidity: #{get_weather_field(packet, :humidity)}%<br>
|
||||
Wind: #{get_weather_field(packet, :wind_direction)}° at #{get_weather_field(packet, :wind_speed)} mph, gusts to #{get_weather_field(packet, :wind_gust)} mph<br>
|
||||
Pressure: #{get_weather_field(packet, :pressure)} hPa<br>
|
||||
Rain (1h): #{get_weather_field(packet, :rain_1h)} in.<br>
|
||||
Rain (24h): #{get_weather_field(packet, :rain_24h)} in.<br>
|
||||
Rain (since midnight): #{get_weather_field(packet, :rain_since_midnight)} in.<br>
|
||||
"""
|
||||
end
|
||||
|
||||
defp build_packet_result(packet, packet_info, lat, lon, popup) do
|
||||
%{
|
||||
"id" => packet_info.callsign,
|
||||
"callsign" => packet_info.callsign,
|
||||
"base_callsign" => get_packet_field(packet, :base_callsign, ""),
|
||||
"ssid" => get_packet_field(packet, :ssid, 0),
|
||||
"lat" => to_float(lat),
|
||||
"lng" => to_float(lon),
|
||||
"data_type" => to_string(get_packet_field(packet, :data_type, "unknown")),
|
||||
"path" => get_packet_field(packet, :path, ""),
|
||||
"comment" => packet_info.comment,
|
||||
"data_extended" => packet_info.safe_data_extended || %{},
|
||||
"symbol_table_id" => packet_info.symbol_table_id,
|
||||
"symbol_code" => packet_info.symbol_code,
|
||||
"symbol_description" => "Symbol: #{packet_info.symbol_table_id}#{packet_info.symbol_code}",
|
||||
"timestamp" => packet_info.timestamp,
|
||||
"popup" => popup
|
||||
}
|
||||
end
|
||||
|
||||
defp get_received_at(packet) do
|
||||
cond do
|
||||
Map.has_key?(packet, :received_at) -> packet.received_at
|
||||
Map.has_key?(packet, "received_at") -> packet["received_at"]
|
||||
true -> nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -26,4 +26,31 @@ defmodule AprsWeb.TimeHelpers do
|
|||
|
||||
defp format_time_diff(seconds) when seconds < 63_072_000, do: "1 year"
|
||||
defp format_time_diff(seconds), do: "#{div(seconds, 31_536_000)} years"
|
||||
|
||||
@doc """
|
||||
Converts various timestamp formats into a standard DateTime object.
|
||||
Handles ISO 8601 strings, Unix timestamps in milliseconds, and existing
|
||||
DateTime or NaiveDateTime structs.
|
||||
"""
|
||||
def to_datetime(ts) do
|
||||
cond do
|
||||
is_binary(ts) ->
|
||||
case DateTime.from_iso8601(ts) do
|
||||
{:ok, dt, _} -> dt
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
is_integer(ts) ->
|
||||
DateTime.from_unix!(ts, :millisecond)
|
||||
|
||||
match?(%DateTime{}, ts) ->
|
||||
ts
|
||||
|
||||
match?(%NaiveDateTime{}, ts) ->
|
||||
DateTime.from_naive!(ts, "Etc/UTC")
|
||||
|
||||
true ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue