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 @default_center %{lat: 39.8283, lng: -98.5795} @default_zoom 5 @ip_api_url "http://ip-api.com/json/" @finch_name Aprs.Finch @impl true def mount(_params, _session, socket) do socket = assign(socket, packets: [], packet_count: 0, page_title: "APRS Map", # Track visible packets by callsign visible_packets: %{}, # Default bounds for USA map_bounds: %{ north: 49.0, south: 24.0, east: -66.0, west: -125.0 }, map_center: @default_center, map_zoom: @default_zoom ) if connected?(socket) do Endpoint.subscribe("aprs_messages") # Get IP-based location on initial load ip = case socket.private[:connect_info][:peer_data][:address] do {a, b, c, d} -> "#{a}.#{b}.#{c}.#{d}" {a, b, c, d, e, f, g, h} -> "#{a}:#{b}:#{c}:#{d}:#{e}:#{f}:#{g}:#{h}" _ -> nil end if ip, do: Task.async(fn -> get_ip_location(ip) end) end {:ok, socket} 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"] } # Don't clear markers - let the client preserve those still in view # Recalculate packet count based on new bounds visible_packets = socket.assigns.visible_packets |> Enum.filter(fn {_callsign, packet} -> within_bounds?(packet, map_bounds) end) |> Map.new() packet_count = map_size(visible_packets) {:noreply, assign(socket, map_bounds: map_bounds, visible_packets: visible_packets, packet_count: packet_count)} end @impl true def handle_event("update_packet_count", %{"count" => count}, socket) do # Update packet count from client after markers are removed {:noreply, assign(socket, packet_count: count)} end @impl true def handle_event("locate_me", _params, socket) do # Send JavaScript command to request browser geolocation {:noreply, push_event(socket, "request_geolocation", %{})} end @impl true def handle_event("set_location", %{"lat" => lat, "lng" => lng}, socket) do # Update map center and zoom when location is received {:noreply, assign(socket, map_center: %{lat: lat, lng: lng}, map_zoom: 12)} end @impl true def handle_info(msg, socket) do case msg do {:ip_location, %{lat: lat, lng: lng}} -> {:noreply, assign(socket, map_center: %{lat: lat, lng: lng}, map_zoom: 12)} %{event: "packet", payload: payload} -> # 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 # Generate a unique key for this packet callsign_key = "#{sanitized_packet.base_callsign}#{if sanitized_packet.ssid, do: "-#{sanitized_packet.ssid}", else: ""}" # Update visible packets tracking visible_packets = Map.put(socket.assigns.visible_packets, callsign_key, sanitized_packet) packet_count = map_size(visible_packets) # Push the packet to the client-side JavaScript socket = socket |> push_event("new_packet", packet_data) |> assign(visible_packets: visible_packets, packet_count: packet_count) {:noreply, socket} else # Invalid packet data, skip it {:noreply, socket} end else # Ignore packets without position data or outside bounds {:noreply, socket} end end end @impl true def render(assigns) do ~H"""