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 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 }) {

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
*/
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;
}
}

View file

@ -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

View file

@ -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

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