Some checks failed
Build and Push / Build and Push Docker Image (push) Failing after 2s
- State (177 lines): mount assigns, defaults, slideover, connection status - Events (540 lines): 22 handle_event handlers, rate limiting, URL helpers - Subscriptions (122 lines): PubSub setup, spatial registration, cleanup - BoundsUpdater (172 lines): bounds pipeline, validation, debouncing - index.ex: 2,062 -> 1,202 lines (42% reduction) - Credo: initial_historical_completed, bounds_update_timer added to ignored_assigns (consumed by extracted modules, invisible to credo) - All 361 map_live tests pass across multiple random seeds - Handoff: marked maintainability #1 as done
172 lines
5.7 KiB
Elixir
172 lines
5.7 KiB
Elixir
defmodule AprsmeWeb.MapLive.BoundsUpdater do
|
|
@moduledoc """
|
|
Handles bounds-change processing pipeline for the map LiveView.
|
|
Extracted from Index to reduce module size.
|
|
"""
|
|
|
|
import Phoenix.Component, only: [assign: 2]
|
|
import Phoenix.LiveView, only: [push_event: 3]
|
|
|
|
alias AprsmeWeb.Live.Shared.BoundsUtils
|
|
alias AprsmeWeb.MapLive.DisplayManager
|
|
alias AprsmeWeb.MapLive.HistoricalLoader
|
|
alias AprsmeWeb.MapLive.Subscriptions
|
|
alias Phoenix.LiveView.Socket
|
|
|
|
require Logger
|
|
|
|
@bounds_update_debounce_ms Application.compile_env(:aprsme, :bounds_update_debounce_ms, 400)
|
|
|
|
@doc """
|
|
Main entry point for bounds updates from client events.
|
|
Delegates to debounce pipeline or processes immediately.
|
|
"""
|
|
@spec handle_bounds_update(map(), Socket.t()) :: {:noreply, Socket.t()}
|
|
def handle_bounds_update(bounds, socket) do
|
|
if socket.assigns.historical_loading do
|
|
{:noreply, assign(socket, pending_bounds: bounds)}
|
|
else
|
|
map_bounds = BoundsUtils.normalize_bounds(bounds)
|
|
|
|
if valid_bounds?(map_bounds) do
|
|
handle_valid_bounds_update(map_bounds, socket)
|
|
else
|
|
{:noreply, socket}
|
|
end
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Process a pending bounds update from the debounce timer or from a forced initial load.
|
|
"""
|
|
@spec process_bounds_update(map(), Socket.t()) :: Socket.t()
|
|
def process_bounds_update(map_bounds, socket) do
|
|
Logger.debug("process_bounds_update called with bounds: #{inspect(map_bounds)}")
|
|
|
|
Subscriptions.update_viewport(socket, map_bounds)
|
|
|
|
bounds_state = bounds_update_state(socket, map_bounds)
|
|
log_bounds_update_state(bounds_state)
|
|
|
|
{new_visible_packets, packets_to_remove} = filtered_packets_for_bounds(socket.assigns.visible_packets, map_bounds)
|
|
|
|
socket =
|
|
socket
|
|
|> DisplayManager.remove_markers_batch(packets_to_remove)
|
|
|> maybe_clear_historical_packets(bounds_state)
|
|
|> push_event("filter_markers_by_bounds", %{bounds: map_bounds})
|
|
|> assign(map_bounds: map_bounds, visible_packets: new_visible_packets)
|
|
|> assign(needs_initial_historical_load: false)
|
|
|
|
log_historical_reload(socket)
|
|
|
|
socket = HistoricalLoader.start_progressive_historical_loading(socket)
|
|
|
|
# If tracking a callsign and at low zoom, send trail line instead of letting
|
|
# historical loader send heat map
|
|
socket =
|
|
if socket.assigns.tracked_callsign != "" and socket.assigns.map_zoom <= 8 do
|
|
DisplayManager.send_trail_line_for_tracked_callsign(socket)
|
|
else
|
|
socket
|
|
end
|
|
|
|
# Mark initial historical as completed if this was the initial load
|
|
if bounds_state.is_initial_load do
|
|
assign(socket, initial_historical_completed: true)
|
|
else
|
|
socket
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Schedule a debounced bounds update. Cancels any pending timer first.
|
|
"""
|
|
@spec schedule_bounds_update(map(), Socket.t()) :: {:noreply, Socket.t()}
|
|
def schedule_bounds_update(map_bounds, socket) do
|
|
if socket.assigns[:bounds_update_timer] do
|
|
case Process.cancel_timer(socket.assigns.bounds_update_timer) do
|
|
false ->
|
|
receive do
|
|
{:process_bounds_update, _} -> :ok
|
|
after
|
|
0 -> :ok
|
|
end
|
|
|
|
_ ->
|
|
:ok
|
|
end
|
|
end
|
|
|
|
socket = HistoricalLoader.cancel_pending_loads(socket)
|
|
|
|
timer_ref = Process.send_after(self(), {:process_bounds_update, map_bounds}, @bounds_update_debounce_ms)
|
|
{:noreply, assign(socket, bounds_update_timer: timer_ref, pending_bounds: map_bounds)}
|
|
end
|
|
|
|
# --- Private helpers ---
|
|
|
|
defp valid_bounds?(map_bounds) do
|
|
map_bounds.north <= 90 and map_bounds.south >= -90 and map_bounds.north > map_bounds.south
|
|
end
|
|
|
|
defp handle_valid_bounds_update(map_bounds, socket) do
|
|
cond do
|
|
socket.assigns[:needs_initial_historical_load] ->
|
|
Logger.debug("Processing initial bounds update immediately (forced): #{inspect(map_bounds)}")
|
|
socket = process_bounds_update(map_bounds, socket)
|
|
{:noreply, socket}
|
|
|
|
BoundsUtils.compare_bounds(map_bounds, socket.assigns.map_bounds) ->
|
|
{:noreply, socket}
|
|
|
|
true ->
|
|
schedule_bounds_update(map_bounds, socket)
|
|
end
|
|
end
|
|
|
|
defp bounds_update_state(socket, map_bounds) do
|
|
%{
|
|
is_initial_load: socket.assigns[:needs_initial_historical_load] || !socket.assigns[:initial_bounds_loaded],
|
|
bounds_changed:
|
|
socket.assigns.map_bounds && not BoundsUtils.compare_bounds(map_bounds, socket.assigns.map_bounds),
|
|
initial_historical_completed: socket.assigns[:initial_historical_completed] || false
|
|
}
|
|
end
|
|
|
|
defp log_bounds_update_state(bounds_state) do
|
|
Logger.debug(
|
|
"is_initial_load: #{bounds_state.is_initial_load}, bounds_changed: #{bounds_state.bounds_changed}, " <>
|
|
"initial_historical_completed: #{bounds_state.initial_historical_completed}"
|
|
)
|
|
end
|
|
|
|
defp filtered_packets_for_bounds(visible_packets, map_bounds) do
|
|
{
|
|
BoundsUtils.filter_packets_by_bounds(visible_packets, map_bounds),
|
|
BoundsUtils.reject_packets_by_bounds(visible_packets, map_bounds)
|
|
}
|
|
end
|
|
|
|
defp maybe_clear_historical_packets(socket, %{
|
|
bounds_changed: true,
|
|
is_initial_load: false,
|
|
initial_historical_completed: true
|
|
}) do
|
|
Logger.debug("Bounds changed after initial load - clearing historical packets")
|
|
push_event(socket, "clear_historical_packets", %{})
|
|
end
|
|
|
|
defp maybe_clear_historical_packets(socket, _bounds_state) do
|
|
Logger.debug("Initial load or no significant change - keeping existing markers")
|
|
socket
|
|
end
|
|
|
|
defp log_historical_reload(socket) do
|
|
Logger.debug("Starting progressive historical loading for new bounds")
|
|
|
|
Logger.debug(
|
|
"Current assigns: historical_loading=#{socket.assigns.historical_loading}, map_bounds=#{inspect(socket.assigns.map_bounds)}"
|
|
)
|
|
end
|
|
end
|