Files
ham/util/maidenhead/maidenhead.go
2026-02-14 15:59:31 +01:00

149 lines
3.9 KiB
Go

// Package maidenhead implements the Maidenhead Locator System, a geographic
// coordinate system used by amataur radio (HAM) operators.
package maidenhead
import (
"errors"
"fmt"
"math"
"strings"
)
// Precision of the computed locator.
const (
FieldPrecision = iota + 1
SquarePrecision
SubSquarePrecision
ExtendedSquarePrecision
)
var (
upper = "ABCDEFGHIJKLMNOPQRSTUVWX"
lower = "abcdefghijklmnopqrstuvwx"
digit = "0123456789"
)
// locator computes the Maidenhead Locator for a given position.
func locator(p Point, precision int) (string, error) {
if math.IsNaN(p.Latitude) {
return "", errors.New("maidenhead: latitude is not a digit")
}
if math.IsInf(p.Latitude, 0) {
return "", errors.New("maidenhead: latitude is infinite")
}
if math.IsNaN(p.Longitude) {
return "", errors.New("maidenhead: longitude is not a digit")
}
if math.IsInf(p.Longitude, 0) {
return "", errors.New("maidenhead: longitude is infinite")
}
if math.Abs(p.Latitude) == 90.0 {
return "", errors.New("maidenhead: grid square invalid at poles")
} else if math.Abs(p.Latitude) > 90.0 {
return "", fmt.Errorf("maidenhead: invalid latitude %.04f", p.Latitude)
} else if math.Abs(p.Longitude) > 180.0 {
return "", fmt.Errorf("maidenhead: invalid longitude %.05f", p.Longitude)
}
var (
lat = p.Latitude + 90.0
lng = p.Longitude + 180.0
loc string
)
lat = lat/10.0 + 0.0000001
lng = lng/20.0 + 0.0000001
loc = loc + string(upper[int(lng)]) + string(upper[int(lat)])
if precision == 1 {
return loc, nil
}
lat = 10 * (lat - math.Floor(lat))
lng = 10 * (lng - math.Floor(lng))
loc = loc + fmt.Sprintf("%d%d", int(lng)%10, int(lat)%10)
if precision == 2 {
return loc, nil
}
lat = 24 * (lat - math.Floor(lat))
lng = 24 * (lng - math.Floor(lng))
loc = loc + string(upper[int(lng)]) + string(upper[int(lat)])
if precision == 3 {
return loc, nil
}
lat = 10 * (lat - math.Floor(lat))
lng = 10 * (lng - math.Floor(lng))
loc = loc + fmt.Sprintf("%d%d", int(lng)%10, int(lat)%10)
if precision == 4 {
return loc, nil
}
lat = 24 * (lat - math.Floor(lat))
lng = 24 * (lng - math.Floor(lng))
loc = loc + string(lower[int(lng)]) + string(lower[int(lat)])
return loc, nil
}
var parseLocatorMult = []struct {
s, p string
mult float64
}{
{upper[:18], lower[:18], 20.0},
{upper[:18], lower[:18], 10.0},
{digit[:10], digit[:10], 20.0 / 10.0},
{digit[:10], digit[:10], 10.0 / 10.0},
{upper[:24], lower[:24], 20.0 / (10.0 * 24.0)},
{upper[:24], lower[:24], 10.0 / (10.0 * 24.0)},
{digit[:10], digit[:10], 20.0 / (10.0 * 24.0 * 10.0)},
{digit[:10], digit[:10], 10.0 / (10.0 * 24.0 * 10.0)},
{lower[:24], lower[:24], 20.0 / (10.0 * 24.0 * 10.0 * 24.0)},
{lower[:24], lower[:24], 10.0 / (10.0 * 24.0 * 10.0 * 24.0)},
}
var maxLocatorLength = len(parseLocatorMult)
func parseLocator(locator string, strict bool, centered bool) (point Point, err error) {
var (
lnglat = [2]float64{
-180.0,
-90.0,
}
i, j int
char rune
)
if len(locator) > maxLocatorLength {
err = fmt.Errorf("maidenhead: locator is too long (%d characters, maximum %d characters allowed)",
len(locator), maxLocatorLength)
return
}
if len(locator)%2 != 0 {
err = fmt.Errorf("maidenhead: locator has odd number of characters")
return
}
if strict {
for i, char = range locator {
if j = strings.Index(parseLocatorMult[i].s, string(char)); j < 0 {
err = fmt.Errorf("maidenhead: invalid character at offset %d", i)
return
}
lnglat[i%2] += float64(j) * parseLocatorMult[i].mult
}
} else {
for i, char = range strings.ToLower(locator) {
if j = strings.Index(parseLocatorMult[i].p, string(char)); j < 0 {
err = fmt.Errorf("maidenhead: invalid character at offset %d", i)
return
}
lnglat[i%2] += float64(j) * parseLocatorMult[i].mult
}
}
if centered {
lnglat[0] += parseLocatorMult[i-1].mult / 2.0
lnglat[1] += parseLocatorMult[i].mult / 2.0
}
point = NewPoint(lnglat[1], lnglat[0])
return
}