- 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
111 lines
2.8 KiB
Go
111 lines
2.8 KiB
Go
// Package plan implements the route-aware planning primitives from
|
||
// DESIGN.md "Driving trips": stop_cost, optimize_stops, and the
|
||
// corridor. All numbers here are router arithmetic (computed
|
||
// provenance) — the LLM is never allowed to substitute estimates.
|
||
//
|
||
// Matrix convention used throughout: for n stops the matrix has n+2
|
||
// rows/cols: indices 0..n-1 are the stops (in slice order), index n is
|
||
// the origin A, index n+1 is the destination C.
|
||
package plan
|
||
|
||
import (
|
||
"maps/router/internal/geo"
|
||
"maps/router/internal/route"
|
||
)
|
||
|
||
// Stop is a candidate stop with a location and a dwell budget.
|
||
type Stop struct {
|
||
ID string
|
||
Name string
|
||
At geo.Point
|
||
DwellMin int // expected time spent at the stop (user-set or sourced)
|
||
OverheadMin int // park/walk-to-entrance/leave; WithDefaults fills 10
|
||
}
|
||
|
||
// WithDefaults applies the DESIGN.md default overhead (10 min) when unset.
|
||
func (s Stop) WithDefaults() Stop {
|
||
if s.OverheadMin == 0 {
|
||
s.OverheadMin = 10
|
||
}
|
||
return s
|
||
}
|
||
|
||
// Cost is the stop_cost result: pure router arithmetic.
|
||
//
|
||
// detour = route(A,C via B) − route(A,C)
|
||
// total = detour + dwell + overhead
|
||
//
|
||
// A stop is "free" along the corridor iff detour ≈ 0.
|
||
type Cost struct {
|
||
Stop Stop
|
||
DetourMin int // extra travel time vs direct A→C, minutes
|
||
DwellMin int
|
||
OverheadMin int
|
||
TotalMin int // DetourMin + DwellMin + OverheadMin
|
||
DirectMin int // direct A→C duration in minutes (context)
|
||
}
|
||
|
||
// StopCost computes the cost of inserting one stop between A and C.
|
||
// m is the (n+2)-layout matrix; i is the stop's index.
|
||
func StopCost(m route.Matrix, i int, s Stop) Cost {
|
||
s = s.WithDefaults()
|
||
n := len(m) - 2
|
||
aIdx, cIdx := n, n+1
|
||
direct := m[aIdx][cIdx]
|
||
via := m[aIdx][i] + m[i][cIdx]
|
||
detour := 0.0
|
||
if direct > 0 && via > 0 {
|
||
detour = via - direct
|
||
if detour < 0 && detour > -30 { // router noise, seconds
|
||
detour = 0
|
||
}
|
||
}
|
||
dm := int(0.5 + detour/60)
|
||
if dm < 0 {
|
||
dm = 0
|
||
}
|
||
return Cost{
|
||
Stop: s,
|
||
DetourMin: dm,
|
||
DwellMin: s.DwellMin,
|
||
OverheadMin: s.OverheadMin,
|
||
TotalMin: dm + s.DwellMin + s.OverheadMin,
|
||
DirectMin: int(0.5 + direct/60),
|
||
}
|
||
}
|
||
|
||
// SeqCost computes (detourMin, totalMin) for an ordered subset of stops
|
||
// between A and C. Returns (-1, -1) when any leg is unroutable.
|
||
func SeqCost(m route.Matrix, order []int, stops []Stop) (detourMin, totalMin int) {
|
||
n := len(m) - 2
|
||
aIdx, cIdx := n, n+1
|
||
direct := m[aIdx][cIdx]
|
||
if direct < 0 {
|
||
return -1, -1
|
||
}
|
||
via := 0.0
|
||
prev := aIdx
|
||
for _, si := range order {
|
||
d := m[prev][si]
|
||
if d < 0 {
|
||
return -1, -1
|
||
}
|
||
via += d
|
||
prev = si
|
||
}
|
||
if d := m[prev][cIdx]; d < 0 {
|
||
return -1, -1
|
||
}
|
||
via += m[prev][cIdx]
|
||
detour := int(0.5 + (via-direct)/60)
|
||
if detour < 0 {
|
||
detour = 0
|
||
}
|
||
total := detour
|
||
for _, si := range order {
|
||
s := stops[si].WithDefaults()
|
||
total += s.DwellMin + s.OverheadMin
|
||
}
|
||
return detour, total
|
||
}
|