// Package geo holds the small, dependency-free geodesy used by the // router/plan layers. Kept tiny on purpose: anything heavier belongs // in the router backend, not here. package geo import "math" // Point is a lat/lon in degrees. type Point struct { Lat float64 `json:"lat"` Lon float64 `json:"lon"` } const R = 6371000.0 // meters func rad(d float64) float64 { return d * math.Pi / 180 } // Meters is great-circle distance in meters. func Meters(a, b Point) float64 { dLat := rad(b.Lat - a.Lat) dLon := rad(b.Lon - a.Lon) q := math.Sin(dLat/2)*math.Sin(dLat/2) + math.Cos(rad(a.Lat))*math.Cos(rad(b.Lat))*math.Sin(dLon/2)*math.Sin(dLon/2) return 2 * R * math.Asin(math.Sqrt(q)) } // MinAt walking minutes at the given km/h, rounded up to whole minutes. func MinAt(meters float64, kmh float64) int { if meters <= 0 { return 0 } return int(math.Ceil(meters / 1000.0 / (kmh / 60.0))) } // distToSeg is the planar (equirect, local) distance from p to segment ab, in meters. // Accurate to <0.1% over the extents we use (tens of km). func distToSeg(p, a, b Point) float64 { x := func(q Point) [2]float64 { lat0 := rad(a.Lat) return [2]float64{rad(q.Lon) * math.Cos(lat0) * R, rad(q.Lat) * R} } px, ax, bx := x(p), x(a), x(b) dx, dy := bx[0]-ax[0], bx[1]-ax[1] l2 := dx*dx + dy*dy t := 0.0 if l2 > 0 { t = ((px[0]-ax[0])*dx + (px[1]-ax[1])*dy) / l2 if t < 0 { t = 0 } else if t > 1 { t = 1 } } cx, cy := ax[0]+t*dx, ax[1]+t*dy return math.Hypot(px[0]-cx, px[1]-cy) } // DistToPolylineMeters is the minimum distance from p to any segment of the polyline. func DistToPolylineMeters(p Point, poly []Point) float64 { best := float64(1e18) for i := 0; i+1 < len(poly); i++ { if d := distToSeg(p, poly[i], poly[i+1]); d < best { best = d } } if len(poly) == 1 { best = Meters(p, poly[0]) } return best }