DRY
This commit is contained in:
parent
ecee5fd677
commit
e68c0aca41
4 changed files with 361 additions and 187 deletions
|
|
@ -4,7 +4,7 @@ defmodule AprsWeb.MapLive.CallsignView do
|
|||
alias Aprs.EncodingUtils
|
||||
alias Aprs.Packets
|
||||
alias AprsWeb.Endpoint
|
||||
alias Parser.Types.MicE
|
||||
alias AprsWeb.MapLive.MapHelpers
|
||||
|
||||
@default_center %{lat: 39.0, lng: -98.0}
|
||||
@default_zoom 4
|
||||
|
|
@ -185,6 +185,10 @@ defmodule AprsWeb.MapLive.CallsignView do
|
|||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("marker_clicked", %{"id" => _id, "callsign" => _callsign, "lat" => _lat, "lng" => _lng}, socket) do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
defp handle_bounds_update(bounds, socket) do
|
||||
# Convert string keys to atom keys and parse values
|
||||
normalized_bounds = %{
|
||||
|
|
@ -197,12 +201,12 @@ defmodule AprsWeb.MapLive.CallsignView do
|
|||
# Remove out-of-bounds visible packets
|
||||
new_visible_packets =
|
||||
socket.assigns.visible_packets
|
||||
|> Enum.filter(fn {_k, packet} -> within_bounds?(packet, normalized_bounds) end)
|
||||
|> Enum.filter(fn {_k, packet} -> MapHelpers.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.reject(fn {_k, packet} -> MapHelpers.within_bounds?(packet, normalized_bounds) end)
|
||||
|> Enum.map(fn {k, _} -> k end)
|
||||
|
||||
socket =
|
||||
|
|
@ -265,14 +269,14 @@ defmodule AprsWeb.MapLive.CallsignView do
|
|||
%Phoenix.Socket.Broadcast{topic: "aprs_messages", event: "packet", payload: payload} ->
|
||||
sanitized_packet = EncodingUtils.sanitize_packet(payload)
|
||||
sanitized_packet = Map.put_new(sanitized_packet, :received_at, DateTime.utc_now())
|
||||
{lat, lng} = get_coordinates(sanitized_packet)
|
||||
{lat, lng, _} = MapHelpers.get_coordinates(sanitized_packet)
|
||||
|
||||
callsign_key =
|
||||
"#{sanitized_packet.base_callsign}#{if sanitized_packet.ssid, do: "-#{sanitized_packet.ssid}", else: ""}"
|
||||
|
||||
if has_position_data?(sanitized_packet) and
|
||||
if MapHelpers.has_position_data?(sanitized_packet) and
|
||||
packet_matches_callsign?(sanitized_packet, socket.assigns.callsign) and
|
||||
within_bounds?(%{lat: lat, lon: lng}, socket.assigns.map_bounds) and
|
||||
MapHelpers.within_bounds?(%{lat: lat, lon: lng}, socket.assigns.map_bounds) and
|
||||
packet_within_time_threshold?(
|
||||
sanitized_packet,
|
||||
socket.assigns.packet_age_threshold
|
||||
|
|
@ -365,7 +369,7 @@ defmodule AprsWeb.MapLive.CallsignView do
|
|||
end
|
||||
|
||||
defp handle_valid_replay_packet(packet, packet_data, socket) do
|
||||
historical_packets = Map.put(socket.assigns.historical_packets, packet_data.id, packet)
|
||||
historical_packets = Map.put(socket.assigns.historical_packets, packet_data["id"], packet)
|
||||
socket = push_event(socket, "historical_packet", Map.put(packet_data, :historical, true))
|
||||
delay = trunc(1000 / socket.assigns.replay_speed)
|
||||
timer_ref = Process.send_after(self(), :replay_next_packet, delay)
|
||||
|
|
@ -737,119 +741,98 @@ defmodule AprsWeb.MapLive.CallsignView do
|
|||
})
|
||||
end
|
||||
|
||||
defp has_position_data?(packet) do
|
||||
case packet.data_extended do
|
||||
%MicE{} ->
|
||||
true
|
||||
|
||||
%{latitude: lat, longitude: lon} when not is_nil(lat) and not is_nil(lon) ->
|
||||
true
|
||||
|
||||
_ ->
|
||||
lat = Map.get(packet, :lat) || Map.get(packet, "lat")
|
||||
lon = Map.get(packet, :lon) || Map.get(packet, "lon")
|
||||
not is_nil(lat) and not is_nil(lon)
|
||||
end
|
||||
end
|
||||
|
||||
defp get_coordinates(%{data_extended: %MicE{} = mic_e}), do: get_coordinates_from_mic_e(mic_e)
|
||||
defp get_coordinates(%{data_extended: %{latitude: lat, longitude: lon}}), do: {lat, lon}
|
||||
defp get_coordinates(_), do: {nil, nil}
|
||||
|
||||
defp get_coordinates_from_mic_e(mic_e) do
|
||||
lat = mic_e.lat_degrees + mic_e.lat_minutes / 60.0 + mic_e.lat_fractional / 6000.0
|
||||
lat = if mic_e.lat_direction == :south, do: -lat, else: lat
|
||||
lng = mic_e.lon_degrees + mic_e.lon_minutes / 60.0 + mic_e.lon_fractional / 6000.0
|
||||
lng = if mic_e.lon_direction == :west, do: -lng, else: lng
|
||||
if lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180, do: {lat, lng}, else: {nil, nil}
|
||||
end
|
||||
|
||||
defp build_packet_data(packet) do
|
||||
{lat, lng} = get_coordinates(packet)
|
||||
{lat, lng, _} = MapHelpers.get_coordinates(packet)
|
||||
callsign = Map.get(packet, :base_callsign, Map.get(packet, "base_callsign", ""))
|
||||
|
||||
if lat && lng do
|
||||
callsign = "#{packet.base_callsign}#{if packet.ssid, do: "-#{packet.ssid}", else: ""}"
|
||||
|
||||
symbol_table_id = get_symbol_table_id(packet.data_extended)
|
||||
symbol_code = get_symbol_code(packet.data_extended)
|
||||
comment = get_comment(packet.data_extended)
|
||||
|
||||
%{
|
||||
id: "#{callsign}_#{:os.system_time(:millisecond)}",
|
||||
callsign: callsign,
|
||||
lat: lat,
|
||||
lng: lng,
|
||||
symbol_table_id: symbol_table_id,
|
||||
symbol_code: symbol_code,
|
||||
comment: comment,
|
||||
received_at: packet.received_at || DateTime.utc_now(),
|
||||
data_extended: build_data_extended(packet.data_extended)
|
||||
}
|
||||
if lat != nil and lng != nil and callsign != "" and callsign != nil do
|
||||
build_packet_map(packet, lat, lng, packet.data_extended)
|
||||
end
|
||||
end
|
||||
|
||||
defp get_symbol_table_id(data) do
|
||||
cond do
|
||||
is_map(data) && (Map.get(data, :symbol_table_id) || Map.get(data, "symbol_table_id")) ->
|
||||
Map.get(data, :symbol_table_id) || Map.get(data, "symbol_table_id")
|
||||
defp build_packet_map(packet, lat, lng, data_extended) do
|
||||
data_extended = data_extended || %{}
|
||||
|
||||
is_map(data) && Map.has_key?(data, :packet) &&
|
||||
(Map.get(data.packet, :symbol_table_id) || Map.get(data.packet, "symbol_table_id")) ->
|
||||
Map.get(data.packet, :symbol_table_id) || Map.get(data.packet, "symbol_table_id")
|
||||
callsign =
|
||||
case {Map.get(packet, :base_callsign), Map.get(packet, :ssid)} do
|
||||
{base, ssid} when is_binary(base) and ssid not in [nil, "", "0"] ->
|
||||
"#{base}-#{ssid}"
|
||||
|
||||
is_map(data) && Map.has_key?(data, "packet") &&
|
||||
(Map.get(data["packet"], :symbol_table_id) || Map.get(data["packet"], "symbol_table_id")) ->
|
||||
Map.get(data["packet"], :symbol_table_id) || Map.get(data["packet"], "symbol_table_id")
|
||||
{base, _} ->
|
||||
base || ""
|
||||
end
|
||||
|
||||
true ->
|
||||
"/"
|
||||
end
|
||||
end
|
||||
symbol_table_id =
|
||||
Map.get(data_extended, :symbol_table_id) || Map.get(data_extended, "symbol_table_id") || "/"
|
||||
|
||||
defp get_symbol_code(data) do
|
||||
cond do
|
||||
is_map(data) && (Map.get(data, :symbol_code) || Map.get(data, "symbol_code")) ->
|
||||
Map.get(data, :symbol_code) || Map.get(data, "symbol_code")
|
||||
symbol_code =
|
||||
Map.get(data_extended, :symbol_code) || Map.get(data_extended, "symbol_code") || ">"
|
||||
|
||||
is_map(data) && Map.has_key?(data, :packet) &&
|
||||
(Map.get(data.packet, :symbol_code) || Map.get(data.packet, "symbol_code")) ->
|
||||
Map.get(data.packet, :symbol_code) || Map.get(data.packet, "symbol_code")
|
||||
symbol_description =
|
||||
Map.get(data_extended, :symbol_description) || Map.get(data_extended, "symbol_description") ||
|
||||
"Symbol: #{symbol_table_id}#{symbol_code}"
|
||||
|
||||
is_map(data) && Map.has_key?(data, "packet") &&
|
||||
(Map.get(data["packet"], :symbol_code) || Map.get(data["packet"], "symbol_code")) ->
|
||||
Map.get(data["packet"], :symbol_code) || Map.get(data["packet"], "symbol_code")
|
||||
timestamp =
|
||||
cond do
|
||||
Map.has_key?(packet, :received_at) && packet.received_at ->
|
||||
DateTime.to_iso8601(packet.received_at)
|
||||
|
||||
true ->
|
||||
">"
|
||||
end
|
||||
end
|
||||
Map.has_key?(packet, "received_at") && packet["received_at"] ->
|
||||
DateTime.to_iso8601(packet["received_at"])
|
||||
|
||||
defp get_comment(%{comment: comment}) when is_binary(comment), do: comment
|
||||
defp get_comment(_), do: ""
|
||||
true ->
|
||||
""
|
||||
end
|
||||
|
||||
defp build_data_extended(nil), do: nil
|
||||
comment = Map.get(data_extended, :comment) || Map.get(data_extended, "comment") || ""
|
||||
|
||||
defp build_data_extended(data_extended) do
|
||||
case data_extended do
|
||||
%MicE{} = mic_e ->
|
||||
%{
|
||||
type: "MicE",
|
||||
latitude: mic_e.lat_degrees + mic_e.lat_minutes / 60.0 + mic_e.lat_fractional / 6000.0,
|
||||
longitude: mic_e.lon_degrees + mic_e.lon_minutes / 60.0 + mic_e.lon_fractional / 6000.0,
|
||||
symbol_table_id: Map.get(mic_e, :symbol_table_id, "/"),
|
||||
symbol_code: Map.get(mic_e, :symbol_code, ">"),
|
||||
comment: Map.get(mic_e, :comment, "")
|
||||
}
|
||||
to_float = fn
|
||||
%Decimal{} = d ->
|
||||
Decimal.to_float(d)
|
||||
|
||||
%{} = data ->
|
||||
# Convert struct to map if needed, filtering out private fields
|
||||
data
|
||||
|> Map.from_struct()
|
||||
|> Map.delete(:__meta__)
|
||||
n when is_float(n) ->
|
||||
n
|
||||
|
||||
n when is_integer(n) ->
|
||||
n * 1.0
|
||||
|
||||
n when is_binary(n) ->
|
||||
case Float.parse(n) do
|
||||
{f, _} -> f
|
||||
:error -> 0.0
|
||||
end
|
||||
|
||||
_ ->
|
||||
nil
|
||||
0.0
|
||||
end
|
||||
|
||||
popup = """
|
||||
<div class=\"aprs-popup\">
|
||||
<div class=\"aprs-callsign\"><strong><a href=\"/#{callsign}\">#{callsign}</a></strong></div>
|
||||
<div class=\"aprs-symbol-info\">#{symbol_description}</div>
|
||||
#{if comment == "", do: "", else: "<div class=\\\"aprs-comment\\\">#{comment}</div>"}
|
||||
<div class=\"aprs-coords\">#{Float.round(to_float.(lat), 4)}, #{Float.round(to_float.(lng), 4)}</div>
|
||||
<div class=\"aprs-time\">#{timestamp}</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
%{
|
||||
"id" => callsign,
|
||||
"callsign" => callsign,
|
||||
"base_callsign" => Map.get(packet, :base_callsign, Map.get(packet, "base_callsign", "")),
|
||||
"ssid" => Map.get(packet, :ssid, Map.get(packet, "ssid", 0)),
|
||||
"lat" => to_float.(lat),
|
||||
"lng" => to_float.(lng),
|
||||
"data_type" => to_string(Map.get(packet, :data_type, Map.get(packet, "data_type", "unknown"))),
|
||||
"path" => Map.get(packet, :path, Map.get(packet, "path", "")),
|
||||
"comment" => comment,
|
||||
"data_extended" => data_extended || %{},
|
||||
"symbol_table_id" => symbol_table_id,
|
||||
"symbol_code" => symbol_code,
|
||||
"symbol_description" => symbol_description,
|
||||
"timestamp" => timestamp,
|
||||
"popup" => popup
|
||||
}
|
||||
end
|
||||
|
||||
defp packet_matches_callsign?(packet, target_callsign) do
|
||||
|
|
@ -900,7 +883,7 @@ defmodule AprsWeb.MapLive.CallsignView do
|
|||
latest_packet =
|
||||
%{callsign: callsign}
|
||||
|> Packets.get_recent_packets()
|
||||
|> Enum.filter(&has_position_data?/1)
|
||||
|> Enum.filter(&MapHelpers.has_position_data?/1)
|
||||
|> Enum.sort_by(& &1.received_at, {:desc, DateTime})
|
||||
|> List.first()
|
||||
|
||||
|
|
@ -910,7 +893,7 @@ defmodule AprsWeb.MapLive.CallsignView do
|
|||
nil
|
||||
|
||||
packet ->
|
||||
{lat, lng} = get_coordinates(packet)
|
||||
{lat, lng, _} = MapHelpers.get_coordinates(packet)
|
||||
if lat && lng, do: %{lat: lat, lng: lng}
|
||||
end
|
||||
|
||||
|
|
@ -960,43 +943,4 @@ defmodule AprsWeb.MapLive.CallsignView do
|
|||
latest_symbol_code: latest_symbol_code
|
||||
)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ defmodule AprsWeb.MapLive.Index do
|
|||
use AprsWeb, :live_view
|
||||
|
||||
alias AprsWeb.Endpoint
|
||||
alias AprsWeb.MapLive.MapHelpers
|
||||
alias Phoenix.LiveView.Socket
|
||||
|
||||
require Logger
|
||||
|
|
@ -391,7 +392,7 @@ defmodule AprsWeb.MapLive.Index do
|
|||
end
|
||||
|
||||
defp handle_info_postgres_packet(packet, socket) do
|
||||
{lat, lon, _data_extended} = get_coordinates(packet)
|
||||
{lat, lon, _data_extended} = MapHelpers.get_coordinates(packet)
|
||||
|
||||
callsign_key =
|
||||
if Map.has_key?(packet, "id"),
|
||||
|
|
@ -399,7 +400,7 @@ defmodule AprsWeb.MapLive.Index do
|
|||
else: System.unique_integer([:positive])
|
||||
|
||||
Logger.debug(
|
||||
"[MAP] Incoming packet: id=#{inspect(callsign_key)} lat=#{inspect(lat)} lon=#{inspect(lon)} bounds=#{inspect(socket.assigns.map_bounds)} within_bounds?=#{inspect(within_bounds?(%{lat: lat, lon: lon}, socket.assigns.map_bounds))}"
|
||||
"[MAP] Incoming packet: id=#{inspect(callsign_key)} lat=#{inspect(lat)} lon=#{inspect(lon)} bounds=#{inspect(socket.assigns.map_bounds)} within_bounds?=#{inspect(MapHelpers.within_bounds?(%{lat: lat, lon: lon}, socket.assigns.map_bounds))}"
|
||||
)
|
||||
|
||||
all_packets = Map.put(socket.assigns.all_packets, callsign_key, packet)
|
||||
|
|
@ -408,7 +409,7 @@ defmodule AprsWeb.MapLive.Index do
|
|||
# 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
|
||||
not MapHelpers.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)}
|
||||
|
|
@ -417,7 +418,7 @@ defmodule AprsWeb.MapLive.Index do
|
|||
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
|
||||
if MapHelpers.within_bounds?(%{lat: lat, lon: lon}, socket.assigns.map_bounds) do
|
||||
handle_valid_postgres_packet(packet, lat, lon, socket)
|
||||
else
|
||||
{:noreply, socket}
|
||||
|
|
@ -755,7 +756,7 @@ defmodule AprsWeb.MapLive.Index do
|
|||
|
||||
@spec within_bounds?(map() | struct(), map()) :: boolean()
|
||||
defp within_bounds?(packet, bounds) do
|
||||
{lat, lon, _data_extended} = get_coordinates(packet)
|
||||
{lat, lon, _data_extended} = MapHelpers.get_coordinates(packet)
|
||||
|
||||
# Basic validation
|
||||
if is_nil(lat) or is_nil(lon) do
|
||||
|
|
@ -778,45 +779,9 @@ defmodule AprsWeb.MapLive.Index do
|
|||
end
|
||||
end
|
||||
|
||||
@spec get_coordinates(map() | struct()) :: {number() | nil, number() | nil, map() | nil}
|
||||
defp get_coordinates(packet) do
|
||||
# Safely get data_extended for both atom and string keys
|
||||
data_extended = Map.get(packet, :data_extended) || Map.get(packet, "data_extended")
|
||||
|
||||
lat =
|
||||
Map.get(packet, :lat) ||
|
||||
Map.get(packet, "lat") ||
|
||||
(is_map(data_extended) &&
|
||||
(Map.get(data_extended, :latitude) || Map.get(data_extended, "latitude")))
|
||||
|
||||
lon =
|
||||
Map.get(packet, :lon) ||
|
||||
Map.get(packet, "lon") ||
|
||||
(is_map(data_extended) &&
|
||||
(Map.get(data_extended, :longitude) || Map.get(data_extended, "longitude")))
|
||||
|
||||
lat =
|
||||
cond do
|
||||
is_struct(lat, Decimal) -> Decimal.to_float(lat)
|
||||
is_float(lat) -> lat
|
||||
is_integer(lat) -> lat * 1.0
|
||||
true -> lat
|
||||
end
|
||||
|
||||
lon =
|
||||
cond do
|
||||
is_struct(lon, Decimal) -> Decimal.to_float(lon)
|
||||
is_float(lon) -> lon
|
||||
is_integer(lon) -> lon * 1.0
|
||||
true -> lon
|
||||
end
|
||||
|
||||
{lat, lon, data_extended}
|
||||
end
|
||||
|
||||
@spec build_packet_data(map() | struct()) :: map() | nil
|
||||
defp build_packet_data(packet) do
|
||||
{lat, lon, data_extended} = get_coordinates(packet)
|
||||
{lat, lon, data_extended} = MapHelpers.get_coordinates(packet)
|
||||
callsign = Map.get(packet, :base_callsign, Map.get(packet, "base_callsign", ""))
|
||||
# Only include packets with valid position data and a non-empty callsign
|
||||
if lat && lon && callsign != "" && callsign != nil do
|
||||
|
|
@ -853,12 +818,32 @@ defmodule AprsWeb.MapLive.Index do
|
|||
|
||||
comment = Map.get(data_extended, :comment) || Map.get(data_extended, "comment") || ""
|
||||
|
||||
to_float = fn
|
||||
%Decimal{} = d ->
|
||||
Decimal.to_float(d)
|
||||
|
||||
n when is_float(n) ->
|
||||
n
|
||||
|
||||
n when is_integer(n) ->
|
||||
n * 1.0
|
||||
|
||||
n when is_binary(n) ->
|
||||
case Float.parse(n) do
|
||||
{f, _} -> f
|
||||
:error -> 0.0
|
||||
end
|
||||
|
||||
_ ->
|
||||
0.0
|
||||
end
|
||||
|
||||
popup = """
|
||||
<div class=\"aprs-popup\">
|
||||
<div class=\"aprs-callsign\"><strong><a href=\"/#{callsign}\">#{callsign}</a></strong></div>
|
||||
<div class=\"aprs-symbol-info\">#{symbol_description}</div>
|
||||
#{if comment == "", do: "", else: "<div class=\\\"aprs-comment\\\">#{comment}</div>"}
|
||||
<div class=\"aprs-coords\">#{Float.round(lat, 4)}, #{Float.round(lon, 4)}</div>
|
||||
<div class=\"aprs-coords\">#{Float.round(to_float.(lat), 4)}, #{Float.round(to_float.(lon), 4)}</div>
|
||||
<div class=\"aprs-time\">#{timestamp}</div>
|
||||
</div>
|
||||
"""
|
||||
|
|
@ -868,8 +853,8 @@ defmodule AprsWeb.MapLive.Index do
|
|||
"callsign" => callsign,
|
||||
"base_callsign" => Map.get(packet, :base_callsign, Map.get(packet, "base_callsign", "")),
|
||||
"ssid" => Map.get(packet, :ssid, Map.get(packet, "ssid", 0)),
|
||||
"lat" => lat,
|
||||
"lng" => lon,
|
||||
"lat" => to_float.(lat),
|
||||
"lng" => to_float.(lon),
|
||||
"data_type" => to_string(Map.get(packet, :data_type, Map.get(packet, "data_type", "unknown"))),
|
||||
"path" => Map.get(packet, :path, Map.get(packet, "path", "")),
|
||||
"comment" => comment,
|
||||
|
|
|
|||
118
lib/aprs_web/live/map_live/map_helpers.ex
Normal file
118
lib/aprs_web/live/map_live/map_helpers.ex
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
defmodule AprsWeb.MapLive.MapHelpers do
|
||||
@moduledoc """
|
||||
Shared helpers for APRS map LiveViews (main map, callsign map, etc).
|
||||
Provides coordinate extraction, position checks, and bounds logic.
|
||||
"""
|
||||
|
||||
alias Parser.Types.MicE
|
||||
|
||||
@spec get_coordinates(map() | struct()) :: {number() | nil, number() | nil, map() | nil}
|
||||
def get_coordinates(%{data_extended: %MicE{} = mic_e}) do
|
||||
{lat, lon} = get_coordinates_from_mic_e(mic_e)
|
||||
{lat, lon, mic_e}
|
||||
end
|
||||
|
||||
def get_coordinates(%{data_extended: %{latitude: lat, longitude: lon}} = packet) do
|
||||
{lat, lon, packet.data_extended}
|
||||
end
|
||||
|
||||
def get_coordinates(packet) do
|
||||
lat = Map.get(packet, :lat) || Map.get(packet, "lat")
|
||||
lon = Map.get(packet, :lon) || Map.get(packet, "lon")
|
||||
data_extended = Map.get(packet, :data_extended) || Map.get(packet, "data_extended")
|
||||
{lat, lon, data_extended}
|
||||
end
|
||||
|
||||
@spec get_coordinates_from_mic_e(MicE.t()) :: {number() | nil, number() | nil}
|
||||
def get_coordinates_from_mic_e(mic_e) do
|
||||
lat = mic_e.lat_degrees + mic_e.lat_minutes / 60.0 + mic_e.lat_fractional / 6000.0
|
||||
lat = if mic_e.lat_direction == :south, do: -lat, else: lat
|
||||
lon = mic_e.lon_degrees + mic_e.lon_minutes / 60.0 + mic_e.lon_fractional / 6000.0
|
||||
lon = if mic_e.lon_direction == :west, do: -lon, else: lon
|
||||
if lat >= -90 && lat <= 90 && lon >= -180 && lon <= 180, do: {lat, lon}, else: {nil, nil}
|
||||
end
|
||||
|
||||
@spec has_position_data?(map()) :: boolean()
|
||||
def has_position_data?(packet) do
|
||||
lat = Map.get(packet, :lat) || Map.get(packet, "lat")
|
||||
lon = Map.get(packet, :lon) || Map.get(packet, "lon")
|
||||
|
||||
if not is_nil(lat) and not is_nil(lon) do
|
||||
true
|
||||
else
|
||||
data_extended = Map.get(packet, :data_extended) || Map.get(packet, "data_extended")
|
||||
|
||||
case data_extended do
|
||||
%MicE{} -> true
|
||||
%{latitude: lat, longitude: lon} when not is_nil(lat) and not is_nil(lon) -> true
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@spec within_bounds?(map() | tuple(), map()) :: boolean()
|
||||
def within_bounds?(packet_or_coords, bounds) do
|
||||
to_float = fn
|
||||
n when is_float(n) ->
|
||||
n
|
||||
|
||||
n when is_integer(n) ->
|
||||
n * 1.0
|
||||
|
||||
%Decimal{} = d ->
|
||||
Decimal.to_float(d)
|
||||
|
||||
n when is_binary(n) ->
|
||||
case Float.parse(n) do
|
||||
{f, _} -> f
|
||||
:error -> 0.0
|
||||
end
|
||||
|
||||
_ ->
|
||||
0.0
|
||||
end
|
||||
|
||||
{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 = 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
|
||||
end
|
||||
end
|
||||
end
|
||||
127
test/aprs_web/live/map_live/map_helpers_test.exs
Normal file
127
test/aprs_web/live/map_live/map_helpers_test.exs
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
defmodule AprsWeb.MapLive.MapHelpersTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias AprsWeb.MapLive.MapHelpers
|
||||
alias Parser.Types.MicE
|
||||
|
||||
describe "get_coordinates/1" do
|
||||
test "returns lat/lon/data_extended for map with lat/lon" do
|
||||
packet = %{lat: 10.0, lon: 20.0, data_extended: %{foo: :bar}}
|
||||
assert MapHelpers.get_coordinates(packet) == {10.0, 20.0, %{foo: :bar}}
|
||||
end
|
||||
|
||||
test "returns lat/lon/data_extended for map with latitude/longitude in data_extended" do
|
||||
packet = %{data_extended: %{latitude: 11.1, longitude: 22.2}}
|
||||
|
||||
assert MapHelpers.get_coordinates(packet) ==
|
||||
{11.1, 22.2, %{latitude: 11.1, longitude: 22.2}}
|
||||
end
|
||||
|
||||
test "returns lat/lon/mic_e for MicE struct" do
|
||||
mic_e = %MicE{
|
||||
lat_degrees: 12,
|
||||
lat_minutes: 34,
|
||||
lat_fractional: 0,
|
||||
lat_direction: :north,
|
||||
lon_degrees: 56,
|
||||
lon_minutes: 78,
|
||||
lon_fractional: 0,
|
||||
lon_direction: :east
|
||||
}
|
||||
|
||||
packet = %{data_extended: mic_e}
|
||||
{lat, lon, ext} = MapHelpers.get_coordinates(packet)
|
||||
assert is_number(lat) and is_number(lon)
|
||||
assert ext == mic_e
|
||||
end
|
||||
|
||||
test "returns {nil, nil, nil} for missing data" do
|
||||
assert MapHelpers.get_coordinates(%{}) == {nil, nil, nil}
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_coordinates_from_mic_e/1" do
|
||||
test "returns correct lat/lon for valid MicE" do
|
||||
mic_e = %MicE{
|
||||
lat_degrees: 10,
|
||||
lat_minutes: 30,
|
||||
lat_fractional: 0,
|
||||
lat_direction: :north,
|
||||
lon_degrees: 20,
|
||||
lon_minutes: 40,
|
||||
lon_fractional: 0,
|
||||
lon_direction: :east
|
||||
}
|
||||
|
||||
{lat, lon} = MapHelpers.get_coordinates_from_mic_e(mic_e)
|
||||
assert_in_delta lat, 10.5, 0.0001
|
||||
assert_in_delta lon, 20.6667, 0.0001
|
||||
end
|
||||
|
||||
test "returns {nil, nil} for out-of-bounds" do
|
||||
mic_e = %MicE{
|
||||
lat_degrees: 100,
|
||||
lat_minutes: 0,
|
||||
lat_fractional: 0,
|
||||
lat_direction: :north,
|
||||
lon_degrees: 200,
|
||||
lon_minutes: 0,
|
||||
lon_fractional: 0,
|
||||
lon_direction: :east
|
||||
}
|
||||
|
||||
assert MapHelpers.get_coordinates_from_mic_e(mic_e) == {nil, nil}
|
||||
end
|
||||
end
|
||||
|
||||
describe "has_position_data?/1" do
|
||||
test "true for MicE in data_extended" do
|
||||
mic_e = %MicE{
|
||||
lat_degrees: 1,
|
||||
lat_minutes: 1,
|
||||
lat_fractional: 0,
|
||||
lat_direction: :north,
|
||||
lon_degrees: 1,
|
||||
lon_minutes: 1,
|
||||
lon_fractional: 0,
|
||||
lon_direction: :east
|
||||
}
|
||||
|
||||
assert MapHelpers.has_position_data?(%{data_extended: mic_e})
|
||||
end
|
||||
|
||||
test "true for latitude/longitude in data_extended" do
|
||||
assert MapHelpers.has_position_data?(%{data_extended: %{latitude: 1, longitude: 2}})
|
||||
end
|
||||
|
||||
test "true for lat/lon at top level" do
|
||||
assert MapHelpers.has_position_data?(%{lat: 1, lon: 2})
|
||||
end
|
||||
|
||||
test "false for missing position" do
|
||||
refute MapHelpers.has_position_data?(%{})
|
||||
end
|
||||
end
|
||||
|
||||
describe "within_bounds?/2" do
|
||||
test "true for point in bounds" do
|
||||
bounds = %{north: 10, south: 0, east: 10, west: 0}
|
||||
assert MapHelpers.within_bounds?(%{lat: 5, lon: 5}, bounds)
|
||||
end
|
||||
|
||||
test "false for point out of bounds" do
|
||||
bounds = %{north: 10, south: 0, east: 10, west: 0}
|
||||
refute MapHelpers.within_bounds?(%{lat: 15, lon: 5}, bounds)
|
||||
end
|
||||
|
||||
test "true for tuple input" do
|
||||
bounds = %{north: 10, south: 0, east: 10, west: 0}
|
||||
assert MapHelpers.within_bounds?({5, 5}, bounds)
|
||||
end
|
||||
|
||||
test "false for nil input" do
|
||||
bounds = %{north: 10, south: 0, east: 10, west: 0}
|
||||
refute MapHelpers.within_bounds?(%{}, bounds)
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue