tests pass

This commit is contained in:
Graham McIntire 2025-06-20 10:58:21 -05:00
parent e4d72ec6cc
commit bd3ec7ce19
No known key found for this signature in database
3 changed files with 189 additions and 91 deletions

View file

@ -194,7 +194,27 @@ defmodule AprsWeb.MapLive.CallsignView do
west: to_float(bounds["west"])
}
socket = assign(socket, map_bounds: normalized_bounds)
# Remove out-of-bounds visible packets
new_visible_packets =
socket.assigns.visible_packets
|> Enum.filter(fn {_k, packet} -> within_bounds?(packet, normalized_bounds) end)
|> Map.new()
markers_to_remove =
socket.assigns.visible_packets
|> Enum.reject(fn {_k, packet} -> within_bounds?(packet, normalized_bounds) end)
|> Enum.map(fn {k, _} -> k end)
socket =
if markers_to_remove == [] do
socket
else
Enum.reduce(markers_to_remove, socket, fn k, acc ->
push_event(acc, "remove_marker", %{id: k})
end)
end
socket = assign(socket, map_bounds: normalized_bounds, visible_packets: new_visible_packets)
{:noreply, socket}
end
@ -241,77 +261,58 @@ defmodule AprsWeb.MapLive.CallsignView do
# Clean up packets older than 1 hour from the map display
handle_cleanup_old_packets(socket)
%{event: "packet", payload: payload} ->
# Sanitize the packet to prevent encoding errors
# Stream packets from PubSub only if within bounds
%Phoenix.Socket.Broadcast{topic: "aprs_messages", event: "packet", payload: payload} ->
sanitized_packet = EncodingUtils.sanitize_packet(payload)
# Add received timestamp if not present
sanitized_packet = Map.put_new(sanitized_packet, :received_at, DateTime.utc_now())
{lat, lng} = get_coordinates(sanitized_packet)
# Check if this packet is from our target callsign
if packet_matches_callsign?(sanitized_packet, socket.assigns.callsign) and
has_position_data?(sanitized_packet) and
packet_within_time_threshold?(sanitized_packet, socket.assigns.packet_age_threshold) do
# Convert to a simple map structure for JSON encoding
packet_data = build_packet_data(sanitized_packet)
callsign_key =
"#{sanitized_packet.base_callsign}#{if sanitized_packet.ssid, do: "-#{sanitized_packet.ssid}", else: ""}"
# Only push if we have valid packet data
if packet_data do
# Generate a unique key for this packet
callsign_key =
"#{sanitized_packet.base_callsign}#{if sanitized_packet.ssid, do: "-#{sanitized_packet.ssid}", else: ""}"
if has_position_data?(sanitized_packet) and
Map.has_key?(socket.assigns.visible_packets, callsign_key) and
not within_bounds?(%{lat: lat, lon: lng}, socket.assigns.map_bounds) do
socket = push_event(socket, "remove_marker", %{id: callsign_key})
new_visible_packets = Map.delete(socket.assigns.visible_packets, callsign_key)
{:noreply, assign(socket, visible_packets: new_visible_packets)}
else
if has_position_data?(sanitized_packet) and
within_bounds?(%{lat: lat, lon: lng}, socket.assigns.map_bounds) and
packet_matches_callsign?(sanitized_packet, socket.assigns.callsign) and
packet_within_time_threshold?(
sanitized_packet,
socket.assigns.packet_age_threshold
) do
packet_data = build_packet_data(sanitized_packet)
# Update visible packets tracking
visible_packets =
Map.put(socket.assigns.visible_packets, callsign_key, sanitized_packet)
if packet_data do
visible_packets =
Map.put(socket.assigns.visible_packets, callsign_key, sanitized_packet)
# Update last known position
{lat, lng} = get_coordinates(sanitized_packet)
last_known_position =
if lat && lng, do: %{lat: lat, lng: lng}, else: socket.assigns.last_known_position
last_known_position =
if lat && lng, do: %{lat: lat, lng: lng}, else: socket.assigns.last_known_position
socket =
socket
|> push_event("new_packet", packet_data)
|> assign(
visible_packets: visible_packets,
last_known_position: last_known_position
)
# Push the packet to the client-side JavaScript
socket =
socket
|> push_event("new_packet", packet_data)
|> assign(
visible_packets: visible_packets,
last_known_position: last_known_position
)
# If this is the first packet and map is ready, zoom to it
# Or if this is a new position that's significantly different, update zoom
socket =
cond do
map_empty?(socket) and socket.assigns.map_ready and last_known_position ->
push_event(socket, "zoom_to_location", %{
lat: last_known_position.lat,
lng: last_known_position.lng,
zoom: 12
})
should_update_zoom?(socket.assigns.last_known_position, last_known_position) and
socket.assigns.map_ready ->
push_event(socket, "zoom_to_location", %{
lat: last_known_position.lat,
lng: last_known_position.lng,
zoom: 12
})
true ->
socket
end
{:noreply, socket}
{:noreply, socket}
else
{:noreply, socket}
end
else
# Invalid packet data, skip it
{:noreply, socket}
end
else
# Ignore packets that don't match our callsign or don't have position data
{:noreply, socket}
end
_ ->
# Ignore packets that don't match our callsign or don't have position data
{:noreply, socket}
end
end
@ -687,16 +688,26 @@ defmodule AprsWeb.MapLive.CallsignView do
socket = assign(socket, packet_age_threshold: one_hour_ago)
# Remove expired packets from visible_packets
updated_visible_packets =
expired_keys =
socket.assigns.visible_packets
|> Enum.filter(fn {_key, packet} ->
packet_within_time_threshold?(packet, one_hour_ago)
not packet_within_time_threshold?(packet, one_hour_ago)
end)
|> Map.new()
|> Enum.map(fn {key, _} -> key end)
socket = assign(socket, visible_packets: updated_visible_packets)
# Only update the client if there are expired markers
socket =
if expired_keys == [] do
socket
else
Enum.reduce(expired_keys, socket, fn key, acc ->
push_event(acc, "remove_marker", %{id: key})
end)
end
{:noreply, socket}
# Use Map.drop/2 for better performance
updated_visible_packets = Map.drop(socket.assigns.visible_packets, expired_keys)
assign(socket, visible_packets: updated_visible_packets)
end
defp packet_within_time_threshold?(packet, threshold) do
@ -987,4 +998,43 @@ defmodule AprsWeb.MapLive.CallsignView do
# Update zoom if position changed by more than ~5km (approximately 0.05 degrees)
lat_diff > 0.05 or lng_diff > 0.05
end
# Helper to check if a packet or lat/lon is within bounds
defp within_bounds?(packet_or_coords, bounds) do
{lat, lon} =
cond do
is_map(packet_or_coords) and Map.has_key?(packet_or_coords, :lat) and
Map.has_key?(packet_or_coords, :lon) ->
{packet_or_coords.lat, packet_or_coords.lon}
is_map(packet_or_coords) and Map.has_key?(packet_or_coords, "lat") and
Map.has_key?(packet_or_coords, "lon") ->
{packet_or_coords["lat"], packet_or_coords["lon"]}
is_map(packet_or_coords) and Map.has_key?(packet_or_coords, :latitude) and
Map.has_key?(packet_or_coords, :longitude) ->
{packet_or_coords.latitude, packet_or_coords.longitude}
is_tuple(packet_or_coords) and tuple_size(packet_or_coords) == 2 ->
packet_or_coords
true ->
{nil, nil}
end
if is_nil(lat) or is_nil(lon) do
false
else
lat_in_bounds = lat >= bounds.south && lat <= bounds.north
lng_in_bounds =
if bounds.west <= bounds.east do
lon >= bounds.west && lon <= bounds.east
else
lon >= bounds.west || lon <= bounds.east
end
lat_in_bounds && lng_in_bounds
end
end
end

