historical lines

This commit is contained in:
Graham McIntire 2025-06-21 13:40:36 -05:00
parent 8fdafcf01e
commit cd011b9667
No known key found for this signature in database
3 changed files with 217 additions and 18 deletions

View file

@ -48,16 +48,23 @@ export class TrailManager {
if (!trailState) { if (!trailState) {
trailState = { positions: [] }; trailState = { positions: [] };
this.trails.set(baseCallsign, trailState); this.trails.set(baseCallsign, trailState);
console.log(`TrailManager: Created new trail for ${baseCallsign}`);
} }
// Only add if position is different from the last one (more than ~1 meter) // Check if this position already exists (avoid duplicates)
const lastPos = trailState.positions[trailState.positions.length - 1]; const existingPos = trailState.positions.find(
if ( (pos) =>
!lastPos || Math.abs(pos.lat - lat) < 0.00001 &&
Math.abs(lastPos.lat - lat) > 0.00001 || Math.abs(pos.lng - lng) < 0.00001 &&
Math.abs(lastPos.lng - lng) > 0.00001 Math.abs(pos.timestamp - timestamp) < 1000, // Within 1 second
) { );
if (!existingPos) {
// Add new position
trailState.positions.push({ lat, lng, timestamp }); trailState.positions.push({ lat, lng, timestamp });
// Sort positions by timestamp to maintain chronological order
trailState.positions.sort((a, b) => a.timestamp - b.timestamp);
} }
// For historical dots, keep all positions. For live positions, filter by time. // For historical dots, keep all positions. For live positions, filter by time.
@ -79,11 +86,15 @@ export class TrailManager {
// Ensure markerId is a string // Ensure markerId is a string
const id = typeof markerId === "string" ? markerId : String(markerId); const id = typeof markerId === "string" ? markerId : String(markerId);
// Extract base callsign from various ID formats // For historical markers like "hist_CALLSIGN_123", extract the CALLSIGN part
if (id.startsWith("hist_")) { if (id.startsWith("hist_")) {
// Remove hist_ prefix and any trailing index // Remove hist_ prefix and any trailing numeric index
return id.replace(/^hist_/, "").replace(/_\d+$/, ""); const withoutPrefix = id.replace(/^hist_/, "");
// Remove trailing _digits pattern
return withoutPrefix.replace(/_\d+$/, "");
} }
// For regular callsigns or IDs, return as-is
return id; return id;
} }
@ -113,6 +124,10 @@ export class TrailManager {
className: "historical-trail-line", className: "historical-trail-line",
}).addTo(this.trailLayer); }).addTo(this.trailLayer);
console.log(
`TrailManager: Created trail line for ${baseCallsign} with ${trailState.positions.length} positions`,
);
// Don't create additional dots here since historical positions are now shown as markers // Don't create additional dots here since historical positions are now shown as markers
trailState.dots = []; trailState.dots = [];
} }

View file

