prop/lib/microwaveprop/radio/maidenhead.ex

130 lines
4.2 KiB
Elixir
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

defmodule Microwaveprop.Radio.Maidenhead do
@moduledoc false
# Maidenhead grid: Field (2 letters A-R) + Square (2 digits) + optional
# repeating pairs of Subsquare (2 letters) and Extended Square (2 digits).
# Minimum 4 chars, always even length. E.g. EM12, EM12kp, EM12kp37, EM12kp37ab
@spec valid?(any()) :: boolean()
def valid?(nil), do: false
def valid?(grid) when is_binary(grid) do
len = byte_size(grid)
len >= 4 and rem(len, 2) == 0 and String.match?(grid, grid_regex())
end
def valid?(_), do: false
# Field: [A-Ra-r]{2}, Square: [0-9]{2}, then alternating letter/digit pairs
defp grid_regex do
~r/^[A-Ra-r]{2}[0-9]{2}(?:[A-Xa-x]{2}(?:[0-9]{2}(?:[A-Xa-x]{2}(?:[0-9]{2})*)*)*)*$/
end
@spec to_latlon(any()) :: {:ok, {float(), float()}} | :error
def to_latlon(nil), do: :error
def to_latlon(grid) when is_binary(grid) do
grid = String.upcase(grid)
if valid?(grid) do
<<f1, f2, s1, s2>> <> rest = grid
# Field: 18 divisions (A-R), 20° lon / 10° lat each
lon = (f1 - ?A) * 20.0 - 180.0
lat = (f2 - ?A) * 10.0 - 90.0
# Square: 10 divisions (0-9), 2° lon / 1° lat each
lon = lon + (s1 - ?0) * 2.0
lat = lat + (s2 - ?0) * 1.0
chars = String.to_charlist(rest)
{lat, lon, lat_size, lon_size} = decode_pairs(chars, lat, lon, 1.0, 2.0, :letter)
{:ok, {lat + lat_size / 2, lon + lon_size / 2}}
else
:error
end
end
def to_latlon(_), do: :error
@doc """
Converts a lat/lon pair into a Maidenhead grid at the requested
precision (must be an even number between 4 and 10). Default precision
is 6 (subsquare level, ~5'×2.5', ~7 km × 4.6 km at the equator).
"""
@spec from_latlon(number(), number(), pos_integer()) :: String.t()
def from_latlon(lat, lon, precision \\ 6) when is_number(lat) and is_number(lon) and precision in [4, 6, 8, 10] do
# Clamp into the open ranges the field encoder expects. Exactly
# `90.0` / `180.0` would otherwise overflow the trunc into 'S'
# (one past 'R'), producing grids outside the A..R alphabet.
lat = min(max(lat + 90.0, 0.0), 180.0 - 1.0e-9)
lon = min(max(lon + 180.0, 0.0), 360.0 - 1.0e-9)
# Field (A-R), 20° lon / 10° lat
f1 = trunc(lon / 20.0)
f2 = trunc(lat / 10.0)
lon = lon - f1 * 20.0
lat = lat - f2 * 10.0
# Square (0-9), 2° lon / 1° lat
s1 = trunc(lon / 2.0)
s2 = trunc(lat / 1.0)
lon = lon - s1 * 2.0
lat = lat - s2 * 1.0
grid = <<?A + f1, ?A + f2, ?0 + s1, ?0 + s2>>
if precision == 4 do
grid
else
# Subsquare (a-x), 5'/60 lon, 2.5'/60 lat
ss1 = trunc(lon * 60.0 / 5.0)
ss2 = trunc(lat * 60.0 / 2.5)
lon = lon - ss1 * (5.0 / 60.0)
lat = lat - ss2 * (2.5 / 60.0)
grid = grid <> <<?a + ss1, ?a + ss2>>
append_extended(grid, lat, lon, precision)
end
end
defp append_extended(grid, _lat, _lon, 6), do: grid
defp append_extended(grid, lat, lon, precision) when precision in [8, 10] do
# Extended square (0-9), (5'/60)/10 lon, (2.5'/60)/10 lat
e1 = trunc(lon * 60.0 * 10.0 / 5.0)
e2 = trunc(lat * 60.0 * 10.0 / 2.5)
lon = lon - e1 * (5.0 / 60.0 / 10.0)
lat = lat - e2 * (2.5 / 60.0 / 10.0)
grid = grid <> <<?0 + e1, ?0 + e2>>
if precision == 8 do
grid
else
# Extended-extended subsquare (a-x), step is the extended-square step / 24
x1 = trunc(lon * 60.0 * 10.0 * 24.0 / 5.0)
x2 = trunc(lat * 60.0 * 10.0 * 24.0 / 2.5)
grid <> <<?a + x1, ?a + x2>>
end
end
# Decode remaining alternating letter(24)/digit(10) pairs after field+square
defp decode_pairs([c1, c2 | rest], lat, lon, lat_size, lon_size, :letter) do
cell_lon = lon_size / 24
cell_lat = lat_size / 24
lon = lon + (c1 - ?A) * cell_lon
lat = lat + (c2 - ?A) * cell_lat
decode_pairs(rest, lat, lon, cell_lat, cell_lon, :digit)
end
defp decode_pairs([c1, c2 | rest], lat, lon, lat_size, lon_size, :digit) do
cell_lon = lon_size / 10
cell_lat = lat_size / 10
lon = lon + (c1 - ?0) * cell_lon
lat = lat + (c2 - ?0) * cell_lat
decode_pairs(rest, lat, lon, cell_lat, cell_lon, :letter)
end
defp decode_pairs([], lat, lon, lat_size, lon_size, _type) do
{lat, lon, lat_size, lon_size}
end
end