This commit is contained in:
Graham McIntire 2025-07-03 10:48:44 -05:00
parent 7ef3f59026
commit 7f06d3200a
No known key found for this signature in database
4 changed files with 120 additions and 164 deletions

View file

@ -160,18 +160,20 @@ defmodule Aprsme.DeviceIdentification do
defp upsert_device_group(group, now) do
Enum.each(group, fn {identifier, attrs} ->
attrs =
attrs
|> Map.put("identifier", identifier)
|> Map.update("features", nil, fn f ->
if is_list(f), do: f, else: [f]
end)
|> Map.put("updated_at", now)
%Devices{} |> Devices.changeset(attrs) |> Repo.insert!()
processed_attrs = process_device_attrs(attrs, identifier, now)
%Devices{} |> Devices.changeset(processed_attrs) |> Repo.insert!()
end)
end
defp process_device_attrs(attrs, identifier, now) do
attrs
|> Map.put("identifier", identifier)
|> Map.update("features", nil, fn f ->
if is_list(f), do: f, else: [f]
end)
|> Map.put("updated_at", now)
end
# Helper to enqueue the job
def enqueue_refresh_job do
# Oban.insert!(Worker.new(%{}))
@ -214,7 +216,8 @@ defmodule Aprsme.DeviceIdentification do
# Converts a pattern with ? wildcards to a regex
defp wildcard_pattern_to_regex(pattern) when is_binary(pattern) do
# Replace ? with a placeholder, escape all regex metacharacters except the placeholder, then replace placeholder with .
# Replace ? with a placeholder, escape all regex metacharacters except the placeholder,
# then replace placeholder with .
pattern
|> String.replace("?", "__WILDCARD__")
# Escape all regex metacharacters

View file

