diff --git a/openspec/changes/multi-day-routes/design.md b/openspec/changes/multi-day-routes/design.md index 2ad3e7b..48aad3b 100644 --- a/openspec/changes/multi-day-routes/design.md +++ b/openspec/changes/multi-day-routes/design.md @@ -192,6 +192,94 @@ Visual feedback follows visual-redesign tokens: - The "OVERNIGHT" badge uses `--stop` text on `--stop-bg` background with `--stop-border` border +### D9: GPX parse recognizes overnight waypoints + +The `parseWaypoints` function in `packages/gpx/src/parse.ts` is extended to +check for `overnight` inside `` elements. When found, the +returned `Waypoint` has `isDayBreak: true`. + +```typescript +function parseWaypoints(doc: Document): Waypoint[] { + const wpts = doc.querySelectorAll("wpt"); + return Array.from(wpts).map((wpt) => { + const lat = parseFloat(wpt.getAttribute("lat") ?? "0"); + const lon = parseFloat(wpt.getAttribute("lon") ?? "0"); + const name = wpt.querySelector("name")?.textContent ?? undefined; + const type = wpt.querySelector("type")?.textContent ?? undefined; + const isDayBreak = type === "overnight" ? true : undefined; + return { lat, lon, name, isDayBreak }; + }); +} +``` + +This makes GPX round-tripping work: Planner exports overnight waypoints with +`overnight` (D7), and any consumer — the Journal's `updateRoute`, +the Planner's GPX import, or a future mobile app — gets `isDayBreak` back. + +### D10: Journal stores dayBreaks on route save + +The `dayBreaks` jsonb column on `journal.routes` already exists. When +`updateRoute` receives GPX and calls `parseGpxAsync`, it extracts the indices +of waypoints where `isDayBreak === true` and writes them to `dayBreaks`: + +```typescript +const parsed = await parseGpxAsync(gpx); +const dayBreaks = parsed.waypoints + .map((w, i) => (w.isDayBreak ? i : -1)) + .filter((i) => i >= 0); +``` + +This is computed on write, not on read — the Journal doesn't need to re-parse +GPX to know day structure. If the GPX has no overnight waypoints, `dayBreaks` +is an empty array (single-day route). + +### D11: Journal route detail day breakdown + +When a route has non-empty `dayBreaks`, the route detail page shows a day +breakdown section below the stats grid. Per-day stats (distance, ascent, +descent) are computed from the stored GPX by splitting at day-break waypoints +— reusing the same `computeDays` logic from D2, extracted into +`@trails-cool/gpx` so both Planner and Journal can use it. + +``` +DAY 1: Berlin → Dessau 120 km ↑340m ↓180m +DAY 2: Dessau → Erfurt 223 km ↑528m ↓490m +``` + +The map also colors each day segment differently (alternating two colors from +the design tokens) so users can visually see where each day starts and ends. + +For single-day routes (no `dayBreaks`), nothing changes — the page renders +exactly as it does today. + +### D12: computeDays as a shared package function + +The `computeDays` pure function (D2) is placed in `@trails-cool/gpx` rather +than in the Planner app, since both the Planner (from Yjs + EnrichedRoute) and +the Journal (from parsed GPX) need day computation. The function signature +works with the `GpxData` tracks and waypoints returned by `parseGpx`: + +```typescript +interface DayStage { + dayNumber: number; + startWaypointIndex: number; + endWaypointIndex: number; + startName?: string; + endName?: string; + distance: number; // meters + ascent: number; // meters + descent: number; // meters +} + +function computeDays( + waypoints: Waypoint[], + tracks: TrackPoint[][], +): DayStage[]; +``` + +The Planner's `useDays()` hook maps its Yjs waypoints + EnrichedRoute into +this same shape before calling `computeDays`. + ## Risks / Trade-offs - **Segment boundary alignment**: The day computation relies on diff --git a/openspec/changes/multi-day-routes/proposal.md b/openspec/changes/multi-day-routes/proposal.md index bb8cbbe..e09b6b3 100644 --- a/openspec/changes/multi-day-routes/proposal.md +++ b/openspec/changes/multi-day-routes/proposal.md @@ -44,6 +44,9 @@ the data model, computation logic, and integration wiring. - `planner-session`: Waypoints gain an `overnight` property in Yjs state - `map-display`: Day boundary labels on route, overnight marker styling - `gpx-export`: Day-break metadata in exported GPX waypoints +- `gpx-import`: Parse `overnight` waypoints to restore day breaks +- `route-management`: Populate `dayBreaks` column on save, expose per-day stats +- `journal-route-detail`: Day breakdown display with per-day stats and map segments ## Non-Goals @@ -52,8 +55,9 @@ the data model, computation logic, and integration wiring. - **Accommodation search**: No POI lookup for campsites or hotels. Out of scope. - **Per-day routing profiles**: All days use the same BRouter profile. Supporting different profiles per day would require rearchitecting the routing pipeline. -- **Journal integration**: Saving multi-day routes to the Journal is a separate - concern (route-features change). This spec is Planner-only. +- **Per-day activity tracking**: Linking individual activities to specific days + of a multi-day route (e.g. "Day 2 activity"). Activities remain linked to the + full route. Per-day activity chaining is a future concern. ## Impact @@ -67,6 +71,13 @@ the data model, computation logic, and integration wiring. sections and overnight toggle buttons - **ElevationChart**: Canvas drawing extended with vertical day dividers - **Map**: New day-label layer and overnight marker variant -- **GPX**: `generateGpx` extended to emit `overnight` on day-break - waypoints +- **GPX generate**: `generateGpx` extended to emit `overnight` on + day-break waypoints +- **GPX parse**: `parseGpx` extended to recognize `overnight` and + set `isDayBreak: true` on parsed waypoints +- **Journal route storage**: `updateRoute` populates `dayBreaks` column from + parsed waypoint indices when saving GPX +- **Journal route detail**: Day breakdown section with per-day stats (distance, + ascent, descent) when `dayBreaks` is non-empty - **i18n**: New keys for day labels, overnight toggle, per-day stats (en + de) + in both planner and journal namespaces diff --git a/openspec/changes/multi-day-routes/tasks.md b/openspec/changes/multi-day-routes/tasks.md index d6f984d..717fbcd 100644 --- a/openspec/changes/multi-day-routes/tasks.md +++ b/openspec/changes/multi-day-routes/tasks.md @@ -1,8 +1,8 @@ ## 1. Data Model & Computation - [ ] 1.1 Add `overnight` property support to Yjs waypoint Y.Maps: helper functions `setOvernight(yjs, index, value)` and `isOvernight(yMap)` in a new `apps/planner/app/lib/overnight.ts` -- [ ] 1.2 Create `apps/planner/app/lib/compute-days.ts` with `computeDays()` pure function: takes waypoints array + `EnrichedRoute`, returns `DayStage[]` (day number, waypoint range, coord range, distance, ascent, descent, estimated time) -- [ ] 1.3 Create `useDays()` hook in `apps/planner/app/lib/use-days.ts`: observes Yjs waypoints + routeData, calls `computeDays()`, returns reactive `DayStage[]` +- [ ] 1.2 Create `computeDays()` pure function in `packages/gpx/src/compute-days.ts`: takes `Waypoint[]` + `TrackPoint[][]`, returns `DayStage[]` (day number, waypoint range, distance, ascent, descent). Export from package index. +- [ ] 1.3 Create `useDays()` hook in `apps/planner/app/lib/use-days.ts`: maps Yjs waypoints + EnrichedRoute into `computeDays()` input, returns reactive `DayStage[]` ## 2. Sidebar Day Breakdown @@ -22,19 +22,30 @@ - [ ] 4.1 Add day divider rendering to `ElevationChart`: dashed vertical lines at overnight waypoint distances with "Day N" labels at top - [ ] 4.2 Show per-day distance ranges on x-axis labels (e.g. "120 km" at each day boundary) -## 5. GPX Export +## 5. GPX Roundtrip - [ ] 5.1 Extend `generateGpx` in `@trails-cool/gpx` to emit `overnight` for waypoints with `isDayBreak: true` -- [ ] 5.2 Add multi-track export option: split track into one `` per day, each named "Day N: Start - End" +- [ ] 5.2 Extend `parseGpx` in `@trails-cool/gpx` to recognize `overnight` and set `isDayBreak: true` on parsed waypoints +- [ ] 5.3 Add multi-track export option: split track into one `` per day, each named "Day N: Start - End" -## 6. i18n +## 6. Journal Integration -- [ ] 6.1 Add translation keys for en + de: day labels ("Day 1", "Tag 1"), overnight toggle ("Mark as overnight stop" / "Als Ubernachtung markieren"), per-day stats, route summary +- [ ] 6.1 Update `updateRoute` in `apps/journal/app/lib/routes.server.ts` to extract `dayBreaks` indices from parsed GPX waypoints and write to `dayBreaks` column +- [ ] 6.2 Expose `dayBreaks` and per-day stats in route detail loader (`routes.$id.tsx`) +- [ ] 6.3 Add day breakdown section to route detail page: per-day distance, ascent, descent, start/end names — shown only when dayBreaks is non-empty +- [ ] 6.4 Color route map segments per day (alternating colors) on the route detail map when dayBreaks exist -## 7. Testing +## 7. i18n -- [ ] 7.1 Unit tests for `computeDays()`: single day (no overnight), two days, three days, empty route, single waypoint, overnight on first/last waypoint edge cases -- [ ] 7.2 Unit tests for `overnight.ts` helpers: set/clear/check overnight on Y.Map -- [ ] 7.3 Unit tests for GPX export with overnight waypoints: verify `overnight` output, multi-track splitting -- [ ] 7.4 E2E test: add waypoints, toggle overnight on one, verify sidebar shows day breakdown with correct stats -- [ ] 7.5 E2E test: export GPX with day breaks, verify downloaded file contains overnight metadata +- [ ] 7.1 Add Planner translation keys for en + de: day labels ("Day 1", "Tag 1"), overnight toggle ("Mark as overnight stop" / "Als Übernachtung markieren"), per-day stats, route summary +- [ ] 7.2 Add Journal translation keys for en + de: day breakdown header, per-day stats labels + +## 8. Testing + +- [ ] 8.1 Unit tests for `computeDays()`: single day (no overnight), two days, three days, empty route, single waypoint, overnight on first/last waypoint edge cases +- [ ] 8.2 Unit tests for `overnight.ts` helpers: set/clear/check overnight on Y.Map +- [ ] 8.3 Unit tests for GPX roundtrip: generate with `isDayBreak`, parse back, verify `isDayBreak` preserved +- [ ] 8.4 Unit tests for `dayBreaks` extraction in route update logic +- [ ] 8.5 E2E test: add waypoints, toggle overnight on one, verify sidebar shows day breakdown with correct stats +- [ ] 8.6 E2E test: export GPX with day breaks, verify downloaded file contains overnight metadata +- [ ] 8.7 E2E test: save multi-day route to Journal, verify day breakdown displays on route detail page