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
|
@moduledoc false
|
||||||
use AprsWeb, :live_view
|
use AprsWeb, :live_view
|
||||||
|
|
||||||
alias Aprs.EncodingUtils
|
|
||||||
alias Aprs.Packets
|
alias Aprs.Packets
|
||||||
alias AprsWeb.Endpoint
|
alias AprsWeb.Endpoint
|
||||||
alias AprsWeb.MapLive.MapHelpers
|
alias AprsWeb.MapLive.MapHelpers
|
||||||
alias AprsWeb.MapLive.PacketUtils
|
alias AprsWeb.MapLive.PacketUtils
|
||||||
alias AprsWeb.TimeHelpers
|
|
||||||
|
|
||||||
@default_center %{lat: 39.0, lng: -98.0}
|
@default_center %{lat: 39.0, lng: -98.0}
|
||||||
@default_zoom 4
|
@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(: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(: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
|
def handle_info(%Phoenix.Socket.Broadcast{topic: "aprs_messages", event: "packet", payload: packet}, socket) do
|
||||||
handle_aprs_packet_broadcast(msg, payload, socket)
|
# 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
|
end
|
||||||
|
|
||||||
def handle_info(_msg, socket), do: {:noreply, socket}
|
def handle_info(_msg, socket), do: {:noreply, socket}
|
||||||
|
|
||||||
defp handle_aprs_packet_broadcast(_msg, payload, socket) do
|
defp handle_info_postgres_packet(packet, socket) do
|
||||||
sanitized_packet = EncodingUtils.sanitize_packet(payload)
|
key = System.unique_integer([:positive])
|
||||||
sanitized_packet = Map.put_new(sanitized_packet, :received_at, DateTime.utc_now())
|
updated_visible_packets = Map.put(socket.assigns.visible_packets, key, packet)
|
||||||
{lat, lng, _} = MapHelpers.get_coordinates(sanitized_packet)
|
socket = assign(socket, visible_packets: updated_visible_packets)
|
||||||
|
|
||||||
callsign_key =
|
packet_data = PacketUtils.build_packet_data(packet)
|
||||||
"#{sanitized_packet.base_callsign}#{if sanitized_packet.ssid, do: "-#{sanitized_packet.ssid}", else: ""}"
|
|
||||||
|
|
||||||
if MapHelpers.has_position_data?(sanitized_packet) and
|
socket =
|
||||||
packet_matches_callsign?(sanitized_packet, socket.assigns.callsign) and
|
if packet_data,
|
||||||
MapHelpers.within_bounds?(%{lat: lat, lon: lng}, socket.assigns.map_bounds) and
|
do: push_event(socket, "add_markers", %{markers: [packet_data]}),
|
||||||
packet_within_time_threshold?(
|
else: socket
|
||||||
sanitized_packet,
|
|
||||||
socket.assigns.packet_age_threshold
|
|
||||||
) do
|
|
||||||
packet_data = build_packet_data(sanitized_packet)
|
|
||||||
|
|
||||||
# Remove any previous marker for this callsign
|
{:noreply, socket}
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_replay_next_packet(socket) do
|
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)
|
socket.assigns.replay_index < length(socket.assigns.replay_packets)
|
||||||
end
|
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
|
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)
|
nil -> handle_invalid_replay_packet(socket)
|
||||||
packet_data -> handle_valid_replay_packet(packet, packet_data, socket)
|
packet_data -> handle_valid_replay_packet(packet, packet_data, socket)
|
||||||
end
|
end
|
||||||
|
|
@ -445,7 +400,7 @@ defmodule AprsWeb.MapLive.CallsignView do
|
||||||
.callsign-header {
|
.callsign-header {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
left: 10px;
|
left: 60px;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
background: rgba(255, 255, 255, 0.95);
|
background: rgba(255, 255, 255, 0.95);
|
||||||
border: 2px solid rgba(0,0,0,0.2);
|
border: 2px solid rgba(0,0,0,0.2);
|
||||||
|
|
@ -614,6 +569,11 @@ defmodule AprsWeb.MapLive.CallsignView do
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Hides the empty state dialog when not needed */
|
||||||
|
.empty-state.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.empty-state h3 {
|
.empty-state h3 {
|
||||||
margin: 0 0 8px 0;
|
margin: 0 0 8px 0;
|
||||||
color: #333;
|
color: #333;
|
||||||
|
|
@ -638,14 +598,15 @@ defmodule AprsWeb.MapLive.CallsignView do
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= if map_size(@visible_packets) == 0 and not @replay_active do %>
|
<div class={[
|
||||||
<div class="empty-state">
|
"empty-state",
|
||||||
<h3>Loading Historical Data</h3>
|
if(map_size(@visible_packets) > 0 or @replay_active, do: "hidden")
|
||||||
<p>
|
]}>
|
||||||
Loading packet history for {@callsign}...
|
<h3>Loading Historical Data</h3>
|
||||||
</p>
|
<p>
|
||||||
</div>
|
Loading packet history for {@callsign}...
|
||||||
<% end %>
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
id="aprs-map"
|
id="aprs-map"
|
||||||
|
|
@ -749,7 +710,7 @@ defmodule AprsWeb.MapLive.CallsignView do
|
||||||
unique_position_packets
|
unique_position_packets
|
||||||
|> Enum.with_index()
|
|> Enum.with_index()
|
||||||
|> Enum.map(fn {packet, index} ->
|
|> Enum.map(fn {packet, index} ->
|
||||||
case build_packet_data(packet) do
|
case PacketUtils.build_packet_data(packet) do
|
||||||
nil ->
|
nil ->
|
||||||
nil
|
nil
|
||||||
|
|
||||||
|
|
@ -794,7 +755,7 @@ defmodule AprsWeb.MapLive.CallsignView do
|
||||||
socket =
|
socket =
|
||||||
if latest_packet && !MapSet.member?(historical_callsigns, latest_packet_id) do
|
if latest_packet && !MapSet.member?(historical_callsigns, latest_packet_id) do
|
||||||
# Push the latest marker as a regular marker (not historical)
|
# 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
|
if packet_data do
|
||||||
push_event(socket, "add_markers", %{markers: [packet_data]})
|
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
|
abs(lat1 - lat2) > 0.00001 || abs(lng1 - lng2) > 0.00001
|
||||||
end
|
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
|
defp load_callsign_packets(socket, callsign) do
|
||||||
latest_packet =
|
latest_packet =
|
||||||
%{callsign: callsign}
|
%{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, nil), do: socket
|
||||||
|
|
||||||
defp maybe_push_latest_marker(socket, packet) do
|
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
|
if packet_data, do: push_event(socket, "add_markers", %{markers: [packet_data]}), else: socket
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
"""
|
"""
|
||||||
use AprsWeb, :live_view
|
use AprsWeb, :live_view
|
||||||
|
|
||||||
|
import AprsWeb.TimeHelpers, only: [time_ago_in_words: 1]
|
||||||
|
|
||||||
alias AprsWeb.Endpoint
|
alias AprsWeb.Endpoint
|
||||||
alias AprsWeb.MapLive.MapHelpers
|
alias AprsWeb.MapLive.MapHelpers
|
||||||
alias AprsWeb.MapLive.PacketUtils
|
alias AprsWeb.MapLive.PacketUtils
|
||||||
|
|
@ -195,7 +197,7 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
|
|
||||||
visible_packets_list =
|
visible_packets_list =
|
||||||
filtered_packets
|
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)
|
|> Enum.filter(& &1)
|
||||||
|
|
||||||
socket = assign(socket, visible_packets: filtered_packets)
|
socket = assign(socket, visible_packets: filtered_packets)
|
||||||
|
|
@ -273,7 +275,7 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
else
|
else
|
||||||
# Navigate to the callsign-specific route
|
# Navigate to the callsign-specific route
|
||||||
{:noreply, push_navigate(socket, to: "/#{trimmed_callsign}")}
|
{:noreply, push_navigate(socket, to: "/map/callsign/#{trimmed_callsign}")}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -307,7 +309,7 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
|
|
||||||
@spec handle_bounds_update(map(), Socket.t()) :: {:noreply, Socket.t()}
|
@spec handle_bounds_update(map(), Socket.t()) :: {:noreply, Socket.t()}
|
||||||
defp handle_bounds_update(bounds, socket) do
|
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 = %{
|
map_bounds = %{
|
||||||
north: bounds["north"],
|
north: bounds["north"],
|
||||||
south: bounds["south"],
|
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)
|
# Only schedule a bounds update if the bounds have actually changed (with rounding)
|
||||||
if compare_bounds(map_bounds, socket.assigns.map_bounds) do
|
if compare_bounds(map_bounds, socket.assigns.map_bounds) do
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
# Cancel any pending bounds update timer
|
|
||||||
else
|
else
|
||||||
if socket.assigns[:bounds_update_timer] do
|
if socket.assigns[:bounds_update_timer] do
|
||||||
Process.cancel_timer(socket.assigns.bounds_update_timer)
|
Process.cancel_timer(socket.assigns.bounds_update_timer)
|
||||||
|
|
@ -495,7 +496,7 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
else: System.unique_integer([:positive])
|
else: System.unique_integer([:positive])
|
||||||
|
|
||||||
new_visible_packets = Map.put(socket.assigns.visible_packets, callsign_key, packet)
|
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 =
|
socket =
|
||||||
if marker_data do
|
if marker_data do
|
||||||
|
|
@ -903,38 +904,6 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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 -->
|
<!-- Deployment Information -->
|
||||||
<div class="pt-4 border-t border-slate-200 space-y-3">
|
<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">
|
<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 =
|
packet_data_list =
|
||||||
historical_packets
|
historical_packets
|
||||||
|> Enum.group_by(&generate_callsign/1)
|
|> Enum.group_by(&PacketUtils.generate_callsign/1)
|
||||||
|> Enum.flat_map(fn {callsign, packets} ->
|
|> Enum.flat_map(fn {callsign, packets} ->
|
||||||
sorted_packets =
|
sorted_packets =
|
||||||
Enum.sort_by(
|
Enum.sort_by(
|
||||||
|
|
@ -1120,7 +1089,7 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp build_historical_packet_data(packet, index, callsign) do
|
defp build_historical_packet_data(packet, index, callsign) do
|
||||||
case build_packet_data(packet) do
|
case PacketUtils.build_packet_data(packet) do
|
||||||
nil ->
|
nil ->
|
||||||
nil
|
nil
|
||||||
|
|
||||||
|
|
@ -1208,7 +1177,7 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
packets = packets_module.get_packets_for_replay(packets_params)
|
packets = packets_module.get_packets_for_replay(packets_params)
|
||||||
|
|
||||||
# Sort packets by received_at timestamp to ensure chronological replay
|
# 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
|
end
|
||||||
|
|
||||||
@spec load_historical_packets_for_bounds(Socket.t(), map()) :: Socket.t()
|
@spec load_historical_packets_for_bounds(Socket.t(), map()) :: Socket.t()
|
||||||
|
|
@ -1258,146 +1227,6 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
end
|
end
|
||||||
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
|
# Get location from IP using ip-api.com
|
||||||
@spec get_ip_location(String.t() | nil) :: {float(), float()} | nil
|
@spec get_ip_location(String.t() | nil) :: {float(), float()} | nil
|
||||||
defp get_ip_location(nil), do: 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))
|
round4.(Map.get(b1, key)) == round4.(Map.get(b2, key))
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Use shared helper
|
|
||||||
defp time_ago_in_words(datetime), do: AprsWeb.TimeHelpers.time_ago_in_words(datetime)
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@ defmodule AprsWeb.MapLive.PacketUtils do
|
||||||
Shared utilities for extracting and processing packet data in map views.
|
Shared utilities for extracting and processing packet data in map views.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
alias AprsWeb.MapLive.MapHelpers
|
||||||
|
alias AprsWeb.TimeHelpers
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Safely extracts a value from a packet or data_extended map with fallback support.
|
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.
|
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, key) ||
|
||||||
Map.get(data_extended, to_string(key)) || "N/A"
|
Map.get(data_extended, to_string(key)) || "N/A"
|
||||||
end
|
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
|
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) when seconds < 63_072_000, do: "1 year"
|
||||||
defp format_time_diff(seconds), do: "#{div(seconds, 31_536_000)} years"
|
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
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue