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>
211 lines
8.1 KiB
Markdown
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.
|