This commit is contained in:
Graham McIntire 2026-02-22 15:33:20 -06:00
parent c4bf50284b
commit f5049a6e2d
No known key found for this signature in database
4 changed files with 145 additions and 49 deletions

View file

@ -1748,6 +1748,46 @@ let MapAPRSMap = {
// Remove existing marker if it exists (without affecting trails since we're just replacing it)
self.removeMarkerWithoutTrail(data.id);
// When adding a "most recent" marker, convert any existing "current" markers
// for the same callsign_group to historical dots. This prevents duplicate labels
// when multiple senders report the same object (e.g. two stations tracking one radiosonde).
if (data.is_most_recent_for_callsign && self.callsignIndex) {
const csGroup = data.callsign_group || data.callsign;
if (csGroup) {
const existingIds = self.callsignIndex.get(csGroup);
if (existingIds) {
for (const eid of existingIds) {
if (String(eid) === String(data.id)) continue;
const eState = self.markerStates.get(eid);
const eMarker = self.markers.get(eid);
if (
eState &&
eMarker &&
eState.is_most_recent_for_callsign
) {
eState.historical = true;
eState.is_most_recent_for_callsign = false;
eMarker.setIcon(
self.createMarkerIcon({
id: String(eid),
lat: eState.lat,
lng: eState.lng,
callsign: eState.callsign || csGroup,
callsign_group: eState.callsign_group || csGroup,
symbol_table_id: eState.symbol_table,
symbol_code: eState.symbol_code,
historical: true,
is_most_recent_for_callsign: false,
popup: eState.popup,
}),
);
(eMarker as APRSMarker)._isHistorical = true;
}
}
}
}
}
// Create marker - use simple dot for older historical positions, APRS icon for most recent
const marker = L.marker([lat, lng], {
icon: self.createMarkerIcon(data),

View file

@ -49,6 +49,7 @@ defmodule AprsmeWeb.MapLive.DataBuilder do
build_packet_data(packet, is_most_recent, locale)
end)
|> Enum.filter(& &1)
|> deduplicate_by_callsign_group()
Process.delete(:weather_callsigns_cache)
result
@ -111,6 +112,7 @@ defmodule AprsmeWeb.MapLive.DataBuilder do
symbol_code = get_packet_field(packet, :symbol_code, ">")
callsign = display_name(packet)
label = map_label(packet)
# For historical (non-most-recent) packets, use a simple red dot HTML
# instead of the full APRS symbol — matches what the JS createMarkerIcon
@ -120,19 +122,16 @@ defmodule AprsmeWeb.MapLive.DataBuilder do
AprsmeWeb.SymbolRenderer.render_marker_symbol(
symbol_table_id,
symbol_code,
callsign,
label,
32
)
else
historical_dot_html(callsign)
historical_dot_html(label)
end
raw_comment = get_packet_field(packet, :comment, "")
clean_comment = Aprsme.EncodingUtils.sanitize_comment(raw_comment)
# Use map_label for display, but keep display_name for grouping
label = map_label(packet)
%{
"id" => if(is_most_recent, do: "current_#{get_packet_id(packet)}", else: "hist_#{get_packet_id(packet)}"),
"lat" => to_float(lat),
@ -229,7 +228,7 @@ defmodule AprsmeWeb.MapLive.DataBuilder do
@spec build_simple_popup(map(), boolean()) :: String.t()
def build_simple_popup(packet, has_weather) do
# Build popup HTML directly without database queries
callsign = display_name(packet)
callsign = map_label(packet)
timestamp_dt = get_packet_received_at(packet)
cache_buster = System.system_time(:millisecond)
@ -685,4 +684,15 @@ defmodule AprsmeWeb.MapLive.DataBuilder do
|> Enum.map(fn packet -> build_minimal_packet_data(packet, false, has_weather) end)
|> Enum.filter(& &1)
end
# When multiple senders track the same object, keep only one "current" marker
# per callsign_group to avoid duplicate labels on the map.
defp deduplicate_by_callsign_group(marker_list) do
marker_list
|> Enum.reduce(%{}, fn marker, acc ->
group = marker["callsign_group"] || marker["callsign"]
Map.put(acc, group, marker)
end)
|> Map.values()
end
end

View file

@ -849,51 +849,13 @@ defmodule AprsmeWeb.MapLive.Index do
end
def handle_info({:packet_batch, packets}, socket) do
# Process batch: collect marker data and removal IDs without pushing events
{socket, marker_data_list, removal_ids} =
Enum.reduce(packets, {socket, [], []}, fn packet, {acc_socket, markers, removals} ->
{new_socket, marker_data, removed_id} = process_packet_for_batch(packet, acc_socket)
markers = if marker_data, do: [marker_data | markers], else: markers
removals = if removed_id, do: [removed_id | removals], else: removals
{new_socket, markers, removals}
end)
{socket, marker_data_list, removal_ids} = process_packet_batch(packets, socket)
# Push a single batched remove event for all removals
socket =
if removal_ids == [] do
socket
else
DisplayManager.remove_markers_batch(socket, removal_ids)
end
socket = assign(socket, :last_update_at, DateTime.utc_now())
# Send heat map update once if in heat map mode, otherwise batch markers
socket =
cond do
socket.assigns.map_zoom <= 8 and marker_data_list != [] ->
DisplayManager.send_heat_map_for_current_bounds(socket)
marker_data_list != [] ->
reversed = Enum.reverse(marker_data_list)
# Extract convert_to_historical callsign keys from markers that replaced existing ones
convert_to_historical =
reversed
|> Enum.map(&Map.get(&1, "convert_from"))
|> Enum.filter(& &1)
|> Enum.uniq()
markers = prepare_markers_for_push(reversed, socket.assigns.station_popup_open)
push_event(socket, "new_packets", %{
packets: markers,
convert_to_historical: convert_to_historical
})
true ->
socket
end
socket
|> remove_markers_if_needed(removal_ids)
|> assign(:last_update_at, DateTime.utc_now())
|> update_display_for_batch(marker_data_list)
{:noreply, socket}
end
@ -1100,6 +1062,61 @@ defmodule AprsmeWeb.MapLive.Index do
end
end
defp process_packet_batch(packets, socket) do
Enum.reduce(packets, {socket, [], []}, fn packet, {acc_socket, markers, removals} ->
{new_socket, marker_data, removed_id} = process_packet_for_batch(packet, acc_socket)
markers = if marker_data, do: [marker_data | markers], else: markers
removals = if removed_id, do: [removed_id | removals], else: removals
{new_socket, markers, removals}
end)
end
defp remove_markers_if_needed(socket, []), do: socket
defp remove_markers_if_needed(socket, removal_ids) do
DisplayManager.remove_markers_batch(socket, removal_ids)
end
defp update_display_for_batch(socket, []), do: socket
defp update_display_for_batch(socket, marker_data_list) do
if socket.assigns.map_zoom <= 8 do
DisplayManager.send_heat_map_for_current_bounds(socket)
else
send_marker_batch(socket, marker_data_list)
end
end
defp send_marker_batch(socket, marker_data_list) do
reversed = Enum.reverse(marker_data_list)
# Deduplicate by callsign_group — when multiple senders track the same
# object (e.g. two stations reporting the same radiosonde), keep only
# the most recent packet per group to avoid duplicate "current" markers.
deduped =
reversed
|> Enum.reduce(%{}, fn marker, acc ->
group = marker["callsign_group"] || marker["callsign"]
Map.put(acc, group, marker)
end)
|> Map.values()
# Send callsign_groups so the frontend can look up existing markers
# via its callsignIndex (keyed by callsign_group, not by packet UUID)
convert_to_historical =
deduped
|> Enum.map(fn m -> m["callsign_group"] || m["callsign"] end)
|> Enum.filter(& &1)
|> Enum.uniq()
markers = prepare_markers_for_push(deduped, socket.assigns.station_popup_open)
push_event(socket, "new_packets", %{
packets: markers,
convert_to_historical: convert_to_historical
})
end
# Handle replaying the next historical packet
@impl true

View file

@ -143,6 +143,9 @@ defmodule AprsmeWeb.MapLive.DataBuilderTest do
assert result["callsign"] == "DB0SDA"
# But grouping uses object name
assert result["callsign_group"] == "P-K5SGD"
# Symbol HTML should show sender, not object name
assert result["symbol_html"] =~ "DB0SDA"
refute result["symbol_html"] =~ "P-K5SGD"
end
test "build_minimal_packet_data uses red dot HTML for historical (non-most-recent) packets" do
@ -343,5 +346,31 @@ defmodule AprsmeWeb.MapLive.DataBuilderTest do
groups = Enum.map(results, & &1["callsign_group"])
assert Enum.all?(groups, &(&1 == "P-K5SGD"))
end
test "build_simple_popup shows sender for object packets" do
packet = %{
id: Ecto.UUID.generate(),
sender: "WA0YMH-2",
base_callsign: "WA0YMH",
ssid: "2",
object_name: "X3234025",
is_object: true,
is_item: false,
lat: 33.251833,
lon: -95.719833,
data_type: "object",
received_at: DateTime.utc_now(),
symbol_table_id: "/",
symbol_code: "O",
comment: "Clb=5.9m/s t=-58.5C 404.800MHz Type=RS41-NG rdzTTGOsonde",
has_position: true,
path: "TCPIP*,qAR,WA0YMH-2"
}
popup = DataBuilder.build_simple_popup(packet, false)
assert popup =~ "WA0YMH-2"
refute popup =~ "X3234025"
end
end
end