historical lines
This commit is contained in:
parent
8fdafcf01e
commit
cd011b9667
3 changed files with 217 additions and 18 deletions
|
|
@ -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 = [];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue