defmodule AprsmeWeb.MapLive.State do @moduledoc """ Manages initial LiveView socket state assignment for the map view. Extracted from Index to reduce module size. """ import Phoenix.Component, only: [assign: 2] import Phoenix.LiveView, only: [connected?: 1, get_connect_params: 1] alias Aprsme.Packets alias AprsmeWeb.Live.Shared.BoundsUtils alias AprsmeWeb.MapLive.PacketBatcher alias AprsmeWeb.MapLive.UrlParams alias Phoenix.LiveView.Socket @doc """ Assign default values to a fresh socket. """ @spec default(Socket.t(), DateTime.t()) :: Socket.t() def default(socket, one_day_ago) do assign(socket, packets: [], visible_packets: %{}, historical_packets: %{}, page_title: "APRS Map", station_popup_open: false, map_bounds: %{ north: 49.0, south: 24.0, east: -66.0, west: -125.0 }, map_center: UrlParams.default_center(), map_zoom: UrlParams.default_zoom(), packet_age_threshold: one_day_ago, map_ready: false, historical_loaded: false, bounds_update_timer: nil, pending_bounds: nil, initial_bounds_loaded: false, # Loading state management historical_loading: false, loading_generation: 0, pending_batch_tasks: [], # Overlay controls overlay_callsign: "", slideover_open: true, # Track when last update occurred for real-time display in map sidebar # Updated when packets are processed or map bounds change last_update_at: DateTime.utc_now(), # Add missing assigns for components loading: false, connection_status: connection_status(), show_all_packets: true ) end @doc """ Finalize mount assigns with computed values. Takes the socket after basic setup and an opts map. """ @spec finalize(Socket.t(), map()) :: Socket.t() def finalize(socket, %{ initial_bounds: initial_bounds, final_map_center: final_map_center, final_map_zoom: final_map_zoom, should_skip_initial_url_update: should_skip_initial_url_update, tracked_callsign: tracked_callsign, deployed_at: deployed_at, one_day_ago: one_day_ago }) do # Don't override trail_duration and historical_hours if they're already set trail_duration = Map.get(socket.assigns, :trail_duration, "1") historical_hours = Map.get(socket.assigns, :historical_hours, "1") packet_age_threshold = Map.get(socket.assigns, :packet_age_threshold, one_day_ago) # If tracking a specific callsign, fetch their latest packet and other SSIDs {tracked_callsign_latest_packet, other_ssids} = if tracked_callsign == "" do {nil, []} else try do packet = Packets.get_latest_packet_for_callsign(tracked_callsign) ssids = Packets.get_other_ssids(tracked_callsign) {packet, ssids} rescue # Handle database connection errors gracefully (especially in tests) DBConnection.OwnershipError -> {nil, []} _ -> {nil, []} end end # Start packet batcher for efficient updates {:ok, batcher_pid} = PacketBatcher.start_link(self()) Process.monitor(batcher_pid) # Determine initial slideover state from client viewport width slideover_open = slideover_open?(socket) assign(socket, map_ready: false, map_bounds: initial_bounds, map_center: final_map_center, map_zoom: final_map_zoom, should_skip_initial_url_update: should_skip_initial_url_update, overlay_callsign: "", tracked_callsign: tracked_callsign, tracked_callsign_latest_packet: tracked_callsign_latest_packet, other_ssids: other_ssids, trail_duration: trail_duration, historical_hours: historical_hours, packet_age_threshold: packet_age_threshold, slideover_open: slideover_open, deployed_at: deployed_at, buffer_timer: nil, batcher_pid: batcher_pid, station_popup_open: false, initial_bounds_loaded: false, # Always load historical data on initial page load needs_initial_historical_load: true, # Loading state management historical_loading: false, loading_generation: 0, pending_batch_tasks: [], # Add missing assigns for components loading: false, connection_status: connection_status(), show_all_packets: true ) end @doc """ Determine whether the slideover should be open on initial render. """ @spec slideover_open?(Socket.t()) :: boolean() def slideover_open?(socket) do if connected?(socket) do viewport_width = get_connect_params(socket)["viewport_width"] || 1024 viewport_width >= 1024 else true end end @doc """ Get the initial APRS connection status. """ @spec connection_status() :: String.t() def connection_status do # Check if APRS connection is disabled connection_disabled = Application.get_env(:aprsme, :disable_aprs_connection, false) if connection_disabled, do: "disconnected", else: "connected" end @doc """ Default map center used by UrlParams. """ @spec default_center() :: %{lat: float(), lng: float()} def default_center, do: UrlParams.default_center() @doc """ Default map zoom used by UrlParams. """ def default_zoom, do: UrlParams.default_zoom() @doc """ Calculate initial bounds from center and zoom. """ @spec initial_bounds(%{lat: float(), lng: float()}, integer()) :: map() def initial_bounds(center, zoom) do BoundsUtils.calculate_bounds_from_center_and_zoom(center, zoom) end end