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) {
trailState = { positions: [] };
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)
const lastPos = trailState.positions[trailState.positions.length - 1];
if (
!lastPos ||
Math.abs(lastPos.lat - lat) > 0.00001 ||
Math.abs(lastPos.lng - lng) > 0.00001
) {
// Check if this position already exists (avoid duplicates)
const existingPos = trailState.positions.find(
(pos) =>
Math.abs(pos.lat - lat) < 0.00001 &&
Math.abs(pos.lng - lng) < 0.00001 &&
Math.abs(pos.timestamp - timestamp) < 1000, // Within 1 second
);
if (!existingPos) {
// Add new position
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.
@ -79,11 +86,15 @@ export class TrailManager {
// Ensure markerId is a string
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_")) {
// Remove hist_ prefix and any trailing index
return id.replace(/^hist_/, "").replace(/_\d+$/, "");
// Remove hist_ prefix and any trailing numeric index
const withoutPrefix = id.replace(/^hist_/, "");
// Remove trailing _digits pattern
return withoutPrefix.replace(/_\d+$/, "");
}
// For regular callsigns or IDs, return as-is
return id;
}
@ -113,6 +124,10 @@ export class TrailManager {
className: "historical-trail-line",
}).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
trailState.dots = [];
}

View file

@ -499,9 +499,73 @@ let MapAPRSMap = {
// Handle new packets from LiveView
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({
...data,
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),
openPopup: true,
});
@ -683,7 +747,10 @@ let MapAPRSMap = {
? new Date(data.timestamp).getTime()
: data.timestamp
: 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) {
@ -738,7 +805,8 @@ let MapAPRSMap = {
popup: data.popup,
historical: data.historical,
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
@ -749,7 +817,10 @@ let MapAPRSMap = {
? new Date(data.timestamp).getTime()
: data.timestamp
: 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
@ -764,15 +835,18 @@ let MapAPRSMap = {
const markerId = typeof id === "string" ? id : String(id);
const marker = self.markers!.get(markerId);
const markerState = self.markerStates!.get(markerId);
if (marker) {
self.markerLayer!.removeLayer(marker);
self.markers!.delete(markerId);
self.markerStates!.delete(markerId);
}
// Remove trail
// Remove trail - use callsign_group for proper trail identification
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()
: data.timestamp
: 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()
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
new_visible_packets =
socket.assigns.visible_packets
@ -259,7 +260,13 @@ defmodule AprsWeb.MapLive.Index do
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)
end
@ -746,6 +753,107 @@ defmodule AprsWeb.MapLive.Index do
Enum.sort_by(packets, fn packet -> packet.received_at 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()
defp within_bounds?(packet, bounds) do
{lat, lon, _data_extended} = MapHelpers.get_coordinates(packet)