misc fixes
This commit is contained in:
parent
01ee6eada5
commit
e7214f5064
8 changed files with 197 additions and 153 deletions
137
assets/js/map.ts
137
assets/js/map.ts
|
|
@ -64,6 +64,7 @@ interface MarkerState {
|
||||||
historical?: boolean;
|
historical?: boolean;
|
||||||
is_most_recent_for_callsign?: boolean;
|
is_most_recent_for_callsign?: boolean;
|
||||||
callsign_group?: string;
|
callsign_group?: string;
|
||||||
|
callsign?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MapEventData {
|
interface MapEventData {
|
||||||
|
|
@ -503,69 +504,60 @@ let MapAPRSMap = {
|
||||||
const incomingCallsign = data.callsign_group || data.callsign || data.id;
|
const incomingCallsign = data.callsign_group || data.callsign || data.id;
|
||||||
|
|
||||||
if (incomingCallsign) {
|
if (incomingCallsign) {
|
||||||
// Find existing marker for this callsign - check both live and historical markers
|
// Find existing live markers for this callsign and convert them to historical dots
|
||||||
let existingMarkerId: string | null = null;
|
const markersToConvert: string[] = [];
|
||||||
let existingCallsign: string | null = null;
|
|
||||||
|
|
||||||
self.markerStates!.forEach((state, id) => {
|
self.markerStates!.forEach((state, id) => {
|
||||||
// Extract callsign from various sources
|
const stateCallsign = state.callsign_group || state.callsign;
|
||||||
const stateCallsign =
|
// Only convert non-historical markers for the same callsign, but exclude the incoming packet's ID
|
||||||
state.callsign_group ||
|
if (
|
||||||
(typeof id === "string" && !id.startsWith("hist_") ? id : null) ||
|
stateCallsign === incomingCallsign &&
|
||||||
(typeof id === "string" && id.startsWith("hist_")
|
!state.historical &&
|
||||||
? id.replace(/^hist_/, "").replace(/_\d+$/, "")
|
String(id) !== String(data.id)
|
||||||
: null);
|
) {
|
||||||
|
markersToConvert.push(String(id));
|
||||||
// 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
|
// Convert existing live markers to historical dots by updating their icon
|
||||||
if (existingMarkerId && existingCallsign) {
|
markersToConvert.forEach((id) => {
|
||||||
const existingMarker = self.markers!.get(existingMarkerId);
|
const existingMarker = self.markers!.get(id);
|
||||||
const existingState = self.markerStates!.get(existingMarkerId);
|
const existingState = self.markerStates!.get(id);
|
||||||
|
|
||||||
if (existingMarker && existingState) {
|
if (existingMarker && existingState) {
|
||||||
// Create historical dot data from existing marker
|
// Update the state to historical
|
||||||
const historicalData = {
|
existingState.historical = true;
|
||||||
id: `hist_${existingCallsign}_${Date.now()}`,
|
existingState.is_most_recent_for_callsign = false;
|
||||||
|
|
||||||
|
// Create new historical dot icon
|
||||||
|
const historicalIconData = {
|
||||||
|
id: String(id),
|
||||||
lat: existingState.lat,
|
lat: existingState.lat,
|
||||||
lng: existingState.lng,
|
lng: existingState.lng,
|
||||||
callsign: existingCallsign,
|
callsign: existingState.callsign || incomingCallsign,
|
||||||
callsign_group: existingCallsign,
|
callsign_group: existingState.callsign_group || incomingCallsign,
|
||||||
symbol_table_id: existingState.symbol_table,
|
symbol_table_id: existingState.symbol_table,
|
||||||
symbol_code: existingState.symbol_code,
|
symbol_code: existingState.symbol_code,
|
||||||
historical: true,
|
historical: true,
|
||||||
is_most_recent_for_callsign: false,
|
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,
|
popup: existingState.popup,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remove the existing marker (this will also clean up its trail reference)
|
// Update the marker's icon to show as a dot
|
||||||
self.removeMarker(existingMarkerId);
|
existingMarker.setIcon(self.createMarkerIcon(historicalIconData));
|
||||||
|
|
||||||
// Add the historical dot marker
|
// Mark as historical
|
||||||
self.addMarker({
|
(existingMarker as any)._isHistorical = true;
|
||||||
...historicalData,
|
|
||||||
popup: self.buildPopupContent(historicalData),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the new marker with proper callsign grouping
|
// Add the new marker as the most recent for this callsign
|
||||||
self.addMarker({
|
self.addMarker({
|
||||||
...data,
|
...data,
|
||||||
historical: false,
|
historical: false,
|
||||||
is_most_recent_for_callsign: true,
|
is_most_recent_for_callsign: true,
|
||||||
callsign_group: data.callsign_group || data.callsign || incomingCallsign, // Ensure callsign_group is set for trail grouping
|
callsign_group: data.callsign_group || data.callsign || incomingCallsign,
|
||||||
popup: self.buildPopupContent(data),
|
popup: self.buildPopupContent(data),
|
||||||
openPopup: true,
|
openPopup: true,
|
||||||
});
|
});
|
||||||
|
|
@ -655,14 +647,46 @@ let MapAPRSMap = {
|
||||||
|
|
||||||
// Handle clearing historical packets
|
// Handle clearing historical packets
|
||||||
self.handleEvent("clear_historical_packets", () => {
|
self.handleEvent("clear_historical_packets", () => {
|
||||||
// Remove all historical markers
|
// 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) => {
|
||||||
if ((marker as any)._isHistorical) {
|
const markerState = self.markerStates!.get(String(id));
|
||||||
|
// Only remove markers that are explicitly historical
|
||||||
|
if ((marker as any)._isHistorical || (markerState && markerState.historical)) {
|
||||||
markersToRemove.push(String(id));
|
markersToRemove.push(String(id));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
markersToRemove.forEach((id) => self.removeMarker(id));
|
|
||||||
|
// Remove historical markers without affecting trails of live markers
|
||||||
|
markersToRemove.forEach((id) => {
|
||||||
|
const marker = self.markers!.get(id);
|
||||||
|
const markerState = self.markerStates!.get(id);
|
||||||
|
|
||||||
|
if (marker) {
|
||||||
|
// Remove from map and storage
|
||||||
|
self.markerLayer!.removeLayer(marker);
|
||||||
|
self.markers!.delete(id);
|
||||||
|
self.markerStates!.delete(id);
|
||||||
|
|
||||||
|
// Only remove trail if this was the only marker for this callsign
|
||||||
|
if (self.trailManager && markerState) {
|
||||||
|
const callsign = markerState.callsign_group || markerState.callsign || id;
|
||||||
|
// Check if there are any live markers left for this callsign
|
||||||
|
let hasLiveMarkers = false;
|
||||||
|
self.markerStates!.forEach((state) => {
|
||||||
|
const stateCallsign = state.callsign_group || state.callsign;
|
||||||
|
if (stateCallsign === callsign && !state.historical) {
|
||||||
|
hasLiveMarkers = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Only remove trail if no live markers remain
|
||||||
|
if (!hasLiveMarkers) {
|
||||||
|
self.trailManager.removeTrail(callsign);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle bounds-based marker filtering
|
// Handle bounds-based marker filtering
|
||||||
|
|
@ -681,6 +705,11 @@ let MapAPRSMap = {
|
||||||
self.handleEvent("clear_and_reload_markers", () => {
|
self.handleEvent("clear_and_reload_markers", () => {
|
||||||
// This event is just a trigger - the server will handle clearing and adding markers
|
// This event is just a trigger - the server will handle clearing and adding markers
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle clear_all_markers event
|
||||||
|
self.handleEvent("clear_all_markers", () => {
|
||||||
|
self.clearAllMarkers();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
sendBoundsToServer() {
|
sendBoundsToServer() {
|
||||||
|
|
@ -763,8 +792,8 @@ let MapAPRSMap = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove existing marker if it exists
|
// Remove existing marker if it exists (without affecting trails since we're just replacing it)
|
||||||
self.removeMarker(data.id);
|
self.removeMarkerWithoutTrail(data.id);
|
||||||
|
|
||||||
// Create marker - use simple dot for older historical positions, APRS icon for most recent
|
// Create marker - use simple dot for older historical positions, APRS icon for most recent
|
||||||
const marker = L.marker([lat, lng], {
|
const marker = L.marker([lat, lng], {
|
||||||
|
|
@ -934,8 +963,24 @@ let MapAPRSMap = {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Remove out-of-bounds markers
|
// Remove out-of-bounds markers without affecting trails
|
||||||
markersToRemove.forEach((id) => self.removeMarker(String(id)));
|
markersToRemove.forEach((id) => self.removeMarkerWithoutTrail(String(id)));
|
||||||
|
},
|
||||||
|
|
||||||
|
removeMarkerWithoutTrail(id: string | any) {
|
||||||
|
const self = this as unknown as LiveViewHookContext;
|
||||||
|
// Ensure id is a string
|
||||||
|
const markerId = typeof id === "string" ? id : String(id);
|
||||||
|
|
||||||
|
const marker = self.markers!.get(markerId);
|
||||||
|
|
||||||
|
if (marker) {
|
||||||
|
// Remove marker from map but don't touch trails
|
||||||
|
self.markerLayer!.removeLayer(marker);
|
||||||
|
self.markers!.delete(markerId);
|
||||||
|
self.markerStates!.delete(markerId);
|
||||||
|
// Note: We intentionally don't remove trails here
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
createMarkerIcon(data: MarkerData): L.DivIcon {
|
createMarkerIcon(data: MarkerData): L.DivIcon {
|
||||||
|
|
|
||||||
|
|
@ -394,20 +394,8 @@ defmodule Aprs.Packets do
|
||||||
defp filter_by_region(query, _), do: query
|
defp filter_by_region(query, _), do: query
|
||||||
|
|
||||||
defp filter_by_callsign(query, %{callsign: callsign}) do
|
defp filter_by_callsign(query, %{callsign: callsign}) do
|
||||||
# Support both exact match and partial match
|
# Use sender field for exact callsign matching
|
||||||
# For exact match, check if callsign contains a hyphen (has SSID)
|
from p in query, where: ilike(p.sender, ^callsign)
|
||||||
if String.contains?(callsign, "-") do
|
|
||||||
# Exact match for callsign with SSID
|
|
||||||
[base_call, ssid] = String.split(callsign, "-", parts: 2)
|
|
||||||
|
|
||||||
from p in query,
|
|
||||||
where:
|
|
||||||
ilike(p.base_callsign, ^base_call) and
|
|
||||||
((is_nil(p.ssid) and ^ssid == "0") or p.ssid == ^ssid)
|
|
||||||
else
|
|
||||||
# Match base callsign exactly, regardless of SSID
|
|
||||||
from p in query, where: ilike(p.base_callsign, ^callsign)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp filter_by_callsign(query, _), do: query
|
defp filter_by_callsign(query, _), do: query
|
||||||
|
|
|
||||||
|
|
@ -26,13 +26,8 @@ defmodule AprsWeb.MapLive.CallsignView do
|
||||||
page_title: "APRS Map - #{normalized_callsign}",
|
page_title: "APRS Map - #{normalized_callsign}",
|
||||||
# Track visible packets by callsign
|
# Track visible packets by callsign
|
||||||
visible_packets: %{},
|
visible_packets: %{},
|
||||||
# Default bounds - will be updated based on packet locations
|
# Map bounds - will be set when first bounds update is received
|
||||||
map_bounds: %{
|
map_bounds: nil,
|
||||||
north: 49.0,
|
|
||||||
south: 24.0,
|
|
||||||
east: -66.0,
|
|
||||||
west: -125.0
|
|
||||||
},
|
|
||||||
map_center: @default_center,
|
map_center: @default_center,
|
||||||
map_zoom: @default_zoom,
|
map_zoom: @default_zoom,
|
||||||
default_center: @default_center,
|
default_center: @default_center,
|
||||||
|
|
@ -62,7 +57,9 @@ defmodule AprsWeb.MapLive.CallsignView do
|
||||||
packets_loaded: false,
|
packets_loaded: false,
|
||||||
# Latest symbol table ID and code
|
# Latest symbol table ID and code
|
||||||
latest_symbol_table_id: "/",
|
latest_symbol_table_id: "/",
|
||||||
latest_symbol_code: ">"
|
latest_symbol_code: ">",
|
||||||
|
# Flag to track if latest marker was already pushed
|
||||||
|
latest_marker_pushed: false
|
||||||
)
|
)
|
||||||
|
|
||||||
if connected?(socket) do
|
if connected?(socket) do
|
||||||
|
|
@ -164,14 +161,7 @@ defmodule AprsWeb.MapLive.CallsignView do
|
||||||
|> assign(packets_loaded: true)
|
|> assign(packets_loaded: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Auto-start replay if it hasn't been started yet
|
# Don't start historical replay yet - wait for bounds to be available
|
||||||
socket =
|
|
||||||
if socket.assigns.replay_started do
|
|
||||||
socket
|
|
||||||
else
|
|
||||||
socket = start_historical_replay(socket)
|
|
||||||
assign(socket, replay_started: true, replay_active: true)
|
|
||||||
end
|
|
||||||
|
|
||||||
# If we have a pending geolocation, zoom to it after a delay
|
# If we have a pending geolocation, zoom to it after a delay
|
||||||
socket =
|
socket =
|
||||||
|
|
@ -208,21 +198,32 @@ defmodule AprsWeb.MapLive.CallsignView do
|
||||||
|> Enum.filter(fn {_k, packet} -> MapHelpers.within_bounds?(packet, normalized_bounds) end)
|
|> Enum.filter(fn {_k, packet} -> MapHelpers.within_bounds?(packet, normalized_bounds) end)
|
||||||
|> Map.new()
|
|> Map.new()
|
||||||
|
|
||||||
markers_to_remove =
|
packets_to_remove =
|
||||||
socket.assigns.visible_packets
|
socket.assigns.visible_packets
|
||||||
|> Enum.reject(fn {_k, packet} -> MapHelpers.within_bounds?(packet, normalized_bounds) end)
|
|> Enum.reject(fn {_k, packet} -> MapHelpers.within_bounds?(packet, normalized_bounds) end)
|
||||||
|> Enum.map(fn {k, _} -> k end)
|
|> Enum.map(fn {k, _} -> k end)
|
||||||
|
|
||||||
socket =
|
socket =
|
||||||
if markers_to_remove == [] do
|
if packets_to_remove == [] do
|
||||||
socket
|
socket
|
||||||
else
|
else
|
||||||
Enum.reduce(markers_to_remove, socket, fn k, acc ->
|
Enum.reduce(packets_to_remove, socket, fn k, acc ->
|
||||||
push_event(acc, "remove_marker", %{id: k})
|
push_event(acc, "remove_marker", %{id: k})
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
socket = assign(socket, map_bounds: normalized_bounds, visible_packets: new_visible_packets)
|
socket = assign(socket, map_bounds: normalized_bounds, visible_packets: new_visible_packets)
|
||||||
|
|
||||||
|
# Start historical replay now that we have bounds (if not already started)
|
||||||
|
socket =
|
||||||
|
if socket.assigns.map_ready and not socket.assigns.replay_started and
|
||||||
|
not is_nil(socket.assigns.map_bounds) do
|
||||||
|
socket = start_historical_replay(socket)
|
||||||
|
assign(socket, replay_started: true, replay_active: true)
|
||||||
|
else
|
||||||
|
socket
|
||||||
|
end
|
||||||
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -249,11 +250,12 @@ defmodule AprsWeb.MapLive.CallsignView do
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_info(:auto_start_replay, socket) do
|
def handle_info(:auto_start_replay, socket) do
|
||||||
if not socket.assigns.replay_started and socket.assigns.map_ready do
|
if not socket.assigns.replay_started and socket.assigns.map_ready and
|
||||||
|
not is_nil(socket.assigns.map_bounds) do
|
||||||
socket = start_historical_replay(socket)
|
socket = start_historical_replay(socket)
|
||||||
{:noreply, assign(socket, replay_started: true, replay_active: true)}
|
{:noreply, assign(socket, replay_started: true, replay_active: true)}
|
||||||
else
|
else
|
||||||
if not socket.assigns.map_ready do
|
if not socket.assigns.map_ready or is_nil(socket.assigns.map_bounds) do
|
||||||
Process.send_after(self(), :auto_start_replay, 1000)
|
Process.send_after(self(), :auto_start_replay, 1000)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -266,7 +268,9 @@ defmodule AprsWeb.MapLive.CallsignView do
|
||||||
|
|
||||||
def handle_info(%Phoenix.Socket.Broadcast{topic: "aprs_messages", event: "packet", payload: packet}, socket) do
|
def handle_info(%Phoenix.Socket.Broadcast{topic: "aprs_messages", event: "packet", payload: packet}, socket) do
|
||||||
# Only process packets for the specific callsign being viewed
|
# Only process packets for the specific callsign being viewed
|
||||||
if PacketUtils.generate_callsign(packet) == socket.assigns.callsign do
|
packet_sender = Map.get(packet, :sender, "")
|
||||||
|
|
||||||
|
if String.upcase(packet_sender) == String.upcase(socket.assigns.callsign) do
|
||||||
handle_info_postgres_packet(packet, socket)
|
handle_info_postgres_packet(packet, socket)
|
||||||
else
|
else
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
|
|
@ -280,11 +284,12 @@ defmodule AprsWeb.MapLive.CallsignView do
|
||||||
updated_visible_packets = Map.put(socket.assigns.visible_packets, key, packet)
|
updated_visible_packets = Map.put(socket.assigns.visible_packets, key, packet)
|
||||||
socket = assign(socket, visible_packets: updated_visible_packets)
|
socket = assign(socket, visible_packets: updated_visible_packets)
|
||||||
|
|
||||||
packet_data = PacketUtils.build_packet_data(packet)
|
# Live packets are always the most recent for their callsign
|
||||||
|
packet_data = PacketUtils.build_packet_data(packet, true)
|
||||||
|
|
||||||
socket =
|
socket =
|
||||||
if packet_data,
|
if packet_data,
|
||||||
do: push_event(socket, "add_markers", %{markers: [packet_data]}),
|
do: push_event(socket, "new_packet", packet_data),
|
||||||
else: socket
|
else: socket
|
||||||
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
|
|
@ -672,7 +677,8 @@ defmodule AprsWeb.MapLive.CallsignView do
|
||||||
fetch_historical_packets_for_callsign(
|
fetch_historical_packets_for_callsign(
|
||||||
socket.assigns.callsign,
|
socket.assigns.callsign,
|
||||||
one_hour_ago,
|
one_hour_ago,
|
||||||
now
|
now,
|
||||||
|
socket.assigns.map_bounds
|
||||||
)
|
)
|
||||||
|
|
||||||
# Always fetch the latest packet with a position, regardless of age
|
# Always fetch the latest packet with a position, regardless of age
|
||||||
|
|
@ -752,23 +758,23 @@ defmodule AprsWeb.MapLive.CallsignView do
|
||||||
|
|
||||||
historical_callsigns = MapSet.new(unique_position_packets, &PacketUtils.generate_callsign/1)
|
historical_callsigns = MapSet.new(unique_position_packets, &PacketUtils.generate_callsign/1)
|
||||||
|
|
||||||
socket =
|
{socket, latest_marker_pushed} =
|
||||||
if latest_packet && !MapSet.member?(historical_callsigns, latest_packet_id) do
|
if latest_packet && !MapSet.member?(historical_callsigns, latest_packet_id) do
|
||||||
# Push the latest marker as a regular marker (not historical)
|
# Push the latest marker as a regular marker (not historical)
|
||||||
packet_data = PacketUtils.build_packet_data(latest_packet)
|
packet_data = PacketUtils.build_packet_data(latest_packet, true)
|
||||||
|
|
||||||
if packet_data do
|
if packet_data do
|
||||||
push_event(socket, "add_markers", %{markers: [packet_data]})
|
{push_event(socket, "new_packet", packet_data), true}
|
||||||
else
|
else
|
||||||
socket
|
{socket, false}
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
socket
|
{socket, false}
|
||||||
end
|
end
|
||||||
|
|
||||||
if Enum.empty?(packet_data_list) do
|
if Enum.empty?(packet_data_list) do
|
||||||
# No historical packets found (but latest marker may have been pushed above)
|
# No historical packets found (but latest marker may have been pushed above)
|
||||||
socket
|
assign(socket, latest_marker_pushed: latest_marker_pushed)
|
||||||
else
|
else
|
||||||
# Clear any previous historical packets from the map
|
# Clear any previous historical packets from the map
|
||||||
socket = push_event(socket, "clear_historical_packets", %{})
|
socket = push_event(socket, "clear_historical_packets", %{})
|
||||||
|
|
@ -787,18 +793,28 @@ defmodule AprsWeb.MapLive.CallsignView do
|
||||||
replay_packets: [],
|
replay_packets: [],
|
||||||
replay_index: 0,
|
replay_index: 0,
|
||||||
replay_start_time: one_hour_ago,
|
replay_start_time: one_hour_ago,
|
||||||
replay_end_time: now
|
replay_end_time: now,
|
||||||
|
latest_marker_pushed: latest_marker_pushed
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp fetch_historical_packets_for_callsign(callsign, start_time, end_time) do
|
defp fetch_historical_packets_for_callsign(callsign, start_time, end_time, bounds) do
|
||||||
Packets.get_packets_for_replay(%{
|
params = %{
|
||||||
callsign: callsign,
|
callsign: callsign,
|
||||||
start_time: start_time,
|
start_time: start_time,
|
||||||
end_time: end_time,
|
end_time: end_time,
|
||||||
limit: 1000
|
limit: 1000
|
||||||
})
|
}
|
||||||
|
|
||||||
|
params =
|
||||||
|
if bounds do
|
||||||
|
Map.put(params, :bounds, [bounds.west, bounds.south, bounds.east, bounds.north])
|
||||||
|
else
|
||||||
|
params
|
||||||
|
end
|
||||||
|
|
||||||
|
Packets.get_packets_for_replay(params)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Filter packets to only include those with unique positions (lat/lon changed)
|
# Filter packets to only include those with unique positions (lat/lon changed)
|
||||||
|
|
@ -886,7 +902,12 @@ defmodule AprsWeb.MapLive.CallsignView do
|
||||||
defp maybe_push_latest_marker(socket, nil), do: socket
|
defp maybe_push_latest_marker(socket, nil), do: socket
|
||||||
|
|
||||||
defp maybe_push_latest_marker(socket, packet) do
|
defp maybe_push_latest_marker(socket, packet) do
|
||||||
packet_data = PacketUtils.build_packet_data(packet)
|
# Only push if we haven't already pushed it during historical replay
|
||||||
if packet_data, do: push_event(socket, "add_markers", %{markers: [packet_data]}), else: socket
|
if Map.get(socket.assigns, :latest_marker_pushed, false) do
|
||||||
|
socket
|
||||||
|
else
|
||||||
|
packet_data = PacketUtils.build_packet_data(packet, true)
|
||||||
|
if packet_data, do: push_event(socket, "new_packet", packet_data), else: socket
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -361,10 +361,10 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Clear existing historical packets
|
# Remove only out-of-bounds historical packets instead of clearing all
|
||||||
socket = push_event(socket, "clear_historical_packets", %{})
|
socket = push_event(socket, "filter_markers_by_bounds", %{bounds: map_bounds})
|
||||||
|
|
||||||
# Load historical packets for the new bounds
|
# Load additional historical packets for the new bounds if needed
|
||||||
socket = load_historical_packets_for_bounds(socket, map_bounds)
|
socket = load_historical_packets_for_bounds(socket, map_bounds)
|
||||||
|
|
||||||
# Update map bounds and visible packets
|
# Update map bounds and visible packets
|
||||||
|
|
@ -496,11 +496,13 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
else: System.unique_integer([:positive])
|
else: System.unique_integer([:positive])
|
||||||
|
|
||||||
new_visible_packets = Map.put(socket.assigns.visible_packets, callsign_key, packet)
|
new_visible_packets = Map.put(socket.assigns.visible_packets, callsign_key, packet)
|
||||||
marker_data = PacketUtils.build_packet_data(packet)
|
|
||||||
|
# Live packets are always the most recent for their callsign
|
||||||
|
marker_data = PacketUtils.build_packet_data(packet, true)
|
||||||
|
|
||||||
socket =
|
socket =
|
||||||
if marker_data do
|
if marker_data do
|
||||||
push_event(socket, "add_markers", %{markers: [marker_data]})
|
push_event(socket, "new_packet", marker_data)
|
||||||
else
|
else
|
||||||
socket
|
socket
|
||||||
end
|
end
|
||||||
|
|
@ -965,8 +967,8 @@ defmodule AprsWeb.MapLive.Index do
|
||||||
# Clear existing historical packets
|
# Clear existing historical packets
|
||||||
socket = assign(socket, historical_packets: %{})
|
socket = assign(socket, historical_packets: %{})
|
||||||
|
|
||||||
# Clear all markers on the client
|
# Clear only historical markers on the client (preserve live markers)
|
||||||
socket = push_event(socket, "clear_all_markers", %{})
|
socket = push_event(socket, "clear_historical_packets", %{})
|
||||||
|
|
||||||
# Load historical packets with new time range
|
# Load historical packets with new time range
|
||||||
socket = load_historical_packets_for_bounds(socket, socket.assigns.map_bounds)
|
socket = load_historical_packets_for_bounds(socket, socket.assigns.map_bounds)
|
||||||
|
|
|
||||||
|
|
@ -81,9 +81,9 @@ defmodule AprsWeb.MapLive.PacketUtils do
|
||||||
@spec generate_callsign(map()) :: String.t()
|
@spec generate_callsign(map()) :: String.t()
|
||||||
def generate_callsign(packet) do
|
def generate_callsign(packet) do
|
||||||
base_callsign = get_packet_field(packet, :base_callsign, "")
|
base_callsign = get_packet_field(packet, :base_callsign, "")
|
||||||
ssid = get_packet_field(packet, :ssid, 0)
|
ssid = get_packet_field(packet, :ssid, nil)
|
||||||
|
|
||||||
if ssid != 0 and ssid != "" and ssid != nil do
|
if ssid != nil and ssid != "" do
|
||||||
"#{base_callsign}-#{ssid}"
|
"#{base_callsign}-#{ssid}"
|
||||||
else
|
else
|
||||||
base_callsign
|
base_callsign
|
||||||
|
|
@ -138,15 +138,20 @@ defmodule AprsWeb.MapLive.PacketUtils do
|
||||||
Map.get(data_extended, to_string(key)) || "N/A"
|
Map.get(data_extended, to_string(key)) || "N/A"
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_packet_data(packet) do
|
def build_packet_data(packet, is_most_recent_for_callsign) when is_boolean(is_most_recent_for_callsign) do
|
||||||
{lat, lon, data_extended} = MapHelpers.get_coordinates(packet)
|
{lat, lon, data_extended} = MapHelpers.get_coordinates(packet)
|
||||||
callsign = generate_callsign(packet)
|
callsign = generate_callsign(packet)
|
||||||
|
|
||||||
if lat && lon && callsign != "" && callsign != nil do
|
if lat && lon && callsign != "" && callsign != nil do
|
||||||
build_packet_map(packet, lat, lon, data_extended)
|
packet_data = build_packet_map(packet, lat, lon, data_extended)
|
||||||
|
Map.put(packet_data, "is_most_recent_for_callsign", is_most_recent_for_callsign)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def build_packet_data(packet) do
|
||||||
|
build_packet_data(packet, false)
|
||||||
|
end
|
||||||
|
|
||||||
defp build_packet_map(packet, lat, lon, data_extended) do
|
defp build_packet_map(packet, lat, lon, data_extended) do
|
||||||
data_extended = data_extended || %{}
|
data_extended = data_extended || %{}
|
||||||
packet_info = extract_packet_info(packet, data_extended)
|
packet_info = extract_packet_info(packet, data_extended)
|
||||||
|
|
@ -157,7 +162,7 @@ defmodule AprsWeb.MapLive.PacketUtils do
|
||||||
|
|
||||||
defp extract_packet_info(packet, data_extended) do
|
defp extract_packet_info(packet, data_extended) do
|
||||||
%{
|
%{
|
||||||
callsign: generate_callsign(packet),
|
callsign: get_packet_field(packet, :sender, ""),
|
||||||
symbol_table_id: get_packet_field(packet, :symbol_table_id, "/"),
|
symbol_table_id: get_packet_field(packet, :symbol_table_id, "/"),
|
||||||
symbol_code: get_packet_field(packet, :symbol_code, ">"),
|
symbol_code: get_packet_field(packet, :symbol_code, ">"),
|
||||||
timestamp: get_timestamp(packet),
|
timestamp: get_timestamp(packet),
|
||||||
|
|
@ -197,7 +202,7 @@ defmodule AprsWeb.MapLive.PacketUtils do
|
||||||
|
|
||||||
"""
|
"""
|
||||||
<div class="aprs-popup">
|
<div class="aprs-popup">
|
||||||
<div class="aprs-callsign"><strong><a href="/map/callsign/#{packet_info.callsign}">#{packet_info.callsign}</a></strong></div>
|
<div class="aprs-callsign"><strong><a href="/#{packet_info.callsign}">#{packet_info.callsign}</a></strong></div>
|
||||||
#{comment_html}
|
#{comment_html}
|
||||||
<div class="aprs-coords">#{Float.round(to_float(lat), 4)}, #{Float.round(to_float(lon), 4)}</div>
|
<div class="aprs-coords">#{Float.round(to_float(lat), 4)}, #{Float.round(to_float(lon), 4)}</div>
|
||||||
#{timestamp_html}
|
#{timestamp_html}
|
||||||
|
|
@ -205,7 +210,7 @@ defmodule AprsWeb.MapLive.PacketUtils do
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
defp build_weather_popup_html(packet, callsign) do
|
defp build_weather_popup_html(packet, sender) do
|
||||||
received_at = get_received_at(packet)
|
received_at = get_received_at(packet)
|
||||||
timestamp_dt = TimeHelpers.to_datetime(received_at)
|
timestamp_dt = TimeHelpers.to_datetime(received_at)
|
||||||
|
|
||||||
|
|
@ -222,7 +227,7 @@ defmodule AprsWeb.MapLive.PacketUtils do
|
||||||
end
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
<strong>#{callsign} - Weather Report</strong>
|
<strong>#{sender} - Weather Report</strong>
|
||||||
#{timestamp_html}
|
#{timestamp_html}
|
||||||
<hr style="margin-top: 4px; margin-bottom: 4px;">
|
<hr style="margin-top: 4px; margin-bottom: 4px;">
|
||||||
Temperature: #{get_weather_field(packet, :temperature)}°F<br>
|
Temperature: #{get_weather_field(packet, :temperature)}°F<br>
|
||||||
|
|
@ -236,11 +241,19 @@ defmodule AprsWeb.MapLive.PacketUtils do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp build_packet_result(packet, packet_info, lat, lon, popup) do
|
defp build_packet_result(packet, packet_info, lat, lon, popup) do
|
||||||
|
# Generate unique ID for live packets to prevent conflicts
|
||||||
|
packet_id =
|
||||||
|
if Map.has_key?(packet, :id) do
|
||||||
|
"live_#{packet.id}_#{System.unique_integer([:positive])}"
|
||||||
|
else
|
||||||
|
"live_#{packet_info.callsign}_#{System.unique_integer([:positive])}"
|
||||||
|
end
|
||||||
|
|
||||||
%{
|
%{
|
||||||
"id" => packet_info.callsign,
|
"id" => packet_id,
|
||||||
"callsign" => packet_info.callsign,
|
"callsign" => packet_info.callsign,
|
||||||
"base_callsign" => get_packet_field(packet, :base_callsign, ""),
|
"base_callsign" => get_packet_field(packet, :base_callsign, ""),
|
||||||
"ssid" => get_packet_field(packet, :ssid, 0),
|
"ssid" => get_packet_field(packet, :ssid, nil),
|
||||||
"lat" => to_float(lat),
|
"lat" => to_float(lat),
|
||||||
"lng" => to_float(lon),
|
"lng" => to_float(lon),
|
||||||
"data_type" => to_string(get_packet_field(packet, :data_type, "unknown")),
|
"data_type" => to_string(get_packet_field(packet, :data_type, "unknown")),
|
||||||
|
|
|
||||||
|
|
@ -95,20 +95,8 @@ defmodule AprsWeb.PacketsLive.CallsignView do
|
||||||
order_by: [desc: p.received_at],
|
order_by: [desc: p.received_at],
|
||||||
limit: ^limit
|
limit: ^limit
|
||||||
|
|
||||||
# Apply callsign filter
|
# Apply callsign filter using sender field for exact matching
|
||||||
filtered_query =
|
filtered_query = from p in query, where: ilike(p.sender, ^callsign)
|
||||||
if String.contains?(callsign, "-") do
|
|
||||||
# Exact match for callsign with SSID
|
|
||||||
[base_call, ssid] = String.split(callsign, "-", parts: 2)
|
|
||||||
|
|
||||||
from p in query,
|
|
||||||
where:
|
|
||||||
ilike(p.base_callsign, ^base_call) and
|
|
||||||
((is_nil(p.ssid) and ^ssid == "0") or p.ssid == ^ssid)
|
|
||||||
else
|
|
||||||
# Match base callsign exactly, regardless of SSID
|
|
||||||
from p in query, where: ilike(p.base_callsign, ^callsign)
|
|
||||||
end
|
|
||||||
|
|
||||||
filtered_query
|
filtered_query
|
||||||
|> Repo.all()
|
|> Repo.all()
|
||||||
|
|
@ -122,20 +110,15 @@ defmodule AprsWeb.PacketsLive.CallsignView do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp packet_matches_callsign?(packet, target_callsign) do
|
defp packet_matches_callsign?(packet, target_callsign) do
|
||||||
# Check if packet sender or base_callsign matches the target callsign
|
# Check exact match for sender field only
|
||||||
# Supports both exact matches and SSID variants (e.g., "N0CALL" matches "N0CALL-1")
|
|
||||||
sender = packet.sender || ""
|
sender = packet.sender || ""
|
||||||
base_callsign = packet.base_callsign || ""
|
|
||||||
|
|
||||||
# Convert to uppercase for case-insensitive comparison
|
# Convert to uppercase for case-insensitive comparison
|
||||||
sender_upper = String.upcase(sender)
|
sender_upper = String.upcase(sender)
|
||||||
base_upper = String.upcase(base_callsign)
|
|
||||||
target_upper = String.upcase(target_callsign)
|
target_upper = String.upcase(target_callsign)
|
||||||
|
|
||||||
# Check exact match for sender or base_callsign
|
# Exact match only
|
||||||
# Also check if the sender starts with the target callsign (for SSID variants)
|
sender_upper == target_upper
|
||||||
sender_upper == target_upper || base_upper == target_upper ||
|
|
||||||
String.starts_with?(sender_upper, target_upper <> "-")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Helper to get all packets (stored + live) in chronological order
|
# Helper to get all packets (stored + live) in chronological order
|
||||||
|
|
|
||||||
|
|
@ -74,19 +74,16 @@ defmodule Parser do
|
||||||
ssid =
|
ssid =
|
||||||
case List.last(callsign_parts) do
|
case List.last(callsign_parts) do
|
||||||
nil ->
|
nil ->
|
||||||
0
|
nil
|
||||||
|
|
||||||
s when is_binary(s) ->
|
s when is_binary(s) ->
|
||||||
case Integer.parse(s) do
|
s
|
||||||
{i, _} -> i
|
|
||||||
:error -> 0
|
|
||||||
end
|
|
||||||
|
|
||||||
i when is_integer(i) ->
|
i when is_integer(i) ->
|
||||||
i
|
to_string(i)
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
0
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
{:ok,
|
{:ok,
|
||||||
|
|
@ -1182,17 +1179,12 @@ defmodule Parser do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp extract_ssid(nil), do: 0
|
defp extract_ssid(nil), do: nil
|
||||||
|
|
||||||
defp extract_ssid(s) when is_binary(s) do
|
defp extract_ssid(s) when is_binary(s), do: s
|
||||||
case Integer.parse(s) do
|
|
||||||
{i, _} -> i
|
|
||||||
:error -> 0
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp extract_ssid(i) when is_integer(i), do: i
|
defp extract_ssid(i) when is_integer(i), do: to_string(i)
|
||||||
defp extract_ssid(_), do: 0
|
defp extract_ssid(_), do: nil
|
||||||
|
|
||||||
defp split_path_for_tunnel(path) do
|
defp split_path_for_tunnel(path) do
|
||||||
split_path(path)
|
split_path(path)
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,7 @@ defmodule Aprs.Integration.HistoricalPacketsTest do
|
||||||
test "only loads packets within map bounds", %{conn: conn, packets: mock_packets} do
|
test "only loads packets within map bounds", %{conn: conn, packets: mock_packets} do
|
||||||
# Mock to return only packets within the specified bounds
|
# Mock to return only packets within the specified bounds
|
||||||
# TEST2 is at lat: 38.8, lon: -97.5, which is outside these bounds
|
# TEST2 is at lat: 38.8, lon: -97.5, which is outside these bounds
|
||||||
test1_packets = Enum.filter(mock_packets, fn packet -> packet.base_callsign == "TEST1" end)
|
test1_packets = Enum.filter(mock_packets, fn packet -> packet.sender == "TEST1" end)
|
||||||
expect_packets_for_replay_with_bounds(test1_packets)
|
expect_packets_for_replay_with_bounds(test1_packets)
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/")
|
{:ok, view, _html} = live(conn, "/")
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue