Nothing computes a distance when a QSO is logged. The DISTANCE field is only ever filled by an ADIF import that carried one, so publishing it straight gave an empty column to anyone whose log was made in OpsLog — which is everyone who reported it. It falls back to the two locators, which are on the QSO already. A stored distance still wins: it came from the log that recorded the contact, which knew more than two four-character squares do. No grids means an empty cell, not a zero — an empty cell is honest, a zero is a claim. Rounded to whole kilometres. The squares are tens of kilometres across and a decimal would assert an accuracy nobody has. The geometry moved to internal/geo on the way. It lived in package main, which internal packages cannot import, so the PSK Reporter watcher already had its maths injected from main and this would have been a third copy. A bearing that disagrees with itself between two panels is a fault nobody reports, because each screen looks perfectly plausible on its own.
70 lines
2.1 KiB
Go
70 lines
2.1 KiB
Go
// Package geo is the one place that turns Maidenhead locators into positions,
|
|
// and positions into distances and bearings.
|
|
//
|
|
// It exists because there were about to be three copies. These functions lived
|
|
// in package main, which the internal packages cannot import, so the PSK
|
|
// Reporter watcher had its geometry injected from main and the web publisher
|
|
// was about to grow its own. A bearing that disagrees with itself between two
|
|
// panels is the kind of fault nobody reports, because each screen looks
|
|
// plausible on its own.
|
|
package geo
|
|
|
|
import (
|
|
"math"
|
|
"strings"
|
|
)
|
|
|
|
// GridToLatLon parses a Maidenhead locator (4 or 6 characters) and returns the
|
|
// centre of that square in degrees. ok=false on malformed input.
|
|
func GridToLatLon(grid string) (lat, lon float64, ok bool) {
|
|
g := strings.ToUpper(strings.TrimSpace(grid))
|
|
if len(g) < 4 {
|
|
return 0, 0, false
|
|
}
|
|
A := g[0] - 'A'
|
|
B := g[1] - 'A'
|
|
C := g[2] - '0'
|
|
D := g[3] - '0'
|
|
if A > 17 || B > 17 || C > 9 || D > 9 {
|
|
return 0, 0, false
|
|
}
|
|
lon = -180 + float64(A)*20 + float64(C)*2
|
|
lat = -90 + float64(B)*10 + float64(D)*1
|
|
if len(g) >= 6 {
|
|
E := g[4] - 'A'
|
|
F := g[5] - 'A'
|
|
if E <= 23 && F <= 23 {
|
|
lon += float64(E)*(5.0/60.0) + 2.5/60.0
|
|
lat += float64(F)*(2.5/60.0) + 1.25/60.0
|
|
return lat, lon, true
|
|
}
|
|
}
|
|
// 4-character locator: aim at the centre of the square.
|
|
lon += 1
|
|
lat += 0.5
|
|
return lat, lon, true
|
|
}
|
|
|
|
// HaversineKm returns the great-circle distance between two positions in
|
|
// kilometres. Mean Earth radius 6371 km.
|
|
func HaversineKm(lat1, lon1, lat2, lon2 float64) float64 {
|
|
const R = 6371.0
|
|
rad := math.Pi / 180.0
|
|
dLat := (lat2 - lat1) * rad
|
|
dLon := (lon2 - lon1) * rad
|
|
a := math.Sin(dLat/2)*math.Sin(dLat/2) +
|
|
math.Cos(lat1*rad)*math.Cos(lat2*rad)*math.Sin(dLon/2)*math.Sin(dLon/2)
|
|
return R * 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
|
|
}
|
|
|
|
// DistanceBetweenGrids is the distance in kilometres between two locators,
|
|
// ok=false when either cannot be parsed.
|
|
func DistanceBetweenGrids(a, b string) (km float64, ok bool) {
|
|
lat1, lon1, ok1 := GridToLatLon(a)
|
|
lat2, lon2, ok2 := GridToLatLon(b)
|
|
if !ok1 || !ok2 {
|
|
return 0, false
|
|
}
|
|
return HaversineKm(lat1, lon1, lat2, lon2), true
|
|
}
|