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"""
""" 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