aprs.me/lib/aprsme/geo_utils.ex
Graham McIntire 3f2e0252d5
refactor: dedupe utilities, prefer pattern matching, tighten specs
- consolidate Haversine: CoordinateUtils delegates to GeoUtils;
  InfoLive.haversine/4 reuses it (m -> km)
- AprsSymbol: collapse 3 sprite-info builders onto build_sprite_info/2
  and add a sprite_info @type
- DataBuilder: 3 weather unit converters share convert_unit/4 with
  explicit clauses for "N/A", numeric, and binary inputs
- mobile_channel: do_callsign_match/2 uses pattern-match clauses for
  wildcard handling instead of nested if
- info_live: replace nested case in position_changed? with multi-clause
  coords_changed?/1; format_timestamp_for_display pipelines into a
  guarded build_timestamp_display/1
- api_docs_live: collapse chained single-key assigns into keyword-list
  assign/2 calls
- add strict @spec coverage on all touched helpers; tighten new specs to
  match dialyzer success typing (no contract_supertype warnings)
2026-04-28 14:18:58 -05:00

55 lines
1.8 KiB
Elixir

defmodule Aprsme.GeoUtils do
@moduledoc """
Geographic utility functions for calculating distances and related operations.
"""
@earth_radius_meters 6_371_000
@doc """
Calculate the Haversine distance between two points in meters.
Returns `nil` when any coordinate is not a number.
## Examples
iex> Aprsme.GeoUtils.haversine_distance(33.16961, -96.4921, 33.16962, -96.4921)
1.11
"""
@spec haversine_distance(number(), number(), number(), number()) :: float()
@spec haversine_distance(any(), any(), any(), any()) :: nil
def haversine_distance(lat1, lon1, lat2, lon2)
when is_number(lat1) and is_number(lon1) and is_number(lat2) and is_number(lon2) do
# Convert to radians
lat1_rad = lat1 * :math.pi() / 180
lat2_rad = lat2 * :math.pi() / 180
dlat_rad = (lat2 - lat1) * :math.pi() / 180
dlon_rad = (lon2 - lon1) * :math.pi() / 180
# Haversine formula
a =
:math.sin(dlat_rad / 2) * :math.sin(dlat_rad / 2) +
:math.cos(lat1_rad) * :math.cos(lat2_rad) *
:math.sin(dlon_rad / 2) * :math.sin(dlon_rad / 2)
c = 2 * :math.atan2(:math.sqrt(a), :math.sqrt(1 - a))
# Distance in meters
@earth_radius_meters * c
end
def haversine_distance(_, _, _, _), do: nil
@doc """
Check if the distance between two points exceeds a minimum threshold.
This helps filter out GPS drift and insignificant movements.
Default threshold is 50 meters to account for typical GPS accuracy variations.
"""
@spec significant_movement?(any(), any(), any(), any(), number()) :: boolean()
def significant_movement?(lat1, lon1, lat2, lon2, threshold_meters \\ 50) do
case haversine_distance(lat1, lon1, lat2, lon2) do
nil -> false
distance -> distance > threshold_meters
end
end
end