fix historical packet loading

This commit is contained in:
Graham McIntire 2025-07-10 13:15:16 -05:00
parent aaf023f2ac
commit d45aed256d
No known key found for this signature in database
6 changed files with 449 additions and 146 deletions

View file

@ -51,51 +51,63 @@ let MapAPRSMap = {
} }
} }
// Try to restore from localStorage // Initialize with URL parameters first (from data attributes)
let initialCenter: CenterData, initialZoom: number; let initialCenter: CenterData, initialZoom: number;
let useUrlParams = false;
try { try {
const saved = localStorage.getItem("aprs_map_state"); const centerData = self.el.dataset.center;
if (saved) { const zoomData = self.el.dataset.zoom;
const { lat, lng, zoom } = JSON.parse(saved); console.log("Map data attributes - center:", centerData, "zoom:", zoomData);
if (
typeof lat === "number" && if (!centerData || !zoomData) throw new Error("Missing map data attributes");
typeof lng === "number" && initialCenter = JSON.parse(centerData);
typeof zoom === "number" && initialZoom = parseInt(zoomData);
lat >= -90 &&
lat <= 90 && console.log("Parsed initial values - center:", initialCenter, "zoom:", initialZoom);
lng >= -180 &&
lng <= 180 && if (
zoom >= 1 && !initialCenter ||
zoom <= 20 typeof initialCenter.lat !== "number" ||
) { typeof initialCenter.lng !== "number"
initialCenter = { lat, lng }; )
initialZoom = zoom; throw new Error("Invalid center data");
} else { if (isNaN(initialZoom) || initialZoom < 1 || initialZoom > 20)
throw new Error("Invalid saved map state"); throw new Error("Invalid zoom data");
}
} else { // Check if URL has explicit parameters (not default values)
throw new Error("No saved map state"); const urlParams = new URLSearchParams(window.location.search);
} useUrlParams = urlParams.has('lat') || urlParams.has('lng') || urlParams.has('z');
} catch (e) { } catch (error) {
// Fallback to server-provided data attributes console.error("Error parsing map data attributes:", error);
initialCenter = { lat: 39.8283, lng: -98.5795 };
initialZoom = 5;
}
// Only use localStorage if no URL params are present
if (!useUrlParams) {
try { try {
const centerData = self.el.dataset.center; const saved = localStorage.getItem("aprs_map_state");
const zoomData = self.el.dataset.zoom; if (saved) {
if (!centerData || !zoomData) throw new Error("Missing map data attributes"); const { lat, lng, zoom } = JSON.parse(saved);
initialCenter = JSON.parse(centerData); if (
initialZoom = parseInt(zoomData); typeof lat === "number" &&
if ( typeof lng === "number" &&
!initialCenter || typeof zoom === "number" &&
typeof initialCenter.lat !== "number" || lat >= -90 &&
typeof initialCenter.lng !== "number" lat <= 90 &&
) lng >= -180 &&
throw new Error("Invalid center data"); lng <= 180 &&
if (isNaN(initialZoom) || initialZoom < 1 || initialZoom > 20) zoom >= 1 &&
throw new Error("Invalid zoom data"); zoom <= 20
} catch (error) { ) {
console.error("Error parsing map data attributes:", error); console.log("Using saved state from localStorage:", { lat, lng, zoom });
initialCenter = { lat: 39.8283, lng: -98.5795 }; initialCenter = { lat, lng };
initialZoom = 5; initialZoom = zoom;
}
}
} catch (e) {
console.log("Could not load from localStorage:", e);
} }
} }
@ -210,8 +222,40 @@ let MapAPRSMap = {
self.map!.whenReady(() => { self.map!.whenReady(() => {
try { try {
self.lastZoom = self.map!.getZoom(); self.lastZoom = self.map!.getZoom();
self.pushEvent("map_ready", {});
self.sendBoundsToServer(); // Ensure we have a valid pushEvent function before using it
if (self.pushEvent && typeof self.pushEvent === 'function') {
console.log("Map ready - sending map_ready event");
self.pushEvent("map_ready", {});
// Send initial bounds to trigger historical loading
console.log("Sending initial bounds to server");
self.sendBoundsToServer();
// Also send update_map_state to ensure URL updates and bounds processing
// Increase delay to ensure LiveView is fully connected and ready
setTimeout(() => {
if (self.map && self.pushEvent && !self.isDestroyed) {
console.log("Sending initial update_map_state for historical loading");
saveMapState(self.map, (event: string, payload: any) => self.pushEvent(event, payload));
}
}, 500);
} else {
console.warn("pushEvent not available in whenReady callback");
// Retry after a short delay
setTimeout(() => {
if (self.pushEvent && typeof self.pushEvent === 'function' && !self.isDestroyed) {
self.pushEvent("map_ready", {});
self.sendBoundsToServer();
// Also trigger map state update after a delay
setTimeout(() => {
if (self.map && self.pushEvent && !self.isDestroyed) {
console.log("Sending initial update_map_state for historical loading (retry path)");
saveMapState(self.map, (event: string, payload: any) => self.pushEvent(event, payload));
}
}, 500);
}
}, 200);
}
// Start periodic cleanup of old trail positions (every 5 minutes) // Start periodic cleanup of old trail positions (every 5 minutes)
self.cleanupInterval = setInterval( self.cleanupInterval = setInterval(
@ -240,7 +284,9 @@ let MapAPRSMap = {
if (self.boundsTimer) clearTimeout(self.boundsTimer); if (self.boundsTimer) clearTimeout(self.boundsTimer);
self.boundsTimer = setTimeout(() => { self.boundsTimer = setTimeout(() => {
saveMapState(self.map, self.pushEvent); if (self.map && !self.isDestroyed) {
saveMapState(self.map, (event: string, payload: any) => self.pushEvent(event, payload));
}
}, 300); }, 300);
}; };
self.map!.on("moveend", moveEndHandler); self.map!.on("moveend", moveEndHandler);
@ -266,7 +312,9 @@ let MapAPRSMap = {
self.lastZoom = currentZoom; self.lastZoom = currentZoom;
// Save map state and update URL // Save map state and update URL
saveMapState(self.map, self.pushEvent); if (self.map && !self.isDestroyed) {
saveMapState(self.map, (event: string, payload: any) => self.pushEvent(event, payload));
}
}, 300); }, 300);
}; };
self.map!.on("zoomend", zoomEndHandler); self.map!.on("zoomend", zoomEndHandler);
@ -637,9 +685,11 @@ let MapAPRSMap = {
// Handle progressive loading of historical packets (batch processing) // Handle progressive loading of historical packets (batch processing)
self.handleEvent("add_historical_packets_batch", (data: { packets: MarkerData[], batch: number, is_final: boolean }) => { self.handleEvent("add_historical_packets_batch", (data: { packets: MarkerData[], batch: number, is_final: boolean }) => {
if (data.packets && Array.isArray(data.packets)) { console.log("Received historical packet batch:", data.batch, "packet count:", data.packets?.length || 0);
// Process all packets immediately for maximum speed try {
const packetsByCallsign = new Map<string, MarkerData[]>(); if (data.packets && Array.isArray(data.packets)) {
// Process all packets immediately for maximum speed
const packetsByCallsign = new Map<string, MarkerData[]>();
data.packets.forEach((packet) => { data.packets.forEach((packet) => {
const callsign = packet.callsign_group || packet.callsign || packet.id; const callsign = packet.callsign_group || packet.callsign || packet.id;
@ -668,6 +718,10 @@ let MapAPRSMap = {
}); });
}); });
} }
} catch (error) {
console.error("Error processing historical packets batch:", error);
// Continue processing other batches even if one fails
}
}); });
// Handle refresh markers event // Handle refresh markers event
@ -681,6 +735,7 @@ let MapAPRSMap = {
// Handle clearing historical packets // Handle clearing historical packets
self.handleEvent("clear_historical_packets", () => { self.handleEvent("clear_historical_packets", () => {
console.log("Clearing historical packets");
// Remove only historical markers (preserve live markers and their trails) // Remove only historical markers (preserve live markers and their trails)
const markersToRemove: string[] = []; const markersToRemove: string[] = [];
self.markers!.forEach((marker: any, id: any) => { self.markers!.forEach((marker: any, id: any) => {
@ -744,29 +799,38 @@ let MapAPRSMap = {
sendBoundsToServer() { sendBoundsToServer() {
const self = this as unknown as LiveViewHookContext; const self = this as unknown as LiveViewHookContext;
if (!self.map) return; if (!self.map || self.isDestroyed) return;
const bounds = self.map!.getBounds(); try {
const center = self.map!.getCenter(); const bounds = self.map!.getBounds();
const zoom = self.map!.getZoom(); const center = self.map!.getCenter();
const zoom = self.map!.getZoom();
// We're no longer removing markers outside bounds during normal panning/zooming // We're no longer removing markers outside bounds during normal panning/zooming
// to preserve historical positions and the current marker // to preserve historical positions and the current marker
// self.removeMarkersOutsideBounds(bounds); // self.removeMarkersOutsideBounds(bounds);
self.pushEvent("bounds_changed", { // Use direct pushEvent call with proper context
bounds: { if (self.pushEvent && typeof self.pushEvent === 'function') {
north: bounds.getNorth(), const boundsData = {
south: bounds.getSouth(), bounds: {
east: bounds.getEast(), north: bounds.getNorth(),
west: bounds.getWest(), south: bounds.getSouth(),
}, east: bounds.getEast(),
center: { west: bounds.getWest(),
lat: center.lat, },
lng: center.lng, center: {
}, lat: center.lat,
zoom: zoom, lng: center.lng,
}); },
zoom: zoom,
};
console.log("Sending bounds_changed event:", boundsData);
self.pushEvent("bounds_changed", boundsData);
}
} catch (error) {
console.error("Error sending bounds to server:", error);
}
}, },
addMarker(data: MarkerData & { openPopup?: boolean }) { addMarker(data: MarkerData & { openPopup?: boolean }) {

View file

@ -38,42 +38,60 @@ export function getTrailId(data: { callsign_group?: string; callsign?: string; i
* Save map state to localStorage and send to server * Save map state to localStorage and send to server
*/ */
export function saveMapState(map: any, pushEvent: Function) { export function saveMapState(map: any, pushEvent: Function) {
const center = map.getCenter(); if (!map || !pushEvent) {
const zoom = map.getZoom(); console.warn("saveMapState called with invalid map or pushEvent");
return;
}
// Truncate lat/lng to 5 decimal places for URL try {
const truncatedLat = Math.round(center.lat * 100000) / 100000; const center = map.getCenter();
const truncatedLng = Math.round(center.lng * 100000) / 100000; const zoom = map.getZoom();
localStorage.setItem( // Truncate lat/lng to 5 decimal places for URL
"aprs_map_state", const truncatedLat = Math.round(center.lat * 100000) / 100000;
JSON.stringify({ lat: truncatedLat, lng: truncatedLng, zoom }), const truncatedLng = Math.round(center.lng * 100000) / 100000;
);
// Always save to localStorage, even if LiveView is disconnected
// Send combined map state update to server for URL and bounds updating localStorage.setItem(
pushEvent("update_map_state", { "aprs_map_state",
center: { lat: truncatedLat, lng: truncatedLng }, JSON.stringify({ lat: truncatedLat, lng: truncatedLng, zoom }),
zoom: zoom, );
bounds: {
north: map.getBounds().getNorth(), // Send combined map state update to server for URL and bounds updating
south: map.getBounds().getSouth(), const payload = {
east: map.getBounds().getEast(), center: { lat: truncatedLat, lng: truncatedLng },
west: map.getBounds().getWest(), zoom: zoom,
} bounds: {
}); north: map.getBounds().getNorth(),
south: map.getBounds().getSouth(),
east: map.getBounds().getEast(),
west: map.getBounds().getWest(),
}
};
console.debug("Sending update_map_state event:", payload);
// Use safePushEvent to handle disconnected state
safePushEvent(pushEvent, "update_map_state", payload);
} catch (error) {
console.error("Error in saveMapState:", error);
}
} }
/** /**
* Safely push event to LiveView * Safely push event to LiveView
*/ */
export function safePushEvent(pushEvent: Function | undefined, event: string, payload: any): boolean { export function safePushEvent(pushEvent: Function | undefined, event: string, payload: any): boolean {
if (!pushEvent) return false; if (!pushEvent || typeof pushEvent !== 'function') {
console.debug(`pushEvent not available for ${event} event`);
return false;
}
try { try {
pushEvent(event, payload); pushEvent(event, payload);
return true; return true;
} catch (e) { } catch (e) {
console.debug(`Unable to send ${event} event - LiveView disconnected`); console.debug(`Unable to send ${event} event:`, e);
return false; return false;
} }
} }

View file

@ -120,10 +120,18 @@ defmodule Aprsme.Packets.QueryBuilder do
@spec within_bounds(Ecto.Query.t(), list(number())) :: Ecto.Query.t() @spec within_bounds(Ecto.Query.t(), list(number())) :: Ecto.Query.t()
def within_bounds(query, [west, south, east, north]) def within_bounds(query, [west, south, east, north])
when is_number(west) and is_number(south) and is_number(east) and is_number(north) do when is_number(west) and is_number(south) and is_number(east) and is_number(north) do
from p in query, # Handle antimeridian crossing (e.g., west=170, east=-170)
where: p.has_position == true, if west > east do
where: p.lat >= ^south and p.lat <= ^north, from p in query,
where: p.lon >= ^west and p.lon <= ^east where: p.has_position == true,
where: p.lat >= ^south and p.lat <= ^north,
where: p.lon >= ^west or p.lon <= ^east
else
from p in query,
where: p.has_position == true,
where: p.lat >= ^south and p.lat <= ^north,
where: p.lon >= ^west and p.lon <= ^east
end
end end
def within_bounds(query, _), do: query def within_bounds(query, _), do: query

View file

@ -67,6 +67,8 @@ defmodule AprsmeWeb.MapLive.Index do
@impl true @impl true
def mount(params, _session, socket) do def mount(params, _session, socket) do
require Logger
if connected?(socket) do if connected?(socket) do
# Subscribe to packet updates # Subscribe to packet updates
Phoenix.PubSub.subscribe(Aprsme.PubSub, "packets") Phoenix.PubSub.subscribe(Aprsme.PubSub, "packets")
@ -85,14 +87,16 @@ defmodule AprsmeWeb.MapLive.Index do
# Parse map state from URL parameters # Parse map state from URL parameters
{map_center, map_zoom} = parse_map_params(params) {map_center, map_zoom} = parse_map_params(params)
Logger.debug("Parsed map params from URL: center=#{inspect(map_center)}, zoom=#{map_zoom}")
Logger.debug("Raw params: #{inspect(params)}")
socket = assign_defaults(socket, one_hour_ago) socket = assign_defaults(socket, one_hour_ago)
socket = assign(socket, map_center: map_center, map_zoom: map_zoom)
# Initialize the flag to track if initial historical load is completed
socket = assign(socket, initial_historical_completed: false)
# Calculate initial bounds based on center and zoom level # Calculate initial bounds based on center and zoom level
initial_bounds = calculate_bounds_from_center_and_zoom(map_center, map_zoom) initial_bounds = calculate_bounds_from_center_and_zoom(map_center, map_zoom)
socket = assign(socket, map_bounds: initial_bounds)
socket = assign(socket, packet_buffer: [], buffer_timer: nil)
socket = assign(socket, all_packets: %{}, station_popup_open: false)
if connected?(socket) do if connected?(socket) do
Endpoint.subscribe("aprs_messages") Endpoint.subscribe("aprs_messages")
@ -104,9 +108,9 @@ defmodule AprsmeWeb.MapLive.Index do
{:ok, {:ok,
assign(socket, assign(socket,
map_ready: false, map_ready: false,
map_bounds: nil, map_bounds: initial_bounds,
map_center: %{lat: 39.8283, lng: -98.5795}, map_center: map_center,
map_zoom: 4, map_zoom: map_zoom,
visible_packets: %{}, visible_packets: %{},
historical_packets: %{}, historical_packets: %{},
overlay_callsign: "", overlay_callsign: "",
@ -115,7 +119,13 @@ defmodule AprsmeWeb.MapLive.Index do
packet_age_threshold: one_hour_ago, packet_age_threshold: one_hour_ago,
slideover_open: true, slideover_open: true,
deployed_at: deployed_at, deployed_at: deployed_at,
map_page: true map_page: true,
packet_buffer: [],
buffer_timer: nil,
all_packets: %{},
station_popup_open: false,
initial_bounds_loaded: false,
needs_initial_historical_load: false
)} )}
end end
@ -256,10 +266,19 @@ defmodule AprsmeWeb.MapLive.Index do
@impl true @impl true
def handle_event("map_ready", _params, socket) do def handle_event("map_ready", _params, socket) do
socket = assign(socket, map_ready: true) require Logger
# Load historical packets immediately since we now have bounds from URL parameters Logger.debug("map_ready event received - current bounds: #{inspect(socket.assigns.map_bounds)}")
Process.send_after(self(), :reload_historical_packets, 10)
# Mark map as ready and that we need to load historical packets
socket =
socket
|> assign(map_ready: true)
|> assign(needs_initial_historical_load: true)
# Wait for JavaScript to send the actual map bounds before loading historical packets
# The calculated bounds might be too small/inaccurate
Logger.debug("Map ready - waiting for JavaScript to send actual bounds before loading historical packets")
# If we have pending geolocation, zoom to it now # If we have pending geolocation, zoom to it now
socket = socket =
@ -358,6 +377,10 @@ defmodule AprsmeWeb.MapLive.Index do
@impl true @impl true
def handle_event("update_map_state", %{"center" => center, "zoom" => zoom} = params, socket) do def handle_event("update_map_state", %{"center" => center, "zoom" => zoom} = params, socket) do
require Logger
Logger.debug("update_map_state event received: center=#{inspect(center)}, zoom=#{zoom}")
# Parse center coordinates # Parse center coordinates
lat = lat =
case center do case center do
@ -383,6 +406,7 @@ defmodule AprsmeWeb.MapLive.Index do
# Update URL without page reload # Update URL without page reload
new_path = "/?lat=#{lat}&lng=#{lng}&z=#{zoom}" new_path = "/?lat=#{lat}&lng=#{lng}&z=#{zoom}"
Logger.debug("Updating URL to: #{new_path}")
socket = push_patch(socket, to: new_path, replace: true) socket = push_patch(socket, to: new_path, replace: true)
# If bounds are included, also process bounds update # If bounds are included, also process bounds update
@ -396,8 +420,16 @@ defmodule AprsmeWeb.MapLive.Index do
west: west west: west
} }
# Only trigger bounds processing if bounds actually changed # Trigger bounds processing if bounds changed OR if this is the initial load OR if we need initial historical load
if socket.assigns.map_bounds != map_bounds do if socket.assigns.map_bounds != map_bounds or
!socket.assigns[:initial_bounds_loaded] or
socket.assigns[:needs_initial_historical_load] do
require Logger
Logger.debug(
"Sending bounds update (initial_load: #{!socket.assigns[:initial_bounds_loaded]}, needs_historical: #{socket.assigns[:needs_initial_historical_load]}): #{inspect(map_bounds)}"
)
send(self(), {:process_bounds_update, map_bounds}) send(self(), {:process_bounds_update, map_bounds})
end end
@ -478,12 +510,25 @@ defmodule AprsmeWeb.MapLive.Index do
# Private handler functions for each message type # Private handler functions for each message type
defp handle_info_process_bounds_update(map_bounds, socket) do defp handle_info_process_bounds_update(map_bounds, socket) do
if !socket.assigns.initial_bounds_loaded or require Logger
not compare_bounds(map_bounds, socket.assigns.map_bounds) do
# Check if we need to process this bounds update
should_process =
!socket.assigns[:initial_bounds_loaded] or
socket.assigns[:needs_initial_historical_load] or
not compare_bounds(map_bounds, socket.assigns.map_bounds)
Logger.debug(
"handle_info_process_bounds_update - should_process: #{should_process}, initial_bounds_loaded: #{socket.assigns[:initial_bounds_loaded]}, needs_initial_historical_load: #{socket.assigns[:needs_initial_historical_load]}"
)
if should_process do
Logger.debug("Processing bounds update: #{inspect(map_bounds)}")
socket = process_bounds_update(map_bounds, socket) socket = process_bounds_update(map_bounds, socket)
socket = assign(socket, initial_bounds_loaded: true) socket = assign(socket, initial_bounds_loaded: true)
{:noreply, socket} {:noreply, socket}
else else
Logger.debug("Skipping bounds update - no change detected")
{:noreply, socket} {:noreply, socket}
end end
end end
@ -1154,6 +1199,12 @@ defmodule AprsmeWeb.MapLive.Index do
end end
defp handle_reload_historical_packets(socket) do defp handle_reload_historical_packets(socket) do
require Logger
Logger.debug(
"handle_reload_historical_packets called - map_ready: #{socket.assigns.map_ready}, map_bounds: #{inspect(socket.assigns.map_bounds)}"
)
if socket.assigns.map_ready and socket.assigns.map_bounds do if socket.assigns.map_ready and socket.assigns.map_bounds do
# Clear existing historical packets # Clear existing historical packets
socket = push_event(socket, "clear_historical_packets", %{}) socket = push_event(socket, "clear_historical_packets", %{})
@ -1163,6 +1214,7 @@ defmodule AprsmeWeb.MapLive.Index do
{:noreply, socket} {:noreply, socket}
else else
Logger.debug("Skipping historical reload - conditions not met")
{:noreply, socket} {:noreply, socket}
end end
end end
@ -1435,14 +1487,22 @@ defmodule AprsmeWeb.MapLive.Index do
# Progressive loading functions using LiveView's efficient update mechanisms # Progressive loading functions using LiveView's efficient update mechanisms
@spec start_progressive_historical_loading(Socket.t()) :: Socket.t() @spec start_progressive_historical_loading(Socket.t()) :: Socket.t()
defp start_progressive_historical_loading(socket) do defp start_progressive_historical_loading(socket) do
# Clear existing historical packets before loading new ones require Logger
socket = push_event(socket, "clear_historical_packets", %{})
Logger.debug(
"start_progressive_historical_loading called with zoom: #{socket.assigns.map_zoom}, bounds: #{inspect(socket.assigns.map_bounds)}"
)
# Don't clear historical packets here - let the caller decide if clearing is needed
# This prevents race conditions where we clear packets that were just loaded
# For high zoom levels, load everything in one batch for maximum speed # For high zoom levels, load everything in one batch for maximum speed
zoom = socket.assigns.map_zoom || 5 zoom = socket.assigns.map_zoom || 5
if zoom >= 10 do if zoom >= 10 do
# High zoom - load everything at once for maximum speed # High zoom - load everything at once for maximum speed
Logger.debug("High zoom (#{zoom}), loading in single batch")
socket socket
|> assign(loading_batch: 0, total_batches: 1) |> assign(loading_batch: 0, total_batches: 1)
|> load_historical_batch(0) |> load_historical_batch(0)
@ -1507,27 +1567,45 @@ defmodule AprsmeWeb.MapLive.Index do
packets_module = Application.get_env(:aprsme, :packets_module, Aprsme.Packets) packets_module = Application.get_env(:aprsme, :packets_module, Aprsme.Packets)
historical_packets = historical_packets =
if packets_module == Aprsme.Packets do try do
# Use cached queries for better performance if packets_module == Aprsme.Packets do
# Include zoom level in cache key for better cache efficiency # Use cached queries for better performance
Aprsme.CachedQueries.get_recent_packets_cached(%{ # Include zoom level in cache key for better cache efficiency
bounds: bounds, Aprsme.CachedQueries.get_recent_packets_cached(%{
limit: batch_size, bounds: bounds,
offset: offset, limit: batch_size,
zoom: zoom offset: offset,
}) zoom: zoom
else })
# Fallback for testing else
packets_module.get_recent_packets_optimized(%{ # Fallback for testing
bounds: bounds, packets_module.get_recent_packets_optimized(%{
limit: batch_size, bounds: bounds,
offset: offset limit: batch_size,
}) offset: offset
})
end
rescue
e ->
require Logger
Logger.error("Error loading historical packets: #{inspect(e)}")
Logger.error("Stack trace: #{Exception.format_stacktrace()}")
[]
end end
if Enum.any?(historical_packets) do if Enum.any?(historical_packets) do
# Process this batch and send to frontend # Process this batch and send to frontend
packet_data_list = build_packet_data_list(historical_packets) packet_data_list =
try do
build_packet_data_list(historical_packets)
rescue
e ->
require Logger
Logger.error("Error building packet data list: #{inspect(e)}")
[]
end
if Enum.any?(packet_data_list) do if Enum.any?(packet_data_list) do
# Use LiveView's efficient push_event for incremental updates # Use LiveView's efficient push_event for incremental updates
@ -1672,20 +1750,21 @@ defmodule AprsmeWeb.MapLive.Index do
end end
defp handle_valid_bounds_update(map_bounds, socket) do defp handle_valid_bounds_update(map_bounds, socket) do
# Only schedule a bounds update if the bounds have actually changed (with rounding) # Force processing if we need initial historical load, regardless of bounds comparison
if compare_bounds(map_bounds, socket.assigns.map_bounds) do cond do
{:noreply, socket} socket.assigns[:needs_initial_historical_load] ->
else require Logger
# If this is the first bounds update (map_bounds is nil), process immediately
# to avoid race condition with historical packet loading Logger.debug("Processing initial bounds update immediately (forced): #{inspect(map_bounds)}")
if is_nil(socket.assigns.map_bounds) do
# Process immediately for initial bounds
socket = process_bounds_update(map_bounds, socket) socket = process_bounds_update(map_bounds, socket)
{:noreply, socket} {:noreply, socket}
else
compare_bounds(map_bounds, socket.assigns.map_bounds) ->
{:noreply, socket}
true ->
# For subsequent updates, use the timer to debounce # For subsequent updates, use the timer to debounce
schedule_bounds_update(map_bounds, socket) schedule_bounds_update(map_bounds, socket)
end
end end
end end
@ -1701,6 +1780,21 @@ defmodule AprsmeWeb.MapLive.Index do
@spec process_bounds_update(map(), Socket.t()) :: Socket.t() @spec process_bounds_update(map(), Socket.t()) :: Socket.t()
defp process_bounds_update(map_bounds, socket) do defp process_bounds_update(map_bounds, socket) do
require Logger
Logger.debug("process_bounds_update called with bounds: #{inspect(map_bounds)}")
# Check if this is the initial load or if bounds have actually changed
is_initial_load = socket.assigns[:needs_initial_historical_load] || !socket.assigns[:initial_bounds_loaded]
bounds_changed = socket.assigns.map_bounds && not compare_bounds(map_bounds, socket.assigns.map_bounds)
# Check if we've completed the initial historical load
initial_historical_completed = socket.assigns[:initial_historical_completed] || false
Logger.debug(
"is_initial_load: #{is_initial_load}, bounds_changed: #{bounds_changed}, initial_historical_completed: #{initial_historical_completed}"
)
# Remove out-of-bounds packets and markers immediately # Remove out-of-bounds packets and markers immediately
new_visible_packets = new_visible_packets =
socket.assigns.visible_packets socket.assigns.visible_packets
@ -1722,14 +1816,37 @@ defmodule AprsmeWeb.MapLive.Index do
end) end)
end end
# Remove only out-of-bounds historical packets instead of clearing all # Only clear historical packets if:
# 1. Bounds actually changed AND
# 2. This is not the initial load AND
# 3. We've already completed the initial historical load
socket =
if bounds_changed and not is_initial_load and initial_historical_completed do
Logger.debug("Bounds changed after initial load - clearing historical packets")
push_event(socket, "clear_historical_packets", %{})
else
Logger.debug("Initial load or no significant change - keeping existing markers")
socket
end
# Always filter markers by bounds
socket = push_event(socket, "filter_markers_by_bounds", %{bounds: map_bounds}) socket = push_event(socket, "filter_markers_by_bounds", %{bounds: map_bounds})
# Load historical packets for the new bounds # Update map bounds FIRST so progressive loading uses the correct bounds
# Always load historical packets when bounds change to ensure new areas have data socket =
socket
|> assign(map_bounds: map_bounds, visible_packets: new_visible_packets)
|> assign(needs_initial_historical_load: false)
# Load historical packets for the new bounds (now socket.assigns.map_bounds is correct)
Logger.debug("Starting progressive historical loading for new bounds")
socket = start_progressive_historical_loading(socket) socket = start_progressive_historical_loading(socket)
# Update map bounds and visible packets # Mark initial historical as completed if this was the initial load
assign(socket, map_bounds: map_bounds, visible_packets: new_visible_packets) if is_initial_load do
assign(socket, initial_historical_completed: true)
else
socket
end
end end
end end

View file

@ -0,0 +1,6 @@
defmodule Aprsme.Repo.Migrations.TestQuery do
use Ecto.Migration
def change do
end
end

View file

@ -0,0 +1,90 @@
defmodule Aprsme.Packets.QueryBuilderTest do
use Aprsme.DataCase
import Ecto.Query
alias Aprsme.Packet
alias Aprsme.Packets.QueryBuilder
describe "within_bounds/2" do
test "filters packets within normal bounds" do
# Create test packets
{:ok, inside} =
Aprsme.Repo.insert(%Packet{
sender: "TEST-1",
lat: 40.5,
lon: -73.5,
has_position: true,
received_at: DateTime.truncate(DateTime.utc_now(), :second)
})
{:ok, _outside} =
Aprsme.Repo.insert(%Packet{
sender: "TEST-2",
lat: 50.0,
lon: -73.5,
has_position: true,
received_at: DateTime.truncate(DateTime.utc_now(), :second)
})
# Test normal bounds [west, south, east, north]
query = QueryBuilder.within_bounds(Packet, [-74.0, 40.0, -73.0, 41.0])
results = Aprsme.Repo.all(query)
assert length(results) == 1
assert hd(results).id == inside.id
end
test "handles antimeridian crossing bounds" do
# Create test packets
{:ok, west_side} =
Aprsme.Repo.insert(%Packet{
sender: "WEST-1",
lat: 0.0,
lon: 175.0,
has_position: true,
received_at: DateTime.truncate(DateTime.utc_now(), :second)
})
{:ok, east_side} =
Aprsme.Repo.insert(%Packet{
sender: "EAST-1",
lat: 0.0,
lon: -175.0,
has_position: true,
received_at: DateTime.truncate(DateTime.utc_now(), :second)
})
{:ok, _middle} =
Aprsme.Repo.insert(%Packet{
sender: "MIDDLE-1",
lat: 0.0,
lon: 0.0,
has_position: true,
received_at: DateTime.truncate(DateTime.utc_now(), :second)
})
# Test antimeridian crossing bounds [west=170, south=-10, east=-170, north=10]
query = QueryBuilder.within_bounds(Packet, [170.0, -10.0, -170.0, 10.0])
results = Aprsme.Repo.all(query)
assert length(results) == 2
result_ids = results |> Enum.map(& &1.id) |> Enum.sort()
expected_ids = Enum.sort([west_side.id, east_side.id])
assert result_ids == expected_ids
end
test "returns query unchanged with invalid bounds" do
original_query = from(p in Packet)
# Test with nil
assert QueryBuilder.within_bounds(original_query, nil) == original_query
# Test with wrong number of elements
assert QueryBuilder.within_bounds(original_query, [1, 2, 3]) == original_query
# Test with non-numeric values
assert QueryBuilder.within_bounds(original_query, ["a", "b", "c", "d"]) == original_query
end
end
end