updates
This commit is contained in:
parent
c4bf50284b
commit
f5049a6e2d
4 changed files with 145 additions and 49 deletions
|
|
@ -1748,6 +1748,46 @@ let MapAPRSMap = {
|
||||||
// Remove existing marker if it exists (without affecting trails since we're just replacing it)
|
// Remove existing marker if it exists (without affecting trails since we're just replacing it)
|
||||||
self.removeMarkerWithoutTrail(data.id);
|
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
|
// 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], {
|
||||||
icon: self.createMarkerIcon(data),
|
icon: self.createMarkerIcon(data),
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ defmodule AprsmeWeb.MapLive.DataBuilder do
|
||||||
build_packet_data(packet, is_most_recent, locale)
|
build_packet_data(packet, is_most_recent, locale)
|
||||||
end)
|
end)
|
||||||
|> Enum.filter(& &1)
|
|> Enum.filter(& &1)
|
||||||
|
|> deduplicate_by_callsign_group()
|
||||||
|
|
||||||
Process.delete(:weather_callsigns_cache)
|
Process.delete(:weather_callsigns_cache)
|
||||||
result
|
result
|
||||||
|
|
@ -111,6 +112,7 @@ defmodule AprsmeWeb.MapLive.DataBuilder do
|
||||||
symbol_code = get_packet_field(packet, :symbol_code, ">")
|
symbol_code = get_packet_field(packet, :symbol_code, ">")
|
||||||
|
|
||||||
callsign = display_name(packet)
|
callsign = display_name(packet)
|
||||||
|
label = map_label(packet)
|
||||||
|
|
||||||
# For historical (non-most-recent) packets, use a simple red dot HTML
|
# For historical (non-most-recent) packets, use a simple red dot HTML
|
||||||
# instead of the full APRS symbol — matches what the JS createMarkerIcon
|
# 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(
|
AprsmeWeb.SymbolRenderer.render_marker_symbol(
|
||||||
symbol_table_id,
|
symbol_table_id,
|
||||||
symbol_code,
|
symbol_code,
|
||||||
callsign,
|
label,
|
||||||
32
|
32
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
historical_dot_html(callsign)
|
historical_dot_html(label)
|
||||||
end
|
end
|
||||||
|
|
||||||
raw_comment = get_packet_field(packet, :comment, "")
|
raw_comment = get_packet_field(packet, :comment, "")
|
||||||
clean_comment = Aprsme.EncodingUtils.sanitize_comment(raw_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)}"),
|
"id" => if(is_most_recent, do: "current_#{get_packet_id(packet)}", else: "hist_#{get_packet_id(packet)}"),
|
||||||
"lat" => to_float(lat),
|
"lat" => to_float(lat),
|
||||||
|
|
@ -229,7 +228,7 @@ defmodule AprsmeWeb.MapLive.DataBuilder do
|
||||||
@spec build_simple_popup(map(), boolean()) :: String.t()
|
@spec build_simple_popup(map(), boolean()) :: String.t()
|
||||||
def build_simple_popup(packet, has_weather) do
|
def build_simple_popup(packet, has_weather) do
|
||||||
# Build popup HTML directly without database queries
|
# Build popup HTML directly without database queries
|
||||||
callsign = display_name(packet)
|
callsign = map_label(packet)
|
||||||
timestamp_dt = get_packet_received_at(packet)
|
timestamp_dt = get_packet_received_at(packet)
|
||||||
cache_buster = System.system_time(:millisecond)
|
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.map(fn packet -> build_minimal_packet_data(packet, false, has_weather) end)
|
||||||
|> Enum.filter(& &1)
|
|> Enum.filter(& &1)
|
||||||
end
|
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
|
end
|
||||||
|
|
|
||||||
|
|
@ -849,51 +849,13 @@ defmodule AprsmeWeb.MapLive.Index do
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_info({:packet_batch, packets}, socket) do
|
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} = process_packet_batch(packets, socket)
|
||||||
{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)
|
|
||||||
|
|
||||||
# Push a single batched remove event for all removals
|
|
||||||
socket =
|
socket =
|
||||||
if removal_ids == [] do
|
socket
|
||||||
socket
|
|> remove_markers_if_needed(removal_ids)
|
||||||
else
|
|> assign(:last_update_at, DateTime.utc_now())
|
||||||
DisplayManager.remove_markers_batch(socket, removal_ids)
|
|> update_display_for_batch(marker_data_list)
|
||||||
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
|
|
||||||
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
@ -1100,6 +1062,61 @@ defmodule AprsmeWeb.MapLive.Index do
|
||||||
end
|
end
|
||||||
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
|
# Handle replaying the next historical packet
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,9 @@ defmodule AprsmeWeb.MapLive.DataBuilderTest do
|
||||||
assert result["callsign"] == "DB0SDA"
|
assert result["callsign"] == "DB0SDA"
|
||||||
# But grouping uses object name
|
# But grouping uses object name
|
||||||
assert result["callsign_group"] == "P-K5SGD"
|
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
|
end
|
||||||
|
|
||||||
test "build_minimal_packet_data uses red dot HTML for historical (non-most-recent) packets" do
|
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"])
|
groups = Enum.map(results, & &1["callsign_group"])
|
||||||
assert Enum.all?(groups, &(&1 == "P-K5SGD"))
|
assert Enum.all?(groups, &(&1 == "P-K5SGD"))
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue