- 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
127 lines
3.5 KiB
Go
127 lines
3.5 KiB
Go
// Package route defines the Router interface and the two backends:
|
|
// a hosted Valhalla (works today, no geometry) and local OSRM
|
|
// (full geometry, self-hosted — see scripts/setup-osrm.sh).
|
|
//
|
|
// Design rule (DESIGN.md R1/R4): the route is computed, never narrated.
|
|
// Every Duration/Distance value returned here is `computed` provenance.
|
|
package route
|
|
|
|
import (
|
|
"context"
|
|
"math"
|
|
"time"
|
|
|
|
"maps/router/internal/geo"
|
|
)
|
|
|
|
// Profile is the routing profile.
|
|
type Profile string
|
|
|
|
const (
|
|
ProfileDrive Profile = "drive"
|
|
ProfileWalk Profile = "walk"
|
|
)
|
|
|
|
// Route is the result of routing through an ordered set of points.
|
|
type Route struct {
|
|
// Duration is total travel time in seconds (computed by the router).
|
|
Duration float64
|
|
// Distance is total path length in meters.
|
|
Distance float64
|
|
// BBox is the bounding box of the routed path (from the router's
|
|
// summary). Zero values if the backend doesn't provide one.
|
|
BBox BBox
|
|
// Geometry is the routed polyline. May be nil for backends that
|
|
// don't return it (hosted Valhalla) — callers must degrade
|
|
// gracefully (chord corridor, flagged low-confidence).
|
|
Geometry []geo.Point
|
|
// Legs holds per-segment durations for multi-point routes.
|
|
// len(Legs) == len(points)-1 when present.
|
|
Legs []Leg
|
|
}
|
|
|
|
// Leg is one segment between consecutive waypoints.
|
|
type Leg struct {
|
|
Duration float64 // seconds
|
|
Distance float64 // meters
|
|
}
|
|
|
|
// BBox is a latitude/longitude bounding box in degrees.
|
|
type BBox struct {
|
|
MinLat, MaxLat, MinLon, MaxLon float64
|
|
}
|
|
|
|
// Empty reports whether the box is unset.
|
|
func (b BBox) Empty() bool { return b == BBox{} }
|
|
|
|
// DistMeters is the minimum distance from p to the box (0 if inside).
|
|
func (b BBox) DistMeters(p geo.Point) float64 {
|
|
dLat, dLon := 0.0, 0.0
|
|
if p.Lat < b.MinLat {
|
|
dLat = b.MinLat - p.Lat
|
|
} else if p.Lat > b.MaxLat {
|
|
dLat = p.Lat - b.MaxLat
|
|
}
|
|
if p.Lon < b.MinLon {
|
|
dLon = b.MinLon - p.Lon
|
|
} else if p.Lon > b.MaxLon {
|
|
dLon = p.Lon - b.MaxLon
|
|
}
|
|
if dLat == 0 && dLon == 0 {
|
|
return 0
|
|
}
|
|
mLat := dLat * 111190.0
|
|
mLon := dLon * 111190.0 * math.Cos(rad(p.Lat))
|
|
return math.Hypot(mLat, mLon)
|
|
}
|
|
|
|
func rad(d float64) float64 { return d * math.Pi / 180 }
|
|
|
|
// Router is the backend interface.
|
|
type Router interface {
|
|
// Route routes through pts in order and returns the combined trip.
|
|
Route(ctx context.Context, profile Profile, pts []geo.Point) (*Route, error)
|
|
// Name identifies the backend in reports ("valhalla-hosted", "osrm-local").
|
|
Name() string
|
|
}
|
|
|
|
// Matrix builds a full origin-destination matrix over pts using the
|
|
// router. m[i][j] is seconds; -1 means unroutable. Built with pairwise
|
|
// two-point Route calls — the single code path that works on both
|
|
// backends (Valhalla's sources_to_targets is capped at 150 km).
|
|
type Matrix [][]float64
|
|
|
|
func (m Matrix) From(i, j int) float64 { return m[i][j] }
|
|
|
|
// BuildMatrix computes the matrix with a small politeness delay between
|
|
// calls when polite > 0 (hosted endpoints).
|
|
func BuildMatrix(ctx context.Context, r Router, profile Profile, pts []geo.Point, polite time.Duration) (Matrix, error) {
|
|
n := len(pts)
|
|
m := make(Matrix, n)
|
|
for i := range m {
|
|
m[i] = make([]float64, n)
|
|
m[i][i] = 0
|
|
}
|
|
for i := 0; i < n; i++ {
|
|
for j := 0; j < n; j++ {
|
|
if i == j {
|
|
continue
|
|
}
|
|
if polite > 0 {
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
case <-time.After(polite):
|
|
}
|
|
}
|
|
rr, err := r.Route(ctx, profile, []geo.Point{pts[i], pts[j]})
|
|
if err != nil {
|
|
m[i][j] = -1
|
|
continue
|
|
}
|
|
m[i][j] = rr.Duration
|
|
}
|
|
}
|
|
return m, nil
|
|
}
|