- DESIGN.md: full design incl. driving-trip requirements (R1-R4), stays model, focus mode, mobile, provenance rules - SURVEY.md: open-source landscape - mock/: interaction mock (Florence itinerary, focus mode, stays, region stops, mobile layout) - router/: Go module (stdlib-only) with Router interface (Valhalla + OSRM backends), stop_cost, optimize_stops, corridor, routectl CLI, bench (5 real NE-corridor tasks, 26 checks passing), integration tests, and setup-osrm.sh for the self-hosted router - osm/: NH+MA+CT+NY PBFs (gitignored) + setup artifacts
71 lines
1.8 KiB
Go
71 lines
1.8 KiB
Go
// 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
|
|
}
|