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