@ -499,9 +499,73 @@ let MapAPRSMap = {
// Handle new packets from LiveView // Handle new packets from LiveView
self.handleEvent("new_packet", (data: MarkerData) => { self.handleEvent("new_packet", (data: MarkerData) => {
// Check if there's already a marker for this callsign
const incomingCallsign = data.callsign_group || data.callsign || data.id;
if (incomingCallsign) {
// Find existing marker for this callsign - check both live and historical markers
let existingMarkerId: string | null = null;
let existingCallsign: string | null = null;
self.markerStates!.forEach((state, id) => {
// Extract callsign from various sources
const stateCallsign =
state.callsign_group ||
(typeof id === "string" && !id.startsWith("hist_") ? id : null) ||
(typeof id === "string" && id.startsWith("hist_")
? id.replace(/^hist_/, "").replace(/_\d+$/, "")
: null);
// Match callsigns (only consider non-historical markers for conversion)
if (stateCallsign === incomingCallsign && !state.historical) {
existingMarkerId = id;
existingCallsign = stateCallsign;
}
});
// If we found an existing live marker, convert it to a historical dot
if (existingMarkerId && existingCallsign) {
const existingMarker = self.markers!.get(existingMarkerId);
const existingState = self.markerStates!.get(existingMarkerId);
if (existingMarker && existingState) {
// Create historical dot data from existing marker
const historicalData = {
id: `hist_${existingCallsign}_${Date.now()}`,
lat: existingState.lat,
lng: existingState.lng,
callsign: existingCallsign,
callsign_group: existingCallsign,
symbol_table_id: existingState.symbol_table,
symbol_code: existingState.symbol_code,
historical: true,
is_most_recent_for_callsign: false,
timestamp: data.timestamp
? typeof data.timestamp === "string"
? new Date(data.timestamp).getTime() - 1000
: data.timestamp - 1000
: Date.now() - 1000,
popup: existingState.popup,
};
// Remove the existing marker (this will also clean up its trail reference)
self.removeMarker(existingMarkerId);
// Add the historical dot marker
self.addMarker({
...historicalData,
popup: self.buildPopupContent(historicalData),
});
}
}
}
// Add the new marker with proper callsign grouping
self.addMarker({ self.addMarker({
...data, ...data,
historical: false, historical: false,
is_most_recent_for_callsign: true,
callsign_group: data.callsign_group || data.callsign || incomingCallsign, // Ensure callsign_group is set for trail grouping
popup: self.buildPopupContent(data), popup: self.buildPopupContent(data),
openPopup: true, openPopup: true,
}); });
@ -683,7 +747,10 @@ let MapAPRSMap = {
? new Date(data.timestamp).getTime() ? new Date(data.timestamp).getTime()
: data.timestamp : data.timestamp
: Date.now(); : Date.now();
self.trailManager.addPosition(data.id, lat, lng, timestamp, isHistoricalDot); // Use callsign_group for proper trail grouping - prioritize callsign_group, then callsign, then id
const trailId = data.callsign_group || data.callsign || data.id;
self.trailManager.addPosition(trailId, lat, lng, timestamp, isHistoricalDot);
} }
if (!positionChanged && !dataChanged) { if (!positionChanged && !dataChanged) {
@ -738,7 +805,8 @@ let MapAPRSMap = {
popup: data.popup, popup: data.popup,
historical: data.historical, historical: data.historical,
is_most_recent_for_callsign: data.is_most_recent_for_callsign, is_most_recent_for_callsign: data.is_most_recent_for_callsign,
callsign_group: data.callsign_group, callsign_group: data.callsign_group || data.callsign,
callsign: data.callsign,
}); });
// Initialize trail for new marker - always add to trail for line drawing // Initialize trail for new marker - always add to trail for line drawing
@ -749,7 +817,10 @@ let MapAPRSMap = {
? new Date(data.timestamp).getTime() ? new Date(data.timestamp).getTime()
: data.timestamp : data.timestamp
: Date.now(); : Date.now();
self.trailManager.addPosition(data.id, lat, lng, timestamp, isHistoricalDot); // Use callsign_group for proper trail grouping - prioritize callsign_group, then callsign, then id
const trailId = data.callsign_group || data.callsign || data.id;
self.trailManager.addPosition(trailId, lat, lng, timestamp, isHistoricalDot);
} }
// Open popup if requested // Open popup if requested
@ -764,15 +835,18 @@ let MapAPRSMap = {
const markerId = typeof id === "string" ? id : String(id); const markerId = typeof id === "string" ? id : String(id);
const marker = self.markers!.get(markerId); const marker = self.markers!.get(markerId);
const markerState = self.markerStates!.get(markerId);
if (marker) { if (marker) {
self.markerLayer!.removeLayer(marker); self.markerLayer!.removeLayer(marker);
self.markers!.delete(markerId); self.markers!.delete(markerId);
self.markerStates!.delete(markerId); self.markerStates!.delete(markerId);
} }
// Remove trail // Remove trail - use callsign_group for proper trail identification
if (self.trailManager) { if (self.trailManager) {
self.trailManager.removeTrail(markerId); const trailId = markerState?.callsign_group || markerState?.callsign || markerId;
self.trailManager.removeTrail(trailId);
} }
}, },
@ -800,7 +874,9 @@ let MapAPRSMap = {
? new Date(data.timestamp).getTime() ? new Date(data.timestamp).getTime()
: data.timestamp : data.timestamp
: Date.now(); : Date.now();
self.trailManager.addPosition(data.id, lat, lng, timestamp, isHistoricalDot); // Use callsign_group for proper trail grouping - prioritize callsign_group, then callsign, then id
const trailId = data.callsign_group || data.callsign || data.id;
self.trailManager.addPosition(trailId, lat, lng, timestamp, isHistoricalDot);
} }
} }
} }

