248 lines
6.9 KiB
Text
248 lines
6.9 KiB
Text
defmodule AprsWeb.MapLive.Index do
|
|
@moduledoc """
|
|
LiveView for displaying real-time APRS packets on a map
|
|
"""
|
|
use AprsWeb, :live_view
|
|
|
|
alias Aprs.EncodingUtils
|
|
alias AprsWeb.Endpoint
|
|
alias Parser.Types.MicE
|
|
|
|
@impl true
|
|
def mount(_params, _session, socket) do
|
|
if connected?(socket) do
|
|
Endpoint.subscribe("aprs_messages")
|
|
end
|
|
|
|
{:ok,
|
|
assign(socket,
|
|
packets: [],
|
|
packet_count: 0,
|
|
page_title: "APRS Map",
|
|
# Default bounds for USA
|
|
map_bounds: %{
|
|
north: 49.0,
|
|
south: 24.0,
|
|
east: -66.0,
|
|
west: -125.0
|
|
}
|
|
)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("update_bounds", %{"bounds" => bounds}, socket) do
|
|
# Update the map bounds from the client
|
|
map_bounds = %{
|
|
north: bounds["north"],
|
|
south: bounds["south"],
|
|
east: bounds["east"],
|
|
west: bounds["west"]
|
|
}
|
|
|
|
# Clear existing markers when bounds change
|
|
socket = push_event(socket, "clear_markers", %{})
|
|
|
|
{:noreply, assign(socket, map_bounds: map_bounds, packet_count: 0)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_info(%{event: "packet", payload: payload}, socket) do
|
|
# Sanitize the packet to prevent encoding errors
|
|
sanitized_packet = EncodingUtils.sanitize_packet(payload)
|
|
|
|
# Log packet type for debugging
|
|
IO.inspect(sanitized_packet.data_type, label: "Packet type")
|
|
|
|
if sanitized_packet.data_extended do
|
|
# Check if data_extended is a struct before accessing __struct__
|
|
case sanitized_packet.data_extended do
|
|
%{__struct__: module} -> IO.inspect(module, label: "Data extended type")
|
|
_ -> IO.inspect("Plain map", label: "Data extended type")
|
|
end
|
|
end
|
|
|
|
# Only process packets with position data that are within current map bounds
|
|
if has_position_data?(sanitized_packet) && within_bounds?(sanitized_packet, socket.assigns.map_bounds) do
|
|
# Convert to a simple map structure for JSON encoding
|
|
packet_data = build_packet_data(sanitized_packet)
|
|
|
|
# Only push if we have valid packet data
|
|
if packet_data do
|
|
# Push the packet to the client-side JavaScript and increment count
|
|
socket =
|
|
socket
|
|
|> push_event("new_packet", packet_data)
|
|
|> update(:packet_count, &(&1 + 1))
|
|
|
|
{:noreply, socket}
|
|
else
|
|
# Invalid packet data, skip it
|
|
{:noreply, socket}
|
|
end
|
|
else
|
|
# Ignore packets without position data or outside bounds
|
|
{:noreply, socket}
|
|
end
|
|
end
|
|
|
|
@impl true
|
|
def render(assigns) do
|
|
~H"""
|
|
<link
|
|
rel="stylesheet"
|
|
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
|
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
|
crossorigin=""
|
|
/>
|
|
<script
|
|
src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
|
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
|
crossorigin=""
|
|
>
|
|
</script>
|
|
|
|
<style>
|
|
#aprs-map {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
z-index: 1;
|
|
}
|
|
|
|
.map-overlay {
|
|
position: absolute;
|
|
top: 10px;
|
|
right: 10px;
|
|
background: rgba(255, 255, 255, 0.9);
|
|
padding: 10px 15px;
|
|
border-radius: 5px;
|
|
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
|
z-index: 1000;
|
|
}
|
|
|
|
.packet-counter {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: #333;
|
|
}
|
|
|
|
.aprs-marker {
|
|
background: transparent !important;
|
|
border: none !important;
|
|
}
|
|
</style>
|
|
|
|
<div id="aprs-map" phx-hook="APRSMap"></div>
|
|
|
|
<div class="map-overlay">
|
|
<div class="packet-counter">
|
|
<span id="packet-count">{@packet_count}</span> packets in view
|
|
</div>
|
|
</div>
|
|
"""
|
|
end
|
|
|
|
# Helper functions
|
|
|
|
defp has_position_data?(packet) do
|
|
case packet.data_extended do
|
|
%MicE{} = mic_e ->
|
|
# MicE packets have lat/lon in separate components
|
|
is_number(mic_e.lat_degrees) && is_number(mic_e.lat_minutes) &&
|
|
is_number(mic_e.lon_degrees) && is_number(mic_e.lon_minutes)
|
|
|
|
%{latitude: lat, longitude: lon} ->
|
|
# Regular position packets have decimal lat/lon
|
|
is_number(lat) && is_number(lon)
|
|
|
|
_ ->
|
|
false
|
|
end
|
|
end
|
|
|
|
defp within_bounds?(packet, bounds) do
|
|
{lat, lng} = get_coordinates(packet)
|
|
|
|
lat && lng &&
|
|
lat >= bounds.south && lat <= bounds.north &&
|
|
lng >= bounds.west && lng <= bounds.east
|
|
end
|
|
|
|
defp get_coordinates(packet) do
|
|
case packet.data_extended do
|
|
%MicE{} = mic_e ->
|
|
# Convert MicE components to decimal degrees
|
|
lat = mic_e.lat_degrees + mic_e.lat_minutes / 60.0 + mic_e.lat_fractional / 6000.0
|
|
lat = if mic_e.lat_direction == :south, do: -lat, else: lat
|
|
|
|
lng = mic_e.lon_degrees + mic_e.lon_minutes / 60.0 + mic_e.lon_fractional / 6000.0
|
|
lng = if mic_e.lon_direction == :west, do: -lng, else: lng
|
|
|
|
# Validate coordinates are within valid ranges
|
|
if lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180 do
|
|
{lat, lng}
|
|
else
|
|
IO.puts("Invalid MicE coordinates: lat=#{lat}, lng=#{lng}")
|
|
{nil, nil}
|
|
end
|
|
|
|
%{latitude: lat, longitude: lon} ->
|
|
{lat, lon}
|
|
|
|
_ ->
|
|
{nil, nil}
|
|
end
|
|
end
|
|
|
|
defp build_packet_data(packet) do
|
|
data_extended = build_data_extended(packet.data_extended)
|
|
|
|
if data_extended do
|
|
%{
|
|
"base_callsign" => packet.base_callsign || "",
|
|
"ssid" => packet.ssid || "",
|
|
"data_type" => to_string(packet.data_type || "unknown"),
|
|
"path" => packet.path || "",
|
|
"data_extended" => data_extended
|
|
}
|
|
end
|
|
end
|
|
|
|
defp build_data_extended(nil), do: nil
|
|
|
|
defp build_data_extended(data_extended) do
|
|
case data_extended do
|
|
%MicE{} = mic_e ->
|
|
# Convert MicE components to decimal degrees
|
|
lat = mic_e.lat_degrees + mic_e.lat_minutes / 60.0 + mic_e.lat_fractional / 6000.0
|
|
lat = if mic_e.lat_direction == :south, do: -lat, else: lat
|
|
|
|
lng = mic_e.lon_degrees + mic_e.lon_minutes / 60.0 + mic_e.lon_fractional / 6000.0
|
|
lng = if mic_e.lon_direction == :west, do: -lng, else: lng
|
|
|
|
# Validate coordinates are within valid ranges
|
|
if lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180 do
|
|
%{
|
|
"latitude" => lat,
|
|
"longitude" => lng,
|
|
"comment" => mic_e.message || "",
|
|
"symbol_table_id" => "/",
|
|
"symbol_code" => ">",
|
|
"aprs_messaging" => false
|
|
}
|
|
end
|
|
|
|
_ ->
|
|
%{
|
|
"latitude" => data_extended[:latitude],
|
|
"longitude" => data_extended[:longitude],
|
|
"comment" => data_extended[:comment] || "",
|
|
"symbol_table_id" => data_extended[:symbol_table_id] || "/",
|
|
"symbol_code" => data_extended[:symbol_code] || ">",
|
|
"aprs_messaging" => data_extended[:aprs_messaging] || false
|
|
}
|
|
end
|
|
end
|
|
end
|