tailscale/types/geo/quantize.go
Simon Law 93511be044
types/geo: add geo.Point and its associated units (#16583)
Package geo provides functionality to represent and process
geographical locations on a sphere. The main type, geo.Point,
represents a pair of latitude and longitude coordinates.

Updates tailscale/corp#29968

Signed-off-by: Simon Law <sfllaw@tailscale.com>
2025-07-17 01:30:08 -07:00

107 lines
3.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package geo
import (
"math"
"sync"
)
// MinSeparation is the minimum separation between two points after quantizing.
// [Point.Quantize] guarantees that two points will either be snapped to exactly
// the same point, which conflates multiple positions together, or that the two
// points will be far enough apart that successfully performing most reverse
// lookups would be highly improbable.
const MinSeparation = 50_000 * Meter
// Latitude
var (
// numSepsEquatorToPole is the number of separations between a point on
// the equator to a point on a pole, that satisfies [minPointSep]. In
// other words, the number of separations between 0° and +90° degrees
// latitude.
numSepsEquatorToPole = int(math.Floor(float64(
earthPolarCircumference / MinSeparation / 4)))
// latSep is the number of degrees between two adjacent latitudinal
// points. In other words, the next point going straight north of
// 0° would be latSep°.
latSep = Degrees(90.0 / float64(numSepsEquatorToPole))
)
// snapToLat returns the number of the nearest latitudinal separation to
// lat. A positive result is north of the equator, a negative result is south,
// and zero is the equator itself. For example, a result of -1 would mean a
// point that is [latSep] south of the equator.
func snapToLat(lat Degrees) int {
return int(math.Round(float64(lat / latSep)))
}
// lngSep is a lookup table for the number of degrees between two adjacent
// longitudinal separations. where the index corresponds to the absolute value
// of the latitude separation. The first value corresponds to the equator and
// the last value corresponds to the separation before the pole. There is no
// value for the pole itself, because longitude has no meaning there.
//
// [lngSep] is calculated on init, which is so quick and will be used so often
// that the startup cost is negligible.
var lngSep = sync.OnceValue(func() []Degrees {
lut := make([]Degrees, numSepsEquatorToPole)
// i ranges from the equator to a pole
for i := range len(lut) {
// lat ranges from [0°, 90°], because the southern hemisphere is
// a reflection of the northern one.
lat := Degrees(i) * latSep
ratio := math.Cos(float64(lat.Radians()))
circ := Distance(ratio) * earthEquatorialCircumference
num := int(math.Floor(float64(circ / MinSeparation)))
// We define lut[0] as 0°, lut[len(lut)] to be the north pole,
// which means -lut[len(lut)] is the south pole.
lut[i] = Degrees(360.0 / float64(num))
}
return lut
})
// snapToLatLng returns the number of the nearest latitudinal separation to lat,
// and the nearest longitudinal separation to lng.
func snapToLatLng(lat, lng Degrees) (Degrees, Degrees) {
latN := snapToLat(lat)
// absolute index into lngSep
n := latN
if n < 0 {
n = -latN
}
lngSep := lngSep()
if n < len(lngSep) {
sep := lngSep[n]
lngN := int(math.Round(float64(lng / sep)))
return Degrees(latN) * latSep, Degrees(lngN) * sep
}
if latN < 0 { // south pole
return -90 * Degree, 0 * Degree
} else { // north pole
return +90 * Degree, 0 * Degree
}
}
// Quantize returns a new [Point] after throwing away enough location data in p
// so that it would be difficult to distinguish a node among all the other nodes
// in its general vicinity. One caveat is that if theres only one point in an
// obscure location, someone could triangulate the node using additional data.
//
// This method is stable: given the same p, it will always return the same
// result. It is equivalent to snapping to points on Earth that are at least
// [MinSeparation] apart.
func (p Point) Quantize() Point {
if p.IsZero() {
return p
}
lat, lng := snapToLatLng(p.lat, p.lng180-180)
return MakePoint(lat, lng)
}