View file

@ -237,7 +237,8 @@ defmodule AprsWeb.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
Logger.debug("process_bounds_update: (no DB query) for bounds #{inspect(map_bounds)}") Logger.debug("process_bounds_update: Loading historical packets for bounds #{inspect(map_bounds)}")
# 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
@ -259,7 +260,13 @@ defmodule AprsWeb.MapLive.Index do
end) end)
end end
# Only update visible_packets with the packets still in bounds # Clear existing historical packets
socket = push_event(socket, "clear_historical_packets", %{})
# Load historical packets for the new bounds
socket = load_historical_packets_for_bounds(socket, map_bounds)
# Update map bounds and visible packets
assign(socket, map_bounds: map_bounds, visible_packets: new_visible_packets) assign(socket, map_bounds: map_bounds, visible_packets: new_visible_packets)
end end
@ -746,6 +753,107 @@ defmodule AprsWeb.MapLive.Index do
Enum.sort_by(packets, fn packet -> packet.received_at end) Enum.sort_by(packets, fn packet -> packet.received_at end)
end end
@spec load_historical_packets_for_bounds(Socket.t(), map()) :: Socket.t()
defp load_historical_packets_for_bounds(socket, map_bounds) do
# Get time range for historical data
now = DateTime.utc_now()
one_hour_ago = DateTime.add(now, -60 * 60, :second)
# Convert map bounds to the format expected by the database query
bounds = [
map_bounds.west,
map_bounds.south,
map_bounds.east,
map_bounds.north
]
# Fetch historical packets with position data within the current map bounds
historical_packets = fetch_historical_packets(bounds, one_hour_ago, now)
if Enum.empty?(historical_packets) do
# No historical packets found
socket
else
# Group packets by callsign-ssid and process all positions
packet_data_list =
historical_packets
|> Enum.group_by(&generate_callsign/1)
|> Enum.flat_map(fn {callsign, packets} ->
# Sort packets by inserted_at to identify the most recent
sorted_packets =
Enum.sort_by(
packets,
fn packet ->
case packet.inserted_at do
%NaiveDateTime{} = naive_dt ->
DateTime.from_naive!(naive_dt, "Etc/UTC")
%DateTime{} = dt ->
dt
_other ->
DateTime.utc_now()
end
end,
{:desc, DateTime}
)
# Filter out packets with unchanged positions
unique_position_packets = filter_unique_positions(sorted_packets)
# Process all packets with unique positions, marking which is the most recent
unique_position_packets
|> Enum.with_index()
|> Enum.map(fn {packet, index} ->
case build_packet_data(packet) do
nil ->
nil
packet_data ->
# Generate a unique ID for this historical packet
packet_id =
"hist_#{if Map.has_key?(packet, :id), do: packet.id, else: System.unique_integer([:positive])}_#{index}"
is_most_recent = index == 0
packet_data
|> Map.put("id", packet_id)
|> Map.put("is_historical", true)
|> Map.put("is_most_recent_for_callsign", is_most_recent)
|> Map.put("callsign_group", callsign)
|> Map.put(
"timestamp",
case packet.inserted_at do
%NaiveDateTime{} = naive_dt ->
DateTime.to_unix(DateTime.from_naive!(naive_dt, "Etc/UTC"), :millisecond)
%DateTime{} = dt ->
DateTime.to_unix(dt, :millisecond)
_other ->
DateTime.to_unix(DateTime.utc_now(), :millisecond)
end
)
end
end)
|> Enum.filter(& &1)
end)
# Send all historical packets at once
socket = push_event(socket, "add_historical_packets", %{packets: packet_data_list})
# Store historical packets in assigns for reference
historical_packets_map =
packet_data_list
|> Enum.zip(historical_packets)
|> Enum.reduce(%{}, fn {packet_data, packet}, acc ->
Map.put(acc, packet_data["id"], packet)
end)
assign(socket, historical_packets: historical_packets_map)
end
end
@spec within_bounds?(map() | struct(), map()) :: boolean() @spec within_bounds?(map() | struct(), map()) :: boolean()
defp within_bounds?(packet, bounds) do defp within_bounds?(packet, bounds) do
{lat, lon, _data_extended} = MapHelpers.get_coordinates(packet) {lat, lon, _data_extended} = MapHelpers.get_coordinates(packet)