tailscale/types/geo/quantize.go

107 lines
3.7 KiB
Go
Raw Permalink Normal View History

// 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)
}