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