This commit is contained in:
Graham McIntire 2025-06-20 14:30:21 -05:00
parent ecee5fd677
commit e68c0aca41
No known key found for this signature in database
4 changed files with 361 additions and 187 deletions

View file

@ -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

View file

@ -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,

View 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

View 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