View file

@ -72,11 +72,32 @@ defmodule AprsWeb.MapLive.Enhanced do
lng: center["lng"]
}
# Remove out-of-bounds active markers
new_active_markers =
socket.assigns.active_markers
|> Enum.filter(fn {_k, %{packet: packet}} -> within_bounds?(packet, bounds) end)
|> Map.new()
markers_to_remove =
socket.assigns.active_markers
|> Enum.reject(fn {_k, %{packet: packet}} -> within_bounds?(packet, bounds) end)
|> Enum.map(fn {k, _} -> k end)
socket =
if markers_to_remove == [] do
socket
else
Enum.reduce(markers_to_remove, socket, fn k, acc ->
push_event(acc, "remove_marker", %{id: k})
end)
end
socket =
socket
|> assign(:map_bounds, bounds)
|> assign(:map_center, normalized_center)
|> assign(:map_zoom, zoom)
|> assign(:active_markers, new_active_markers)
|> load_markers_in_bounds()
{:noreply, socket}
@ -166,12 +187,21 @@ defmodule AprsWeb.MapLive.Enhanced do
end
def handle_info({:new_packet, packet}, socket) do
# Handle real-time packet updates
if has_position_data?(packet) and within_bounds?(packet, socket.assigns.map_bounds) do
socket = add_packet_marker(socket, packet, false)
{:noreply, socket}
id = packet_to_marker_data(packet).id
if has_position_data?(packet) and Map.has_key?(socket.assigns.active_markers, id) and
not within_bounds?(packet, socket.assigns.map_bounds) do
socket = push_event(socket, "remove_marker", %{id: id})
new_active_markers = Map.delete(socket.assigns.active_markers, id)
{:noreply, assign(socket, :active_markers, new_active_markers)}
else
{:noreply, socket}
# Only add marker if it is within bounds
if has_position_data?(packet) and within_bounds?(packet, socket.assigns.map_bounds) do
socket = add_packet_marker(socket, packet, false)
{:noreply, socket}
else
{:noreply, socket}
end
end
end

