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 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 }) {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
);
|
|
||||||
|
|
||||||
// Send combined map state update to server for URL and bounds updating
|
// Always save to localStorage, even if LiveView is disconnected
|
||||||
pushEvent("update_map_state", {
|
localStorage.setItem(
|
||||||
center: { lat: truncatedLat, lng: truncatedLng },
|
"aprs_map_state",
|
||||||
zoom: zoom,
|
JSON.stringify({ lat: truncatedLat, lng: truncatedLng, zoom }),
|
||||||
bounds: {
|
);
|
||||||
north: map.getBounds().getNorth(),
|
|
||||||
south: map.getBounds().getSouth(),
|
// Send combined map state update to server for URL and bounds updating
|
||||||
east: map.getBounds().getEast(),
|
const payload = {
|
||||||
west: map.getBounds().getWest(),
|
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
|
* 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
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