trips/router/internal/plan/stopcost.go
Greg Pomerantz efde2cc71b maps project: design, survey, mock app, and route-aware planning backend
- 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
2026-09-06 00:05:17 -04:00

111 lines
2.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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