fix historical packet loading
This commit is contained in:
parent
aaf023f2ac
commit
d45aed256d
6 changed files with 449 additions and 146 deletions
202
assets/js/map.ts
202
assets/js/map.ts
|
|
@ -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 useUrlParams = false;
|
||||
|
||||
try {
|
||||
const saved = localStorage.getItem("aprs_map_state");
|
||||
if (saved) {
|
||||
const { lat, lng, zoom } = JSON.parse(saved);
|
||||
if (
|
||||
typeof lat === "number" &&
|
||||
typeof lng === "number" &&
|
||||
typeof zoom === "number" &&
|
||||
lat >= -90 &&
|
||||
lat <= 90 &&
|
||||
lng >= -180 &&
|
||||
lng <= 180 &&
|
||||
zoom >= 1 &&
|
||||
zoom <= 20
|
||||
) {
|
||||
initialCenter = { lat, lng };
|
||||
initialZoom = zoom;
|
||||
} else {
|
||||
throw new Error("Invalid saved map state");
|
||||
}
|
||||
} else {
|
||||
throw new Error("No saved map state");
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback to server-provided data attributes
|
||||
const centerData = self.el.dataset.center;
|
||||
const zoomData = self.el.dataset.zoom;
|
||||
console.log("Map data attributes - center:", centerData, "zoom:", zoomData);
|
||||
|
||||
if (!centerData || !zoomData) throw new Error("Missing map data attributes");
|
||||
initialCenter = JSON.parse(centerData);
|
||||
initialZoom = parseInt(zoomData);
|
||||
|
||||
console.log("Parsed initial values - center:", initialCenter, "zoom:", initialZoom);
|
||||
|
||||
if (
|
||||
!initialCenter ||
|
||||
typeof initialCenter.lat !== "number" ||
|
||||
typeof initialCenter.lng !== "number"
|
||||
)
|
||||
throw new Error("Invalid center data");
|
||||
if (isNaN(initialZoom) || initialZoom < 1 || initialZoom > 20)
|
||||
throw new Error("Invalid zoom data");
|
||||
|
||||
// Check if URL has explicit parameters (not default values)
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
useUrlParams = urlParams.has('lat') || urlParams.has('lng') || urlParams.has('z');
|
||||
} catch (error) {
|
||||
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 {
|
||||
const centerData = self.el.dataset.center;
|
||||
const zoomData = self.el.dataset.zoom;
|
||||
if (!centerData || !zoomData) throw new Error("Missing map data attributes");
|
||||
initialCenter = JSON.parse(centerData);
|
||||
initialZoom = parseInt(zoomData);
|
||||
if (
|
||||
!initialCenter ||
|
||||
typeof initialCenter.lat !== "number" ||
|
||||
typeof initialCenter.lng !== "number"
|
||||
)
|
||||
throw new Error("Invalid center data");
|
||||
if (isNaN(initialZoom) || initialZoom < 1 || initialZoom > 20)
|
||||
throw new Error("Invalid zoom data");
|
||||
} catch (error) {
|
||||
console.error("Error parsing map data attributes:", error);
|
||||
initialCenter = { lat: 39.8283, lng: -98.5795 };
|
||||
initialZoom = 5;
|
||||
const saved = localStorage.getItem("aprs_map_state");
|
||||
if (saved) {
|
||||
const { lat, lng, zoom } = JSON.parse(saved);
|
||||
if (
|
||||
typeof lat === "number" &&
|
||||
typeof lng === "number" &&
|
||||
typeof zoom === "number" &&
|
||||
lat >= -90 &&
|
||||
lat <= 90 &&
|
||||
lng >= -180 &&
|
||||
lng <= 180 &&
|
||||
zoom >= 1 &&
|
||||
zoom <= 20
|
||||
) {
|
||||
console.log("Using saved state from localStorage:", { lat, lng, zoom });
|
||||
initialCenter = { lat, lng };
|
||||
initialZoom = zoom;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Could not load from localStorage:", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -210,8 +222,40 @@ let MapAPRSMap = {
|
|||
self.map!.whenReady(() => {
|
||||
try {
|
||||
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)
|
||||
self.cleanupInterval = setInterval(
|
||||
|
|
@ -240,7 +284,9 @@ let MapAPRSMap = {
|
|||
|
||||
if (self.boundsTimer) clearTimeout(self.boundsTimer);
|
||||
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);
|
||||
};
|
||||
self.map!.on("moveend", moveEndHandler);
|
||||
|
|
@ -266,7 +312,9 @@ let MapAPRSMap = {
|
|||
|
||||
self.lastZoom = currentZoom;
|
||||
// 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);
|
||||
};
|
||||
self.map!.on("zoomend", zoomEndHandler);
|
||||
|
|
@ -637,9 +685,11 @@ let MapAPRSMap = {
|
|||
|
||||
// Handle progressive loading of historical packets (batch processing)
|
||||
self.handleEvent("add_historical_packets_batch", (data: { packets: MarkerData[], batch: number, is_final: boolean }) => {
|
||||
if (data.packets && Array.isArray(data.packets)) {
|
||||
// Process all packets immediately for maximum speed
|
||||
const packetsByCallsign = new Map<string, MarkerData[]>();
|
||||
console.log("Received historical packet batch:", data.batch, "packet count:", data.packets?.length || 0);
|
||||
try {
|
||||
if (data.packets && Array.isArray(data.packets)) {
|
||||
// Process all packets immediately for maximum speed
|
||||
const packetsByCallsign = new Map<string, MarkerData[]>();
|
||||
|
||||
data.packets.forEach((packet) => {
|
||||
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
|
||||
|
|
@ -681,6 +735,7 @@ let MapAPRSMap = {
|
|||
|
||||
// Handle clearing historical packets
|
||||
self.handleEvent("clear_historical_packets", () => {
|
||||
console.log("Clearing historical packets");
|
||||
// Remove only historical markers (preserve live markers and their trails)
|
||||
const markersToRemove: string[] = [];
|
||||
self.markers!.forEach((marker: any, id: any) => {
|
||||
|
|
@ -744,29 +799,38 @@ let MapAPRSMap = {
|
|||
|
||||
sendBoundsToServer() {
|
||||
const self = this as unknown as LiveViewHookContext;
|
||||
if (!self.map) return;
|
||||
if (!self.map || self.isDestroyed) return;
|
||||
|
||||
const bounds = self.map!.getBounds();
|
||||
const center = self.map!.getCenter();
|
||||
const zoom = self.map!.getZoom();
|
||||
try {
|
||||
const bounds = self.map!.getBounds();
|
||||
const center = self.map!.getCenter();
|
||||
const zoom = self.map!.getZoom();
|
||||
|
||||
// We're no longer removing markers outside bounds during normal panning/zooming
|
||||
// to preserve historical positions and the current marker
|
||||
// self.removeMarkersOutsideBounds(bounds);
|
||||
// We're no longer removing markers outside bounds during normal panning/zooming
|
||||
// to preserve historical positions and the current marker
|
||||
// self.removeMarkersOutsideBounds(bounds);
|
||||
|
||||
self.pushEvent("bounds_changed", {
|
||||
bounds: {
|
||||
north: bounds.getNorth(),
|
||||
south: bounds.getSouth(),
|
||||
east: bounds.getEast(),
|
||||
west: bounds.getWest(),
|
||||
},
|
||||
center: {
|
||||
lat: center.lat,
|
||||
lng: center.lng,
|
||||
},
|
||||
zoom: zoom,
|
||||
});
|
||||
// Use direct pushEvent call with proper context
|
||||
if (self.pushEvent && typeof self.pushEvent === 'function') {
|
||||
const boundsData = {
|
||||
bounds: {
|
||||
north: bounds.getNorth(),
|
||||
south: bounds.getSouth(),
|
||||
east: bounds.getEast(),
|
||||
west: bounds.getWest(),
|
||||
},
|
||||
center: {
|
||||
lat: center.lat,
|
||||
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 }) {
|
||||
|
|
|
|||
|
|
@ -38,42 +38,60 @@ export function getTrailId(data: { callsign_group?: string; callsign?: string; i
|
|||
* Save map state to localStorage and send to server
|
||||
*/
|
||||
export function saveMapState(map: any, pushEvent: Function) {
|
||||
const center = map.getCenter();
|
||||
const zoom = map.getZoom();
|
||||
if (!map || !pushEvent) {
|
||||
console.warn("saveMapState called with invalid map or pushEvent");
|
||||
return;
|
||||
}
|
||||
|
||||
// Truncate lat/lng to 5 decimal places for URL
|
||||
const truncatedLat = Math.round(center.lat * 100000) / 100000;
|
||||
const truncatedLng = Math.round(center.lng * 100000) / 100000;
|
||||
|
||||
localStorage.setItem(
|
||||
"aprs_map_state",
|
||||
JSON.stringify({ lat: truncatedLat, lng: truncatedLng, zoom }),
|
||||
);
|
||||
|
||||
// Send combined map state update to server for URL and bounds updating
|
||||
pushEvent("update_map_state", {
|
||||
center: { lat: truncatedLat, lng: truncatedLng },
|
||||
zoom: zoom,
|
||||
bounds: {
|
||||
north: map.getBounds().getNorth(),
|
||||
south: map.getBounds().getSouth(),
|
||||
east: map.getBounds().getEast(),
|
||||
west: map.getBounds().getWest(),
|
||||
}
|
||||
});
|
||||
try {
|
||||
const center = map.getCenter();
|
||||
const zoom = map.getZoom();
|
||||
|
||||
// Truncate lat/lng to 5 decimal places for URL
|
||||
const truncatedLat = Math.round(center.lat * 100000) / 100000;
|
||||
const truncatedLng = Math.round(center.lng * 100000) / 100000;
|
||||
|
||||
// Always save to localStorage, even if LiveView is disconnected
|
||||
localStorage.setItem(
|
||||
"aprs_map_state",
|
||||
JSON.stringify({ lat: truncatedLat, lng: truncatedLng, zoom }),
|
||||
);
|
||||
|
||||
// Send combined map state update to server for URL and bounds updating
|
||||
const payload = {
|
||||
center: { lat: truncatedLat, lng: truncatedLng },
|
||||
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
|
||||
*/
|
||||
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 {
|
||||
pushEvent(event, payload);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.debug(`Unable to send ${event} event - LiveView disconnected`);
|
||||
console.debug(`Unable to send ${event} event:`, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,10 +120,18 @@ defmodule Aprsme.Packets.QueryBuilder do
|
|||
@spec within_bounds(Ecto.Query.t(), list(number())) :: Ecto.Query.t()
|
||||
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
|
||||
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
|
||||
# Handle antimeridian crossing (e.g., west=170, east=-170)
|
||||
if west > east do
|
||||
from p in query,
|
||||
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
|
||||
|
||||
def within_bounds(query, _), do: query
|
||||
|
|
|
|||
|
|
@ -67,6 +67,8 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
|
||||
@impl true
|
||||
def mount(params, _session, socket) do
|
||||
require Logger
|
||||
|
||||
if connected?(socket) do
|
||||
# Subscribe to packet updates
|
||||
Phoenix.PubSub.subscribe(Aprsme.PubSub, "packets")
|
||||
|
|
@ -85,14 +87,16 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
# Parse map state from URL parameters
|
||||
{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(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
|
||||
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
|
||||
Endpoint.subscribe("aprs_messages")
|
||||
|
|
@ -104,9 +108,9 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
{:ok,
|
||||
assign(socket,
|
||||
map_ready: false,
|
||||
map_bounds: nil,
|
||||
map_center: %{lat: 39.8283, lng: -98.5795},
|
||||
map_zoom: 4,
|
||||
map_bounds: initial_bounds,
|
||||
map_center: map_center,
|
||||
map_zoom: map_zoom,
|
||||
visible_packets: %{},
|
||||
historical_packets: %{},
|
||||
overlay_callsign: "",
|
||||
|
|
@ -115,7 +119,13 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
packet_age_threshold: one_hour_ago,
|
||||
slideover_open: true,
|
||||
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
|
||||
|
||||
|
|
@ -256,10 +266,19 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
|
||||
@impl true
|
||||
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
|
||||
Process.send_after(self(), :reload_historical_packets, 10)
|
||||
Logger.debug("map_ready event received - current bounds: #{inspect(socket.assigns.map_bounds)}")
|
||||
|
||||
# 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
|
||||
socket =
|
||||
|
|
@ -358,6 +377,10 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
|
||||
@impl true
|
||||
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
|
||||
lat =
|
||||
case center do
|
||||
|
|
@ -383,6 +406,7 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
|
||||
# Update URL without page reload
|
||||
new_path = "/?lat=#{lat}&lng=#{lng}&z=#{zoom}"
|
||||
Logger.debug("Updating URL to: #{new_path}")
|
||||
socket = push_patch(socket, to: new_path, replace: true)
|
||||
|
||||
# If bounds are included, also process bounds update
|
||||
|
|
@ -396,8 +420,16 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
west: west
|
||||
}
|
||||
|
||||
# Only trigger bounds processing if bounds actually changed
|
||||
if socket.assigns.map_bounds != map_bounds do
|
||||
# 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 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})
|
||||
end
|
||||
|
||||
|
|
@ -478,12 +510,25 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
# Private handler functions for each message type
|
||||
|
||||
defp handle_info_process_bounds_update(map_bounds, socket) do
|
||||
if !socket.assigns.initial_bounds_loaded or
|
||||
not compare_bounds(map_bounds, socket.assigns.map_bounds) do
|
||||
require Logger
|
||||
|
||||
# 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 = assign(socket, initial_bounds_loaded: true)
|
||||
{:noreply, socket}
|
||||
else
|
||||
Logger.debug("Skipping bounds update - no change detected")
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
|
@ -1154,6 +1199,12 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
end
|
||||
|
||||
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
|
||||
# Clear existing historical packets
|
||||
socket = push_event(socket, "clear_historical_packets", %{})
|
||||
|
|
@ -1163,6 +1214,7 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
|
||||
{:noreply, socket}
|
||||
else
|
||||
Logger.debug("Skipping historical reload - conditions not met")
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
|
@ -1435,14 +1487,22 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
# Progressive loading functions using LiveView's efficient update mechanisms
|
||||
@spec start_progressive_historical_loading(Socket.t()) :: Socket.t()
|
||||
defp start_progressive_historical_loading(socket) do
|
||||
# Clear existing historical packets before loading new ones
|
||||
socket = push_event(socket, "clear_historical_packets", %{})
|
||||
require Logger
|
||||
|
||||
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
|
||||
zoom = socket.assigns.map_zoom || 5
|
||||
|
||||
if zoom >= 10 do
|
||||
# High zoom - load everything at once for maximum speed
|
||||
Logger.debug("High zoom (#{zoom}), loading in single batch")
|
||||
|
||||
socket
|
||||
|> assign(loading_batch: 0, total_batches: 1)
|
||||
|> load_historical_batch(0)
|
||||
|
|
@ -1507,27 +1567,45 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
packets_module = Application.get_env(:aprsme, :packets_module, Aprsme.Packets)
|
||||
|
||||
historical_packets =
|
||||
if packets_module == Aprsme.Packets do
|
||||
# Use cached queries for better performance
|
||||
# Include zoom level in cache key for better cache efficiency
|
||||
Aprsme.CachedQueries.get_recent_packets_cached(%{
|
||||
bounds: bounds,
|
||||
limit: batch_size,
|
||||
offset: offset,
|
||||
zoom: zoom
|
||||
})
|
||||
else
|
||||
# Fallback for testing
|
||||
packets_module.get_recent_packets_optimized(%{
|
||||
bounds: bounds,
|
||||
limit: batch_size,
|
||||
offset: offset
|
||||
})
|
||||
try do
|
||||
if packets_module == Aprsme.Packets do
|
||||
# Use cached queries for better performance
|
||||
# Include zoom level in cache key for better cache efficiency
|
||||
Aprsme.CachedQueries.get_recent_packets_cached(%{
|
||||
bounds: bounds,
|
||||
limit: batch_size,
|
||||
offset: offset,
|
||||
zoom: zoom
|
||||
})
|
||||
else
|
||||
# Fallback for testing
|
||||
packets_module.get_recent_packets_optimized(%{
|
||||
bounds: bounds,
|
||||
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
|
||||
|
||||
if Enum.any?(historical_packets) do
|
||||
# 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
|
||||
# Use LiveView's efficient push_event for incremental updates
|
||||
|
|
@ -1672,20 +1750,21 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
end
|
||||
|
||||
defp handle_valid_bounds_update(map_bounds, socket) do
|
||||
# Only schedule a bounds update if the bounds have actually changed (with rounding)
|
||||
if compare_bounds(map_bounds, socket.assigns.map_bounds) do
|
||||
{:noreply, socket}
|
||||
else
|
||||
# If this is the first bounds update (map_bounds is nil), process immediately
|
||||
# to avoid race condition with historical packet loading
|
||||
if is_nil(socket.assigns.map_bounds) do
|
||||
# Process immediately for initial bounds
|
||||
# Force processing if we need initial historical load, regardless of bounds comparison
|
||||
cond do
|
||||
socket.assigns[:needs_initial_historical_load] ->
|
||||
require Logger
|
||||
|
||||
Logger.debug("Processing initial bounds update immediately (forced): #{inspect(map_bounds)}")
|
||||
socket = process_bounds_update(map_bounds, socket)
|
||||
{:noreply, socket}
|
||||
else
|
||||
|
||||
compare_bounds(map_bounds, socket.assigns.map_bounds) ->
|
||||
{:noreply, socket}
|
||||
|
||||
true ->
|
||||
# For subsequent updates, use the timer to debounce
|
||||
schedule_bounds_update(map_bounds, socket)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -1701,6 +1780,21 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
|
||||
@spec process_bounds_update(map(), Socket.t()) :: Socket.t()
|
||||
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
|
||||
new_visible_packets =
|
||||
socket.assigns.visible_packets
|
||||
|
|
@ -1722,14 +1816,37 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
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})
|
||||
|
||||
# Load historical packets for the new bounds
|
||||
# Always load historical packets when bounds change to ensure new areas have data
|
||||
# Update map bounds FIRST so progressive loading uses the correct bounds
|
||||
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)
|
||||
|
||||
# Update map bounds and visible packets
|
||||
assign(socket, map_bounds: map_bounds, visible_packets: new_visible_packets)
|
||||
# Mark initial historical as completed if this was the initial load
|
||||
if is_initial_load do
|
||||
assign(socket, initial_historical_completed: true)
|
||||
else
|
||||
socket
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
6
priv/repo/migrations/20250710175129_test_query.exs
Normal file
6
priv/repo/migrations/20250710175129_test_query.exs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
defmodule Aprsme.Repo.Migrations.TestQuery do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
end
|
||||
end
|
||||
90
test/aprsme/packets/query_builder_test.exs
Normal file
90
test/aprsme/packets/query_builder_test.exs
Normal 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
|
||||
Loading…
Add table
Reference in a new issue