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