From 737de10bf4f210cef44f643023fd532277f44cdf Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Thu, 2 Apr 2026 07:31:52 -0500 Subject: [PATCH] Support arbitrary-precision Maidenhead grid squares Extend grid validation and decoding from 4/6 chars to unlimited even-length grids (8, 10, etc). Recursive decoder handles alternating letter(/24) and digit(/10) subdivision pairs after the field+square. Validated against gridmap-web's Maidenhead implementation. --- lib/microwaveprop/radio/maidenhead.ex | 52 ++++++++++++++++++++------- 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/lib/microwaveprop/radio/maidenhead.ex b/lib/microwaveprop/radio/maidenhead.ex index ad730d09..b532af3d 100644 --- a/lib/microwaveprop/radio/maidenhead.ex +++ b/lib/microwaveprop/radio/maidenhead.ex @@ -1,16 +1,24 @@ 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 - String.match?(grid, ~r/^[A-Ra-r]{2}[0-9]{2}$/) or - String.match?(grid, ~r/^[A-Ra-r]{2}[0-9]{2}[A-Xa-x]{2}$/) + 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 @@ -20,22 +28,42 @@ defmodule Microwaveprop.Radio.Maidenhead do if valid?(grid) do <> <> rest = grid - lon = (f1 - ?A) * 20 + (s1 - ?0) * 2 - 180 - lat = (f2 - ?A) * 10 + (s2 - ?0) * 1 - 90 + # Field: 18 divisions (A-R), 20° lon / 10° lat each + lon = (f1 - ?A) * 20.0 - 180.0 + lat = (f2 - ?A) * 10.0 - 90.0 - case rest do - <> -> - lon = lon + (ss1 - ?A) * (5 / 60) + 5 / 120 - lat = lat + (ss2 - ?A) * (2.5 / 60) + 2.5 / 120 - {:ok, {lat, lon}} + # Square: 10 divisions (0-9), 2° lon / 1° lat each + lon = lon + (s1 - ?0) * 2.0 + lat = lat + (s2 - ?0) * 1.0 - "" -> - {:ok, {lat + 0.5, lon + 1.0}} - end + 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 + + # 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