@ -302,70 +302,6 @@ defmodule AprsmeWeb.MapLive.Index do
{:noreply, socket}
end
@spec handle_bounds_update(map(), Socket.t()) :: {:noreply, Socket.t()}
defp handle_bounds_update(bounds, socket) do
# Update the map bounds from the client, converting to atom keys
map_bounds = %{
north: bounds["north"],
south: bounds["south"],
east: bounds["east"],
west: bounds["west"]
}
# Validate bounds to prevent invalid coordinates
if map_bounds.north > 90 or map_bounds.south < -90 or
map_bounds.north <= map_bounds.south do
# Invalid bounds, skip update
{:noreply, socket}
else
# Only schedule a bounds update if the bounds have actually changed (with rounding)
if compare_bounds(map_bounds, socket.assigns.map_bounds) do
{:noreply, socket}
else
if socket.assigns[:bounds_update_timer] do
Process.cancel_timer(socket.assigns.bounds_update_timer)
end
timer_ref = Process.send_after(self(), {:process_bounds_update, map_bounds}, 250)
socket = assign(socket, bounds_update_timer: timer_ref, pending_bounds: map_bounds)
{:noreply, socket}
end
end
end
@spec process_bounds_update(map(), Socket.t()) :: Socket.t()
defp process_bounds_update(map_bounds, socket) do
# Remove out-of-bounds packets and markers immediately
new_visible_packets =
socket.assigns.visible_packets
|> Enum.filter(fn {_k, packet} -> within_bounds?(packet, map_bounds) end)
|> Map.new()
packets_to_remove =
socket.assigns.visible_packets
|> 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 =
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
# Remove only out-of-bounds historical packets instead of clearing all
socket = push_event(socket, "filter_markers_by_bounds", %{bounds: map_bounds})
# Load additional historical packets for the new bounds if needed
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)
end
@impl true
def handle_info({:process_bounds_update, map_bounds}, socket), do: handle_info_process_bounds_update(map_bounds, socket)
@ -1015,29 +951,11 @@ defmodule AprsmeWeb.MapLive.Index do
end
# Check if a packet is within the time threshold (not too old)
@spec packet_within_time_threshold?(map(), DateTime.t()) :: boolean()
@spec packet_within_time_threshold?(struct(), any()) :: boolean()
defp packet_within_time_threshold?(packet, threshold) do
case packet do
%{received_at: received_at} when not is_nil(received_at) ->
threshold_dt =
cond do
is_integer(threshold) ->
# Assume seconds since epoch
DateTime.from_unix!(threshold)
is_binary(threshold) ->
case DateTime.from_iso8601(threshold) do
{:ok, dt, _} -> dt
_ -> DateTime.utc_now()
end
match?(%DateTime{}, threshold) ->
threshold
true ->
DateTime.utc_now()
end
threshold_dt = convert_threshold_to_datetime(threshold)
DateTime.compare(received_at, threshold_dt) in [:gt, :eq]
_ ->
@ -1046,48 +964,33 @@ defmodule AprsmeWeb.MapLive.Index do
end
end
defp convert_threshold_to_datetime(threshold) do
cond do
is_integer(threshold) ->
# Assume seconds since epoch
DateTime.from_unix!(threshold)
is_binary(threshold) ->
case DateTime.from_iso8601(threshold) do
{:ok, dt, _} -> dt
_ -> DateTime.utc_now()
end
match?(%DateTime{}, threshold) ->
threshold
true ->
DateTime.utc_now()
end
end
# Helper functions
# Fetch historical packets from the database
defp process_historical_packets(socket, historical_packets) do
socket = push_event(socket, "clear_historical_packets", %{})
packet_data_list =
historical_packets
|> Enum.group_by(&PacketUtils.generate_callsign/1)
|> Enum.flat_map(fn {callsign, packets} ->
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}
)
unique_position_packets = filter_unique_positions(sorted_packets)
unique_position_packets
|> Enum.with_index()
|> Enum.map(fn {packet, index} ->
# The first packet (index 0) is the most recent for this callsign
# Only show as red dot if it's not the most recent position
is_most_recent = index == 0
packet_data = PacketUtils.build_packet_data(packet, is_most_recent)
if packet_data do
packet_data
|> Map.put(:callsign, callsign)
|> Map.put(:historical, true)
end
end)
|> Enum.filter(& &1)
end)
packet_data_list = build_packet_data_list(historical_packets)
if Enum.any?(packet_data_list) do
push_event(socket, "add_historical_packets", %{packets: packet_data_list})
@ -1096,6 +999,50 @@ defmodule AprsmeWeb.MapLive.Index do
end
end
defp build_packet_data_list(historical_packets) do
historical_packets
|> Enum.group_by(&PacketUtils.generate_callsign/1)
|> Enum.flat_map(&process_callsign_packets/1)
end
defp process_callsign_packets({callsign, packets}) do
sorted_packets = sort_packets_by_inserted_at(packets)
unique_position_packets = filter_unique_positions(sorted_packets)
unique_position_packets
|> Enum.with_index()
|> Enum.map(&build_packet_data_with_index(&1, callsign))
|> Enum.filter(& &1)
end
defp sort_packets_by_inserted_at(packets) do
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}
)
end
defp build_packet_data_with_index({packet, index}, callsign) do
# The first packet (index 0) is the most recent for this callsign
# Only show as red dot if it's not the most recent position
is_most_recent = index == 0
packet_data = PacketUtils.build_packet_data(packet, is_most_recent)
if packet_data do
packet_data
|> Map.put(:callsign, callsign)
|> Map.put(:historical, true)
end
end
defp filter_unique_positions(packets) do
packets
|> Enum.reduce([], fn packet, acc ->

View file

@ -66,26 +66,33 @@ defmodule AprsmeWeb.MapLive.MapHelpers do
if is_nil(lat) or is_nil(lon) do
false
else
lat = to_float(lat)
lon = to_float(lon)
south = to_float(bounds.south)
north = to_float(bounds.north)
west = to_float(bounds.west)
east = to_float(bounds.east)
lat_in_bounds = lat >= south && lat <= north
lng_in_bounds =
if west <= east do
lon >= west && lon <= east
else
lon >= west || lon <= east
end
lat_in_bounds && lng_in_bounds
check_bounds(lat, lon, bounds)
end
end
end
defp check_bounds(lat, lon, bounds) do
lat = to_float(lat)
lon = to_float(lon)
south = to_float(bounds.south)
north = to_float(bounds.north)
west = to_float(bounds.west)
east = to_float(bounds.east)
lat_in_bounds = lat >= south && lat <= north
lng_in_bounds = check_longitude_bounds(lon, west, east)
lat_in_bounds && lng_in_bounds
end
defp check_longitude_bounds(lon, west, east) do
if west <= east do
lon >= west && lon <= east
else
lon >= west || lon <= east
end
end
defp extract_lat_lon(%{lat: lat, lon: lon}), do: extract_lat_lon_atom(%{lat: lat, lon: lon})
defp extract_lat_lon(%{"lat" => lat, "lon" => lon}), do: extract_lat_lon_string(%{"lat" => lat, "lon" => lon})

View file

@ -149,28 +149,27 @@ defmodule AprsmeWeb.PacketsLive.CallsignView do
defp get_all_packets_list(stored, live) do
# Combine and sort by received_at timestamp (newest first)
(live ++ stored)
|> Enum.sort_by(
fn packet ->
case packet.received_at do
%DateTime{} = dt ->
DateTime.to_unix(dt, :microsecond)
dt when is_binary(dt) ->
case DateTime.from_iso8601(dt) do
{:ok, parsed_dt, _} -> DateTime.to_unix(parsed_dt, :microsecond)
_ -> 0
end
_ ->
0
end
end,
:desc
)
|> Enum.sort_by(&get_timestamp_microseconds/1, :desc)
# Ensure we never exceed 100 total
|> Enum.take(100)
end
defp get_timestamp_microseconds(packet) do
case packet.received_at do
%DateTime{} = dt ->
DateTime.to_unix(dt, :microsecond)
dt when is_binary(dt) ->
case DateTime.from_iso8601(dt) do
{:ok, parsed_dt, _} -> DateTime.to_unix(parsed_dt, :microsecond)
_ -> 0
end
_ ->
0
end
end
# Helper to update stored and live packet lists, keeping total <= 100
defp update_packet_lists(current_stored, current_live, sanitized_payload) do
total_count = length(current_live) + length(current_stored)