Archive multi-day-routes change, sync spec to main
Synced delta spec (6 requirements) to openspec/specs/multi-day-routes/. Archived change to openspec/changes/archive/2026-04-11-multi-day-routes/. All 4 artifacts complete. All 35 tasks complete. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d325127a06
commit
adc5b44739
6 changed files with 50 additions and 0 deletions
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-03-28
|
||||
329
openspec/changes/archive/2026-04-11-multi-day-routes/design.md
Normal file
329
openspec/changes/archive/2026-04-11-multi-day-routes/design.md
Normal file
|
|
@ -0,0 +1,329 @@
|
|||
## 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
|
||||
|
||||
### D9: GPX parse recognizes overnight waypoints
|
||||
|
||||
The `parseWaypoints` function in `packages/gpx/src/parse.ts` is extended to
|
||||
check for `<type>overnight</type>` inside `<wpt>` 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
|
||||
`<type>overnight</type>` (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`.
|
||||
|
||||
### D13: Sidebar waypoint hover highlights map marker
|
||||
|
||||
Hovering a waypoint row in the `WaypointSidebar` passes the waypoint index
|
||||
up to `SessionView` via an `onWaypointHover` callback. `PlannerMap` receives
|
||||
a `highlightedWaypoint` index and renders the corresponding marker with a
|
||||
CSS `scale(1.17)` transform (0.2s ease transition). This reuses the existing
|
||||
`waypointIcon` function with a `highlighted` parameter — no extra DOM
|
||||
elements or Leaflet layers needed.
|
||||
|
||||
### D14: Journal route detail — day segment hover interaction
|
||||
|
||||
Hovering a day row in the route detail day breakdown triggers two effects:
|
||||
|
||||
1. **Map segment highlighting**: The hovered day's polyline thickens (weight 5,
|
||||
opacity 1) while other days dim (weight 2, opacity 0.3). Implemented via
|
||||
`highlightedDay` state passed to `DayColoredRoute`.
|
||||
|
||||
2. **Fly-to-segment**: A `FlyToSegment` component calls `map.flyToBounds()`
|
||||
on the hovered segment's bounds (200ms animation). On mouse leave it flies
|
||||
back to the full route bounds. The full route bounds are cached on first
|
||||
render to avoid recomputation.
|
||||
|
||||
### D15: Per-day GPX export endpoint
|
||||
|
||||
The existing `/api/routes/:id/gpx` endpoint gains an optional `?day=N` query
|
||||
parameter. When present, it parses the stored GPX, runs `computeDays()` to
|
||||
find the track point range for that day, and returns a GPX containing only
|
||||
that day's track segment. The filename includes the day number
|
||||
(`route_day1.gpx`).
|
||||
|
||||
## 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.
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
## Why
|
||||
|
||||
Long routes -- multi-day bike tours, thru-hikes, extended backpacking trips --
|
||||
are flat lists of waypoints with no structure. Users planning a 5-day ride from
|
||||
Berlin to Prague have no way to mark where each day ends, see per-day distance
|
||||
and climbing, or reason about daily effort. They resort to external spreadsheets
|
||||
or mental arithmetic to divide the route into manageable stages.
|
||||
|
||||
The Planner already computes total distance and elevation across the full route.
|
||||
Adding day structure is a matter of marking overnight stops on existing waypoints
|
||||
and deriving per-day stats from the segment data we already have.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **Day-break waypoints**: Any waypoint can be toggled as an overnight stop.
|
||||
This adds an `overnight: true` flag to the waypoint's Y.Map in the existing
|
||||
Yjs waypoints array. First and last waypoints are implicit day boundaries.
|
||||
- **Per-day stats**: Distance, total ascent, and estimated duration computed per
|
||||
day by splitting the route at overnight waypoints. Derived from the existing
|
||||
`segmentBoundaries` and `coordinates` in the enriched route data.
|
||||
- **Sidebar day breakdown**: Waypoints grouped by day with collapsible sections,
|
||||
per-day stats, and overnight toggle. Day 1 expanded by default.
|
||||
- **Elevation chart day dividers**: Dashed vertical lines at day boundaries with
|
||||
"Day N" labels.
|
||||
- **Map day labels**: White pill markers on the route at day boundaries showing
|
||||
"Day 1 . 120 km".
|
||||
- **GPX export**: Day-break waypoints exported with a `<type>overnight</type>`
|
||||
element so the structure survives round-trips.
|
||||
- **Waypoint highlighting**: Hovering a waypoint in the sidebar highlights
|
||||
the corresponding marker on the map with a smooth scale animation.
|
||||
- **Journal day interaction**: Hovering a day in the Journal route detail
|
||||
highlights that segment on the map and flies the map to fit it.
|
||||
- **Per-day GPX export**: Each day in the Journal route detail has a GPX
|
||||
download button that exports just that day's track segment.
|
||||
|
||||
All state lives in Yjs. No database changes are needed -- the Planner remains
|
||||
stateless. The visual design is already specified in the `visual-redesign`
|
||||
change (D4 sidebar, D5 map markers, D6 elevation chart); this spec covers
|
||||
the data model, computation logic, and integration wiring.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `multi-day-routes`: Overnight waypoint markers, per-day stats computation,
|
||||
day-aware sidebar/chart/map display, multi-day GPX export
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `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 `<type>overnight</type>` 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, map segment
|
||||
highlighting, fly-to-segment on hover, per-day GPX export
|
||||
- `planner-sidebar`: Waypoint hover highlights corresponding map marker
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- **Automatic day splitting**: No algorithm to suggest where to stop. Users
|
||||
decide manually. This avoids opinionated defaults and keeps the logic simple.
|
||||
- **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.
|
||||
- **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
|
||||
|
||||
- **Yjs state**: `overnight` boolean added to waypoint Y.Map entries (additive,
|
||||
backwards-compatible -- existing sessions without it behave as single-day)
|
||||
- **Shared types**: `Waypoint.isDayBreak` already exists in `@trails-cool/types`
|
||||
but is unused. This change activates it.
|
||||
- **New utility**: `compute-days.ts` -- pure function that splits route data at
|
||||
overnight waypoints and returns per-day stats
|
||||
- **Sidebar**: `WaypointSidebar.tsx` gains day-grouped view with collapsible
|
||||
sections and overnight toggle buttons
|
||||
- **ElevationChart**: Canvas drawing extended with vertical day dividers
|
||||
- **Map**: New day-label layer and overnight marker variant
|
||||
- **GPX generate**: `generateGpx` extended to emit `<type>overnight</type>` on
|
||||
day-break waypoints
|
||||
- **GPX parse**: `parseGpx` extended to recognize `<type>overnight</type>` 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. Hover highlights segment on
|
||||
map and flies to fit. Per-day GPX download via `?day=N` query param.
|
||||
- **Sidebar hover**: Hovering waypoint rows highlights the marker on the map
|
||||
with CSS `scale(1.17)` transition (0.2s ease)
|
||||
- **i18n**: New keys for day labels, overnight toggle, per-day stats (en + de)
|
||||
in both planner and journal namespaces
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Overnight waypoint markers
|
||||
Any waypoint SHALL be toggleable as an overnight stop, creating day boundaries in the route.
|
||||
|
||||
#### Scenario: Toggle overnight
|
||||
- **WHEN** a user toggles the overnight flag on a waypoint
|
||||
- **THEN** the waypoint's `overnight: true` flag is set in the Yjs document
|
||||
- **AND** the route is visually divided into days at that point
|
||||
|
||||
#### Scenario: Implicit day boundaries
|
||||
- **WHEN** overnight stops are set
|
||||
- **THEN** the first waypoint is the implicit start of Day 1 and the last waypoint is the implicit end of the final day
|
||||
|
||||
### Requirement: Per-day statistics
|
||||
The Planner SHALL compute and display distance, ascent, and estimated duration for each day.
|
||||
|
||||
#### Scenario: Day stats computed
|
||||
- **WHEN** a route has overnight waypoints
|
||||
- **THEN** per-day distance, total ascent, and estimated duration are derived from segment boundaries and coordinates
|
||||
|
||||
### Requirement: Day-aware sidebar
|
||||
The sidebar SHALL group waypoints by day with collapsible sections and per-day stats.
|
||||
|
||||
#### Scenario: Day breakdown
|
||||
- **WHEN** a route has multiple days
|
||||
- **THEN** waypoints are grouped under "Day 1", "Day 2", etc. with collapsible sections
|
||||
- **AND** each section header shows day distance and ascent
|
||||
|
||||
### Requirement: Elevation chart day dividers
|
||||
The elevation chart SHALL show day boundaries as dashed vertical lines.
|
||||
|
||||
#### Scenario: Day dividers on chart
|
||||
- **WHEN** a route has multiple days
|
||||
- **THEN** dashed vertical lines with "Day N" labels appear at each overnight waypoint position
|
||||
|
||||
### Requirement: Map day labels
|
||||
The map SHALL display day summary labels at day boundary waypoints.
|
||||
|
||||
#### Scenario: Day labels on map
|
||||
- **WHEN** a route has multiple days
|
||||
- **THEN** white pill markers at day boundaries show "Day N . X km"
|
||||
|
||||
### Requirement: Multi-day GPX export
|
||||
Day structure SHALL be preserved in GPX exports via waypoint type elements.
|
||||
|
||||
#### Scenario: Export multi-day route
|
||||
- **WHEN** a user exports a plan with overnight waypoints
|
||||
- **THEN** overnight waypoints include a `<type>overnight</type>` element in the GPX
|
||||
- **AND** reimporting the GPX restores the day structure
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
## 1. Data Model & Computation
|
||||
|
||||
- [x] 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`
|
||||
- [x] 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.
|
||||
- [x] 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
|
||||
|
||||
- [x] 2.1 Create `DayBreakdown` component: renders collapsible day sections with per-day stats (distance, ascent, estimated time), Day 1 expanded by default
|
||||
- [x] 2.2 Add overnight toggle button (moon icon) to each waypoint row in `WaypointSidebar` — amber styling when active, calls `setOvernight()`
|
||||
- [x] 2.3 Integrate `DayBreakdown` into `WaypointSidebar`: show day-grouped view when any waypoint has overnight, flat list otherwise
|
||||
- [x] 2.4 Add route summary header to sidebar: total distance, ascent, number of days (e.g. "Berlin -> Erfurt 343 km ^868m 2 days")
|
||||
|
||||
## 3. Map Integration
|
||||
|
||||
- [x] 3.1 Add overnight marker variant to `PlannerMap`: amber-brown circle (`--stop` token) for overnight waypoints, replacing default olive marker
|
||||
- [x] 3.2 Add day label DivIcon markers on route at day boundaries: white pill with "Day N . X km" text, positioned at overnight waypoint coordinates
|
||||
- [x] 3.3 Add right-click context menu on waypoint markers with "Mark as overnight stop" / "Remove overnight stop" option
|
||||
|
||||
## 4. Elevation Chart
|
||||
|
||||
- [x] 4.1 Add day divider rendering to `ElevationChart`: dashed vertical lines at overnight waypoint distances with "Day N" labels at top
|
||||
- [x] 4.2 Show per-day distance ranges on x-axis labels (e.g. "120 km" at each day boundary)
|
||||
|
||||
## 5. GPX Roundtrip
|
||||
|
||||
- [x] 5.1 Extend `generateGpx` in `@trails-cool/gpx` to emit `<type>overnight</type>` for waypoints with `isDayBreak: true`
|
||||
- [x] 5.2 Extend `parseGpx` in `@trails-cool/gpx` to recognize `<type>overnight</type>` and set `isDayBreak: true` on parsed waypoints
|
||||
- [x] 5.3 Add multi-track export option: split track into one `<trk>` per day, each named "Day N: Start - End"
|
||||
|
||||
## 6. Journal Integration
|
||||
|
||||
- [x] 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
|
||||
- [x] 6.2 Expose `dayBreaks` and per-day stats in route detail loader (`routes.$id.tsx`)
|
||||
- [x] 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
|
||||
- [x] 6.4 Color route map segments per day (alternating colors) on the route detail map when dayBreaks exist
|
||||
|
||||
## 7. i18n
|
||||
|
||||
- [x] 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
|
||||
- [x] 7.2 Add Journal translation keys for en + de: day breakdown header, per-day stats labels
|
||||
|
||||
## 8. Planner Waypoint Hover
|
||||
|
||||
- [x] 8.0a Add `onWaypointHover` callback to `WaypointSidebar`, emit waypoint index on row hover
|
||||
- [x] 8.0b Pass `highlightedWaypoint` index to `PlannerMap`, render highlighted marker with CSS `scale(1.17)` and 0.2s ease transition
|
||||
|
||||
## 9. Journal Day Interaction
|
||||
|
||||
- [x] 9.1 Add `highlightedDay` state to route detail page, pass to `RouteMapThumbnail`
|
||||
- [x] 9.2 Highlight hovered day segment on map: thicken active (weight 5), dim others (opacity 0.3)
|
||||
- [x] 9.3 Fly map to hovered day segment bounds (200ms), fly back to full route on mouse leave
|
||||
- [x] 9.4 Add per-day GPX download button on each day row in route detail breakdown
|
||||
- [x] 9.5 Extend `/api/routes/:id/gpx` with `?day=N` query param to export single day's track segment
|
||||
|
||||
## 10. Testing
|
||||
|
||||
- [x] 10.1 Unit tests for `computeDays()`: single day (no overnight), two days, three days, empty route, single waypoint, overnight on first/last waypoint edge cases
|
||||
- [x] 10.2 Unit tests for `overnight.ts` helpers: set/clear/check overnight on Y.Map
|
||||
- [x] 10.3 Unit tests for GPX roundtrip: generate with `isDayBreak`, parse back, verify `isDayBreak` preserved
|
||||
- [x] 10.4 Unit tests for `dayBreaks` extraction in route update logic
|
||||
- [x] 10.5 E2E test: add waypoints, toggle overnight on one, verify sidebar shows day breakdown with correct stats
|
||||
- [x] 10.6 E2E test: export GPX with day breaks, verify downloaded file contains overnight metadata
|
||||
- [x] 10.7 E2E test: save multi-day route to Journal, verify day breakdown displays on route detail page
|
||||
Loading…
Add table
Add a link
Reference in a new issue