trails/openspec/changes/multi-day-routes/design.md
Ullrich Schäfer 0a330e4466
Break up route-features into focused specs, add new changes
Archive the monolithic route-features spec and replace with 9 focused
OpenSpec changes: multi-day-routes, waypoint-notes (with POI snapping),
undo-redo, local-dev-stack, route-sharing, route-discovery,
activity-photos, osm-overlays, plus the existing changelog and
komoot-import.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 09:53:41 +02:00

211 lines
8.1 KiB
Markdown

## Context
The Planner stores waypoints as a Yjs `Y.Array<Y.Map<unknown>>` where each
Y.Map holds `lat`, `lon`, and optionally `name`. Routes are computed
segment-by-segment between consecutive waypoints via BRouter, producing an
`EnrichedRoute` with `coordinates`, `segmentBoundaries`, `totalLength`,
`totalAscend`, and `totalTime`. The visual-redesign change already defines the
UI treatment for multi-day routes (sidebar day breakdown, elevation chart
dividers, map day labels) but explicitly defers the data model and logic.
This design covers the data model, computation, and integration decisions.
## Decisions
### D1: Day-break waypoints via `overnight` flag
A waypoint becomes a day boundary by setting `overnight: true` on its Y.Map.
The first waypoint of the route is the implicit start of Day 1, the last
waypoint is the implicit end of the final day, and every waypoint with
`overnight: true` marks the end of one day and start of the next.
```
Waypoints: [Berlin, Zossen, Dessau(overnight), Halle, Erfurt]
|------- Day 1 -------||------- Day 2 ------|
```
This is the simplest possible model: a single boolean on existing data. No new
Yjs types, no separate array, no ordering concerns. It composes naturally with
waypoint reordering, insertion, and deletion -- if an overnight waypoint is
removed, the two days merge automatically.
### D2: Day computation as a pure utility
A `computeDays()` function takes the waypoints array and the `EnrichedRoute`
and returns an array of day objects:
```typescript
interface DayStage {
dayNumber: number;
startWaypointIndex: number;
endWaypointIndex: number;
startName: string;
endName: string;
distance: number; // meters
ascent: number; // meters
descent: number; // meters
estimatedTime: number; // seconds
coordStartIndex: number;
coordEndIndex: number;
}
function computeDays(
waypoints: Array<{ lat: number; lon: number; name?: string; overnight?: boolean }>,
route: EnrichedRoute,
): DayStage[];
```
The function walks `segmentBoundaries` to map waypoint indices to coordinate
ranges, then accumulates distance and elevation per day by iterating
coordinates within each range. If no waypoints have `overnight: true`, it
returns a single day covering the entire route.
This is a pure function with no Yjs dependency -- it takes plain data and
returns plain data. This makes it easy to test and reuse.
### D3: Yjs state -- minimal addition
The only change to Yjs state is adding an `overnight` key to waypoint Y.Maps:
```typescript
// Setting overnight on a waypoint
const waypointMap = yjs.waypoints.get(index);
waypointMap.set("overnight", true);
// Clearing overnight
waypointMap.delete("overnight");
```
No new Y.Array or Y.Map types are introduced. The `overnight` key is optional
-- existing waypoints without it are treated as regular (non-overnight)
waypoints. This is fully backwards-compatible: sessions created before this
feature work identically, and clients that don't understand `overnight` simply
ignore it.
The existing crash-recovery logic (periodic localStorage save of Y.Doc state)
preserves overnight flags automatically since they are part of the Y.Doc.
### D4: Sidebar day breakdown
The `WaypointSidebar` component gains a day-grouped view when any waypoint has
`overnight: true`. The layout follows visual-redesign D4:
```
ACTIVE ROUTE
Berlin -> Erfurt 343 km ^868m 2 days
DAY 1 - Berlin -> Dessau [v]
^340m 120 km ~4h 30m
1. Berlin Alexanderplatz
2. Zossen
3. Juterbog
4. Dessau [OVERNIGHT]
> DAY 2 - Dessau -> Erfurt 223 km [>]
(collapsed days show summary only)
```
**Behavior:**
- Day 1 is expanded by default; other days are collapsed
- Clicking a day header toggles expand/collapse
- Per-day stats (distance, ascent, estimated time) shown in day header
- Overnight waypoints display an amber badge
- Waypoints within each day are numbered sequentially (1, 2, 3... restarting
per day would be confusing -- use global numbering)
- When no waypoints have `overnight: true`, the sidebar shows the flat list
as it does today (no "Day 1" wrapper for single-day routes)
### D5: Elevation chart day dividers
The `ElevationChart` canvas drawing is extended to render day boundaries as
vertical dashed lines. For each day boundary (overnight waypoint), the
corresponding distance along the route is computed from `segmentBoundaries`
and `coordinates`, and a dashed vertical line is drawn at that x-position.
```
Day 1 Day 2 Day 3
___/\__/\___ : __/\_____ : __/\___
/ \ : / \ : / \
/_______________\:/___________\:/________\
0 km 120 km 120 km 250 km 250 km 343 km
```
Each divider has a "Day N" label at the top of the chart area. The dashed line
uses a muted color (`--text-lo` / `#9A9484`) to avoid competing with the
elevation profile. This follows visual-redesign D6.
### D6: Map day labels
White pill-shaped labels are placed on the route at each day boundary, showing
"Day 1 . 120 km". These use Leaflet DivIcon markers positioned at the
coordinate of the overnight waypoint.
Styling follows visual-redesign D5:
- White background with subtle shadow (`--shadow-sm`)
- Text in `--text-hi` with distance in `--font-mono`
- Positioned slightly offset from the route line to avoid overlap with the
route itself
Day labels are only shown when there are 2+ days. They update reactively when
overnight flags change (same Yjs observe pattern as existing markers).
### D7: GPX export with day-break metadata
Day-break waypoints are exported with a `<type>overnight</type>` element inside
the `<wpt>` tag. This is valid GPX 1.1 (the `<type>` element is a standard
child of `<wpt>`).
```xml
<wpt lat="51.8365" lon="12.2428">
<name>Dessau</name>
<type>overnight</type>
</wpt>
```
Additionally, the track can optionally be split into multiple `<trk>` elements
(one per day), each with a `<name>` like "Day 1: Berlin - Dessau". This gives
GPS devices and other tools a natural per-day breakdown. The single-track export
remains the default; multi-track is an option in the export dialog.
On GPX import (future), the parser should recognize `<type>overnight</type>`
waypoints and restore the overnight flags. This is not in scope for this change
but the format is designed to support it.
### D8: Overnight toggle UX
Two interaction paths to toggle a waypoint as overnight:
1. **Sidebar**: Each waypoint row in the sidebar gains an overnight toggle
button (crescent moon icon). Clicking it sets/clears `overnight` on the
waypoint's Y.Map. The button uses amber styling (`--stop`, `--stop-bg`)
when active.
2. **Map context menu**: Right-clicking (long-press on mobile) a waypoint
marker on the map shows a context menu with "Mark as overnight stop" /
"Remove overnight stop". This reuses the same Y.Map mutation.
Visual feedback follows visual-redesign tokens:
- Overnight waypoint markers on the map use amber-brown (`--stop`: `#8B6D3A`)
instead of the default olive (`--accent`: `#4A6B40`)
- Sidebar overnight waypoints have a subtle amber background (`--stop-bg`)
- The "OVERNIGHT" badge uses `--stop` text on `--stop-bg` background with
`--stop-border` border
## Risks / Trade-offs
- **Segment boundary alignment**: The day computation relies on
`segmentBoundaries` from `EnrichedRoute` to map waypoint indices to
coordinate ranges. If the segment merge logic changes, day computation
breaks. Mitigate with thorough unit tests on `computeDays`.
- **Large routes**: A route with 50+ waypoints and many overnight stops could
make the sidebar unwieldy. Collapsible sections mitigate this. We can add
virtual scrolling later if needed.
- **Yjs backwards compatibility**: Adding `overnight` to Y.Maps is safe, but
older clients that don't understand it will silently ignore overnight flags.
In a collaborative session, one user could see day breakdown while another
does not. This is acceptable for now since all clients will be updated
together.
- **GPX round-trip**: The `<type>overnight</type>` convention is not a standard
GPX extension namespace. Other tools will ignore it, which is fine. The data
is not lost, just not interpreted.