Initial import
This commit is contained in:
10
util/maidenhead/doc.go
Normal file
10
util/maidenhead/doc.go
Normal file
@@ -0,0 +1,10 @@
|
||||
// Package maidenhead implements the maidenhead locator system.
|
||||
//
|
||||
// The Maidenhead Locator System (a.k.a. QTH Locator and IARU Locator) is a geocode system used
|
||||
// by amateur radio operators to succinctly describe their geographic coordinates, which replaced
|
||||
// the deprecated QRA locator, which was limited to European contacts.
|
||||
//
|
||||
// Maidenhead locators are also commonly referred to as QTH locators, grid locators or grid
|
||||
// squares, although the "squares" are distorted on any non-equirectangular cartographic
|
||||
// projection.
|
||||
package maidenhead
|
||||
148
util/maidenhead/maidenhead.go
Normal file
148
util/maidenhead/maidenhead.go
Normal file
@@ -0,0 +1,148 @@
|
||||
// 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
|
||||
}
|
||||
142
util/maidenhead/maidenhead_test.go
Normal file
142
util/maidenhead/maidenhead_test.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package maidenhead
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var tests = []struct {
|
||||
point Point
|
||||
loc string
|
||||
loc4 string
|
||||
}{
|
||||
{Point{48.14666, 11.60833}, "JN58TD", "JN58TD25"},
|
||||
{Point{-34.91, -56.21166}, "GF15VC", "GF15VC41"},
|
||||
{Point{38.92, -77.065}, "FM18LW", "FM18LW20"},
|
||||
{Point{-41.28333, 174.745}, "RE78IR", "RE78IR92"},
|
||||
{Point{41.714775, -72.727260}, "FN31PR", "FN31PR21"},
|
||||
{Point{37.413708, -122.1073236}, "CM87WJ", "CM87WJ79"},
|
||||
{Point{35.0542, -85.1142}, "EM75KB", "EM75KB63"},
|
||||
}
|
||||
|
||||
func TestGridSquare(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
t.Run(test.loc, func(t *testing.T) {
|
||||
enc, err := test.point.GridSquare()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if enc != test.loc {
|
||||
t.Fatalf("%s want %q, got %q\n", test.point, test.loc, enc)
|
||||
}
|
||||
//t.Logf("%s encoded to %q\n", test.point, enc)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtendedSquarePrecision(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
t.Run(test.loc4, func(t *testing.T) {
|
||||
got, err := test.point.Locator(ExtendedSquarePrecision)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != test.loc4 {
|
||||
t.Fatalf("%s want %q, got %q\n", test.point, test.loc4, got)
|
||||
}
|
||||
//t.Logf("%s encoded to %q\n", test.point, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// parsed locator must be translated to the same locator
|
||||
// using GridSquare()
|
||||
func TestParseLocator(t *testing.T) {
|
||||
var locTests = map[string]Point{
|
||||
"JN88RT": Point{48.791667, 17.416667},
|
||||
"JN89HF": Point{49.208333, 16.583333},
|
||||
"JN58TD": Point{48.125000, 11.583333},
|
||||
"GF15VC": Point{-34.916667, -56.250000},
|
||||
"FM18LW": Point{38.916667, -77.083333},
|
||||
"RE78IR": Point{-41.291667, 174.666667},
|
||||
"PM45lm": Point{35.5, 128.916667},
|
||||
}
|
||||
|
||||
for loc, p := range locTests {
|
||||
t.Run(loc, func(t *testing.T) {
|
||||
point, err := ParseLocator(loc)
|
||||
if err != nil {
|
||||
t.Fatalf("%s parsing error: %s", loc, err)
|
||||
}
|
||||
|
||||
l, err := point.GridSquare()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v to GridSquare(): %s", loc, point, err)
|
||||
}
|
||||
|
||||
if !strings.EqualFold(l, loc) {
|
||||
t.Errorf("%s: parsed to %v produces %s\n", loc, point, l)
|
||||
}
|
||||
|
||||
if !(almostEqual(p.Latitude, point.Latitude) && almostEqual(p.Longitude, point.Longitude)) {
|
||||
t.Errorf("%s: at %s, expeted %s", loc, point, p)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func almostEqual(a, b float64) bool {
|
||||
return math.Abs(a-b) < 1e-06
|
||||
}
|
||||
|
||||
// invalid Maiden Head locators must return error
|
||||
func TestParseInvalidLocatorStrict(t *testing.T) {
|
||||
locs := []string{
|
||||
"JN58td",
|
||||
"JN58TDAA",
|
||||
"JNH",
|
||||
"QN58jh",
|
||||
"JN77ya",
|
||||
" ",
|
||||
"JN55J",
|
||||
"JN89HA11aa2",
|
||||
"JN89HA11aa22",
|
||||
}
|
||||
|
||||
for _, l := range locs {
|
||||
t.Run(l, func(t *testing.T) {
|
||||
_, err := ParseLocatorStrict(l)
|
||||
if err == nil {
|
||||
t.Errorf("Parsing invalid locator '%s' with ParseLocatorStrict() doesn't return any error", l)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Distance between corner point and center of the locator square
|
||||
func TestParseLocatorCentered(t *testing.T) {
|
||||
tests := []struct {
|
||||
loc string
|
||||
distExpected float64
|
||||
}{
|
||||
{"JN89", 91.42870273454076},
|
||||
{"JN89HF", 3.8111046375990782},
|
||||
{"JN89HF23", 0.38109528459829756},
|
||||
{"JN89HF23ag", 0.015878904160500258},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.loc, func(t *testing.T) {
|
||||
p, _ := ParseLocator(test.loc)
|
||||
pc, _ := ParseLocatorCentered(test.loc)
|
||||
|
||||
dist := pc.Distance(p)
|
||||
|
||||
if !almostEqual(dist, test.distExpected) {
|
||||
t.Errorf("Distance between the center and corner of square locator '%s' is %g, expected %g",
|
||||
test.loc, dist, test.distExpected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
162
util/maidenhead/point.go
Normal file
162
util/maidenhead/point.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package maidenhead
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
const (
|
||||
// Earth radius
|
||||
r = 6371
|
||||
)
|
||||
|
||||
var compassBearing = []struct {
|
||||
label string
|
||||
start, ended float64
|
||||
}{
|
||||
{"N", 000.00, 011.25}, {"NNE", 011.25, 033.75}, {"NE", 033.75, 056.25}, {"ENE", 056.25, 078.75},
|
||||
{"E", 078.75, 101.25}, {"ESE", 101.25, 123.75}, {"SE", 123.75, 146.25}, {"SSE", 146.25, 168.75},
|
||||
{"S", 168.75, 191.25}, {"SSW", 191.25, 213.75}, {"SW", 213.75, 236.25}, {"WSW", 236.25, 258.75},
|
||||
{"W", 258.75, 281.25}, {"WNW", 281.25, 303.75}, {"NW", 303.75, 326.25}, {"NNW", 326.25, 348.75},
|
||||
{"N", 348.75, 360.00},
|
||||
}
|
||||
|
||||
// Point is a geographical point on the map.
|
||||
type Point struct {
|
||||
Latitude float64
|
||||
Longitude float64
|
||||
}
|
||||
|
||||
// NewPoint returns a new Point structure with given latitude and longitude.
|
||||
func NewPoint(latitude, longitude float64) Point {
|
||||
return Point{latitude, longitude}
|
||||
}
|
||||
|
||||
// ParseLocator parses a Maidenhead Locator with permissive rule matching.
|
||||
func ParseLocator(locator string) (Point, error) {
|
||||
return parseLocator(locator, false, false)
|
||||
}
|
||||
|
||||
// ParseLocatorStrict parses a Maidenhead Locator with strict rule matching.
|
||||
func ParseLocatorStrict(locator string) (Point, error) {
|
||||
return parseLocator(locator, true, false)
|
||||
}
|
||||
|
||||
// ParseLocatorCentered parses a Maidenhead Locator with permissive rule matching.
|
||||
// Returns Points structure with coordinates of the square center
|
||||
func ParseLocatorCentered(locator string) (Point, error) {
|
||||
return parseLocator(locator, false, true)
|
||||
}
|
||||
|
||||
// ParseLocatorStrictCentered parses a Maidenhead Locator with strict rule matching.
|
||||
// Returns Points structure with coordinates of the square center
|
||||
func ParseLocatorStrictCentered(locator string) (Point, error) {
|
||||
return parseLocator(locator, true, true)
|
||||
}
|
||||
|
||||
// EqualTo returns true if the coordinates point to the same geographical location.
|
||||
func (p Point) EqualTo(other Point) bool {
|
||||
var (
|
||||
dlat = p.Latitude - other.Latitude
|
||||
dlng = p.Longitude - other.Longitude
|
||||
)
|
||||
|
||||
for dlat < -180.0 {
|
||||
dlat += 360.0
|
||||
}
|
||||
for dlat > 180.0 {
|
||||
dlat -= 360.0
|
||||
}
|
||||
for dlng < -90.0 {
|
||||
dlng += 90.0
|
||||
}
|
||||
for dlng > 90.0 {
|
||||
dlng -= 90.0
|
||||
}
|
||||
|
||||
return dlat == 0.0 && dlng == 0.0
|
||||
}
|
||||
|
||||
// Bearing calculates the (approximate) bearing to another heading.
|
||||
func (p Point) Bearing(heading Point) float64 {
|
||||
var (
|
||||
hn = p.Latitude / 180 * math.Pi
|
||||
he = p.Longitude / 180 * math.Pi
|
||||
n = heading.Latitude / 180 * math.Pi
|
||||
e = heading.Longitude / 180 * math.Pi
|
||||
co = math.Cos(he-e)*math.Cos(hn)*math.Cos(n) + math.Sin(hn)*math.Sin(n)
|
||||
ca = math.Atan(math.Abs(math.Sqrt(1-co*co) / co))
|
||||
)
|
||||
|
||||
if co < 0.0 {
|
||||
ca = math.Pi - ca
|
||||
}
|
||||
|
||||
var si = math.Sin(e-he) * math.Cos(n) * math.Cos(hn)
|
||||
co = math.Sin(n) - math.Sin(hn)*math.Cos(ca)
|
||||
var az = math.Atan(math.Abs(si / co))
|
||||
if co < 0.0 {
|
||||
az = math.Pi - az
|
||||
}
|
||||
if si < 0.0 {
|
||||
az = -az
|
||||
}
|
||||
if az < 0.0 {
|
||||
az = az + 2.0*math.Pi
|
||||
}
|
||||
return az * 180 / math.Pi
|
||||
}
|
||||
|
||||
// CompassBearing returns the compass bearing to a heading.
|
||||
func (p Point) CompassBearing(heading Point) string {
|
||||
bearing := p.Bearing(heading)
|
||||
for bearing < 0.0 {
|
||||
bearing += 360.0
|
||||
}
|
||||
for bearing > 360.0 {
|
||||
bearing -= 360.0
|
||||
}
|
||||
|
||||
for _, compass := range compassBearing {
|
||||
if bearing >= compass.start && bearing <= compass.ended {
|
||||
return compass.label
|
||||
}
|
||||
}
|
||||
|
||||
// Should never reach
|
||||
return ""
|
||||
}
|
||||
|
||||
// Distance calculates the (approximate) distance to another point in km.
|
||||
func (p Point) Distance(other Point) float64 {
|
||||
var (
|
||||
hn = p.Latitude / 180 * math.Pi
|
||||
he = p.Longitude / 180 * math.Pi
|
||||
n = other.Latitude / 180 * math.Pi
|
||||
e = other.Longitude / 180 * math.Pi
|
||||
co = math.Cos(he-e)*math.Cos(hn)*math.Cos(n) + math.Sin(hn)*math.Sin(n)
|
||||
ca = math.Atan(math.Abs(math.Sqrt(1-co*co) / co))
|
||||
)
|
||||
|
||||
if co < 0.0 {
|
||||
ca = math.Pi - ca
|
||||
}
|
||||
|
||||
return r * ca
|
||||
}
|
||||
|
||||
// GridSquare returns a Maidenhead Locator for the point coordinates.
|
||||
func (p Point) GridSquare() (string, error) {
|
||||
return locator(p, SubSquarePrecision)
|
||||
}
|
||||
|
||||
// Locator returns a Maidenhead Locator for the point coordinates with
|
||||
// specified precision
|
||||
func (p Point) Locator(precision int) (string, error) {
|
||||
return locator(p, precision)
|
||||
}
|
||||
|
||||
// String returns a stringified Point structure.
|
||||
func (p Point) String() string {
|
||||
return fmt.Sprintf("Point(%f, %f)", p.Latitude, p.Longitude)
|
||||
}
|
||||
37
util/maidenhead/point_test.go
Normal file
37
util/maidenhead/point_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package maidenhead
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var pointTests = []struct {
|
||||
point Point
|
||||
bearing float64
|
||||
compass string
|
||||
}{
|
||||
{Point{48.14666, 11.60833}, 195, "SSW"},
|
||||
{Point{-34.91, -56.21166}, 69, "ENE"},
|
||||
{Point{38.92, -77.065}, 98, "E"},
|
||||
{Point{-41.28333, 174.745}, 187, "S"},
|
||||
{Point{41.714775, -72.727260}, 101, "ESE"},
|
||||
{Point{37.413708, -122.1073236}, 69, "ENE"},
|
||||
{Point{35.0542, -85.1142}, 92, "E"},
|
||||
}
|
||||
|
||||
func TestPointBearing(t *testing.T) {
|
||||
var center = NewPoint(0, 0)
|
||||
for _, test := range pointTests {
|
||||
t.Run("", func(t *testing.T) {
|
||||
bearing := math.Floor(test.point.Bearing(center))
|
||||
if bearing != test.bearing {
|
||||
t.Fatalf("%s -> %s, expected %0.f, got %0.f\n", test.point, center, test.bearing, bearing)
|
||||
}
|
||||
compass := test.point.CompassBearing(center)
|
||||
if compass != test.compass {
|
||||
t.Logf("%s -> %s, expected %q, got %q\n", test.point, center, test.compass, compass)
|
||||
}
|
||||
//t.Logf("%s -> %s, bearing %.0f %s\n", test.point, center, bearing, compass)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user