View file

@ -305,10 +305,15 @@ defmodule AprsWeb.MapLive.Index do
|> Enum.reject(fn {_k, packet} -> within_bounds?(packet, map_bounds) end)
|> Enum.map(fn {k, _} -> k end)
# Remove markers for out-of-bounds packets
socket =
Enum.reduce(packets_to_remove, socket, fn k, acc ->
push_event(acc, "remove_marker", %{id: k})
end)
if packets_to_remove == [] do
socket
else
Enum.reduce(packets_to_remove, socket, fn k, acc ->
push_event(acc, "remove_marker", %{id: k})
end)
end
# Fetch the latest packets within the new bounds and push to client (historical only)
bounds_list = [map_bounds.west, map_bounds.south, map_bounds.east, map_bounds.north]
@ -406,7 +411,6 @@ defmodule AprsWeb.MapLive.Index do
defp handle_info_postgres_packet(packet, socket) do
{lat, lon, _data_extended} = get_coordinates(packet)
# Always store the packet in all_packets
callsign_key =
if Map.has_key?(packet, "id"),
do: to_string(packet["id"]),
@ -415,10 +419,25 @@ defmodule AprsWeb.MapLive.Index do
all_packets = Map.put(socket.assigns.all_packets, callsign_key, packet)
socket = assign(socket, all_packets: all_packets)
# Only add marker if it is within bounds and not already present
if is_nil(lat) or is_nil(lon) or Map.has_key?(socket.assigns.visible_packets, callsign_key),
do: {:noreply, socket},
else: handle_valid_postgres_packet(packet, lat, lon, socket)
# Remove marker if packet is out of bounds but present
if !is_nil(lat) and !is_nil(lon) and
Map.has_key?(socket.assigns.visible_packets, callsign_key) and
not within_bounds?(%{lat: lat, lon: lon}, socket.assigns.map_bounds) do
socket = push_event(socket, "remove_marker", %{id: callsign_key})
new_visible_packets = Map.delete(socket.assigns.visible_packets, callsign_key)
{:noreply, assign(socket, visible_packets: new_visible_packets)}
# Only add marker if it is within bounds and not already present
else
if is_nil(lat) or is_nil(lon) or Map.has_key?(socket.assigns.visible_packets, callsign_key) do
{:noreply, socket}
else
if within_bounds?(%{lat: lat, lon: lon}, socket.assigns.map_bounds) do
handle_valid_postgres_packet(packet, lat, lon, socket)
else
{:noreply, socket}
end
end
end
end
defp handle_valid_postgres_packet(packet, lat, lon, socket) do
@ -698,26 +717,25 @@ defmodule AprsWeb.MapLive.Index do
one_hour_ago = DateTime.add(DateTime.utc_now(), -3600, :second)
# Remove expired packets from visible_packets
updated_visible_packets =
socket.assigns.visible_packets
|> Enum.filter(fn {_key, packet} ->
packet_within_time_threshold?(packet, one_hour_ago)
end)
|> Map.new()
# Remove expired markers from the client
expired_keys =
socket.assigns.visible_packets
|> Enum.reject(fn {_key, packet} ->
packet_within_time_threshold?(packet, one_hour_ago)
|> Enum.filter(fn {_key, packet} ->
not packet_within_time_threshold?(packet, one_hour_ago)
end)
|> Enum.map(fn {key, _} -> key end)
# Only update the client if there are expired markers
socket =
Enum.reduce(expired_keys, socket, fn key, acc ->
push_event(acc, "remove_marker", %{id: key})
end)
if expired_keys == [] do
socket
else
Enum.reduce(expired_keys, socket, fn key, acc ->
push_event(acc, "remove_marker", %{id: key})
end)
end
# Use Map.drop/2 for better performance
updated_visible_packets = Map.drop(socket.assigns.visible_packets, expired_keys)
assign(socket, visible_packets: updated_visible_packets)
end