initial commit
This commit is contained in:
commit
1253955070
75 changed files with 13972 additions and 0 deletions
92
api/internal/geo/geo.go
Normal file
92
api/internal/geo/geo.go
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
// Package geo provides the geographic distance calculations used to decide
|
||||
// whether two pictures were taken at the same place.
|
||||
package geo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
|
||||
"go-places-api/internal/domain"
|
||||
)
|
||||
|
||||
// EarthRadiusMeters is the mean radius of the WGS-84 ellipsoid.
|
||||
const EarthRadiusMeters = 6371008.8
|
||||
|
||||
var ErrInvalidCoordinate = errors.New("invalid coordinate")
|
||||
|
||||
// DistanceMeters returns the great-circle distance between two points using the
|
||||
// haversine formula. It is accurate to roughly 0.5% which is far finer than the
|
||||
// radii this application compares against.
|
||||
func DistanceMeters(a, b domain.Coordinate) float64 {
|
||||
lat1 := degreesToRadians(a.Latitude)
|
||||
lat2 := degreesToRadians(b.Latitude)
|
||||
dLat := lat2 - lat1
|
||||
dLon := degreesToRadians(b.Longitude - a.Longitude)
|
||||
|
||||
sinLat := math.Sin(dLat / 2)
|
||||
sinLon := math.Sin(dLon / 2)
|
||||
|
||||
h := sinLat*sinLat + math.Cos(lat1)*math.Cos(lat2)*sinLon*sinLon
|
||||
return 2 * EarthRadiusMeters * math.Asin(math.Sqrt(math.Min(1, h)))
|
||||
}
|
||||
|
||||
// WithinRadius reports whether two points are no further apart than radiusMeters.
|
||||
func WithinRadius(a, b domain.Coordinate, radiusMeters float64) bool {
|
||||
return DistanceMeters(a, b) <= radiusMeters
|
||||
}
|
||||
|
||||
// BoundingBox is a coarse lat/lon window that fully contains a circle of the
|
||||
// given radius around a centre. It exists to let the store pre-filter rows in
|
||||
// SQL before the exact haversine test is applied.
|
||||
type BoundingBox struct {
|
||||
MinLat float64
|
||||
MaxLat float64
|
||||
MinLon float64
|
||||
MaxLon float64
|
||||
}
|
||||
|
||||
// Around returns the bounding box of a circle of radiusMeters around center.
|
||||
func Around(center domain.Coordinate, radiusMeters float64) BoundingBox {
|
||||
// Latitude degrees are a constant distance apart, so the latitude delta is
|
||||
// a simple division. Longitude degrees shrink towards the poles, so the
|
||||
// delta is scaled by 1/cos(latitude) and clamped near the poles.
|
||||
latDelta := radiusMeters / EarthRadiusMeters * 180 / math.Pi
|
||||
|
||||
cos := math.Cos(degreesToRadians(center.Latitude))
|
||||
var lonDelta float64
|
||||
if math.Abs(cos) > 1e-9 {
|
||||
lonDelta = latDelta / math.Abs(cos)
|
||||
} else {
|
||||
// At the poles every longitude is the same meridian; use everything.
|
||||
lonDelta = 180
|
||||
}
|
||||
// Clamp so the box stays within valid longitude range.
|
||||
if lonDelta > 180 {
|
||||
lonDelta = 180
|
||||
}
|
||||
|
||||
return BoundingBox{
|
||||
MinLat: clamp(center.Latitude-latDelta, -90, 90),
|
||||
MaxLat: clamp(center.Latitude+latDelta, -90, 90),
|
||||
MinLon: clamp(center.Longitude-lonDelta, -180, 180),
|
||||
MaxLon: clamp(center.Longitude+lonDelta, -180, 180),
|
||||
}
|
||||
}
|
||||
|
||||
// Contains reports whether the point falls inside the box.
|
||||
func (b BoundingBox) Contains(c domain.Coordinate) bool {
|
||||
return c.Latitude >= b.MinLat && c.Latitude <= b.MaxLat &&
|
||||
c.Longitude >= b.MinLon && c.Longitude <= b.MaxLon
|
||||
}
|
||||
|
||||
// Round rounds a distance to the nearest metre so that JSON output and equality
|
||||
// assertions are stable.
|
||||
func Round(meters float64) float64 {
|
||||
return math.Round(meters)
|
||||
}
|
||||
|
||||
func degreesToRadians(deg float64) float64 { return deg * math.Pi / 180 }
|
||||
|
||||
func clamp(v, lo, hi float64) float64 {
|
||||
return math.Max(lo, math.Min(hi, v))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue