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>
8.1 KiB
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:
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:
// 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-hiwith 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>).
<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:
-
Sidebar: Each waypoint row in the sidebar gains an overnight toggle button (crescent moon icon). Clicking it sets/clears
overnighton the waypoint's Y.Map. The button uses amber styling (--stop,--stop-bg) when active. -
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
--stoptext on--stop-bgbackground with--stop-borderborder
Risks / Trade-offs
- Segment boundary alignment: The day computation relies on
segmentBoundariesfromEnrichedRouteto map waypoint indices to coordinate ranges. If the segment merge logic changes, day computation breaks. Mitigate with thorough unit tests oncomputeDays. - 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
overnightto 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.