diff --git a/lib/aprs_web/live/map_live/callsign_view.ex b/lib/aprs_web/live/map_live/callsign_view.ex
index 735590f..d7ec274 100644
--- a/lib/aprs_web/live/map_live/callsign_view.ex
+++ b/lib/aprs_web/live/map_live/callsign_view.ex
@@ -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 = """
+
+ """
+
+ %{
+ "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
diff --git a/lib/aprs_web/live/map_live/index.ex b/lib/aprs_web/live/map_live/index.ex
index 84994b6..ebe947a 100644
--- a/lib/aprs_web/live/map_live/index.ex
+++ b/lib/aprs_web/live/map_live/index.ex
@@ -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 = """
"""
@@ -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,
diff --git a/lib/aprs_web/live/map_live/map_helpers.ex b/lib/aprs_web/live/map_live/map_helpers.ex
new file mode 100644
index 0000000..4aa3915
--- /dev/null
+++ b/lib/aprs_web/live/map_live/map_helpers.ex
@@ -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
diff --git a/test/aprs_web/live/map_live/map_helpers_test.exs b/test/aprs_web/live/map_live/map_helpers_test.exs
new file mode 100644
index 0000000..1495ff6
--- /dev/null
+++ b/test/aprs_web/live/map_live/map_helpers_test.exs
@@ -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