Merge pull request #87 from trails-cool/planner-route-interactions

Update OpenSpec artifacts for route interactions
This commit is contained in:
Ullrich Schäfer 2026-03-26 21:38:52 +00:00 committed by GitHub
commit ce964cae96
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 533 additions and 285 deletions

View file

@ -0,0 +1,127 @@
## Context
The Planner currently renders routes as a single-color `L.Polyline` with no
interactivity — users can't click on the route or drag it. Waypoints can only
be appended (click on map) or reordered in the sidebar. BRouter returns
per-point elevation in coordinates (`[lon, lat, ele]`) and can return surface
tags, but `mergeGeoJsonSegments` currently discards all per-point data and
only keeps track-level totals.
bikerouter.de and komoot both support click-to-split and drag-to-reshape as
primary editing interactions. These are the most natural way to refine a route
after the initial waypoints are placed.
## Goals / Non-Goals
**Goals:**
- Hover over the route to see a transient ghost marker at the nearest route point
- Click or drag the ghost marker to insert a waypoint (split / reshape)
- Color the route by elevation gradient (green→yellow→red) or surface type
- Sync elevation chart colors with route coloring in elevation mode
- Preserve per-point data from BRouter through the merge pipeline
- All interactions synced via Yjs (collaborative)
**Non-Goals:**
- Undo/redo system (future change)
- Route alternatives (show multiple options)
- Custom color gradient configuration
- Surface type legend or detailed surface info panel
- Offline route coloring (requires BRouter data)
## Decisions
### D1: Ghost marker interaction (inspired by brouter-web)
Following brouter-web's `L.Routing.Edit` pattern: a **single persistent
draggable Marker** (`RouteInteraction` component) that appears when the cursor
is near the route and can be clicked or dragged to insert a waypoint.
Key techniques from brouter-web:
- **Distance-based snap**: Listen on `map.mousemove`, compute pixel distance
to nearest route coordinate (15px tolerance). No reliance on SVG polyline
mouseout events (which cause flickering).
- **Single reusable marker**: Created once via `L.marker({ draggable: true })`,
shown/hidden with `addTo(map)`/`remove()`. Not destroyed/recreated on each
hover.
- **Dragging guard**: `draggingRef` flag freezes snap updates during drag and
prevents hide-on-mouseout.
- **Leaflet's built-in drag**: `draggable: true` on the Marker uses Leaflet's
`L.Draggable` which automatically calls `L.DomUtil.disableTextSelection()`
during drag and re-enables on dragend.
- **Trailer lines**: Two persistent dashed `L.Polyline` instances shown during
drag, connecting the ghost to adjacent waypoints for visual feedback.
**Alternative considered and rejected**: Fixed midpoint CircleMarkers between
waypoints. Caused flickering (render loop between marker and polyline events),
required zoom threshold to avoid clutter, and `CircleMarker` SVG rendering
was unreliable in react-leaflet after programmatic zoom.
**Alternative considered and rejected**: Invisible wide polyline for click
detection. Playwright couldn't trigger mousemove events on SVG paths with
opacity 0. The map-level listener with distance-based snap is more reliable.
### D2: Per-point data preservation in BRouter response
BRouter GeoJSON coordinates are `[lon, lat, ele]`. For surface data, BRouter's
`tiledesc=true` parameter includes per-point `WayTags` in the
`properties.messages` array (e.g., `surface=asphalt highway=primary`).
`mergeGeoJsonSegments` now:
1. Preserves full 3D coordinates `[lon, lat, ele]`
2. Tracks segment boundary indices (where each waypoint segment starts)
3. Extracts surface type per point from `WayTags` via regex
The `EnrichedRoute` interface:
```typescript
interface EnrichedRoute {
coordinates: [number, number, number][]; // [lon, lat, ele]
segmentBoundaries: number[]; // coordinate index where each segment starts
surfaces: string[]; // surface type per point (from WayTags)
totalLength: number;
totalAscend: number;
totalTime: number;
geojson: GeoJsonCollection; // backwards compat
}
```
All enriched data is stored in `routeData` Y.Map (as JSON strings) so all
participants receive it.
### D3: Colored route rendering with segmented polylines
`ColoredRoute` component renders multiple `L.Polyline` segments colored by
per-point data. Three modes toggled via a `<select>` in the session header,
synced via Yjs `routeData.colorMode`:
1. **Plain**: Single-color blue polyline (default)
2. **Elevation**: green→yellow→red gradient normalized to route elevation range
3. **Surface**: Fixed color palette per surface type (asphalt=gray, gravel=brown,
path=green, track=orange, etc.). Falls back to plain if surface data
unavailable.
### D4: Elevation chart color sync
`ElevationChart` reads `colorMode` from Yjs and uses the same `elevationColor()`
function as `ColoredRoute` when in elevation mode. Each chart segment is drawn
with the corresponding elevation color for both the fill and line.
### D5: Map click suppression
When the ghost marker click or drag inserts a waypoint, a `suppressMapClickRef`
flag prevents the subsequent `MapClickHandler` from also appending a waypoint.
This solves the Leaflet event model issue where map click fires independently
from layer click.
## Risks / Trade-offs
- **Performance with many segments** → Hundreds of small `L.Polyline` instances
for colored rendering could be slow. Can switch to canvas if needed later.
- **BRouter surface data availability** → Not all BRouter profiles return
surface tags. Surface coloring falls back to plain when data is missing.
- **Snap precision** → Distance-based snap at 15px works well at typical zoom
levels. On very dense routes, the nearest coordinate might be slightly off
from the visual route line.
- **Playwright E2E limitations** → Ghost marker hover can't be reliably tested
via Playwright's mouse simulation on Leaflet SVG. Verified visually via
cmux browser instead.

View file

@ -0,0 +1,54 @@
## Why
The Planner's route editing is click-only: users place waypoints and the route
auto-computes between them. But refining a route is tedious — you can't insert
a waypoint on the route itself, drag a route segment to reshape it, or see what
kind of terrain you're heading into. These are table-stakes features in modern
route planners (bikerouter.de, komoot, Ride with GPS) and the most common
feedback from early testers.
## What Changes
- **Route splitting**: Hover near the route to see a ghost marker at the nearest
point. Click it to insert a new waypoint, splitting the segment. The waypoint
snaps to the closest coordinate on the existing route.
- **Drag-to-reshape**: Drag the ghost marker to a new position to reshape the
route. Dashed trailer lines connect to adjacent waypoints during drag.
Inspired by brouter-web's single-persistent-marker pattern.
- **Colored route rendering**: Replace the single-color route polyline with
segments colored by elevation gradient or surface type (from BRouter's
per-point data). Users toggle between plain, elevation, and surface color
modes. Elevation chart syncs with route colors in elevation mode.
## Capabilities
### New Capabilities
- `route-splitting`: Insert waypoints by clicking the ghost marker on the route,
with snapping to the nearest route coordinate
- `route-drag-reshape`: Drag the ghost marker to reshape the route interactively
with trailer lines and text selection prevention
- `route-coloring`: Color-code the route polyline by elevation gradient or
surface type using BRouter tiledesc data, with elevation chart color sync
### Modified Capabilities
- `map-display`: Route visualization changes from a single-color polyline to
a segmented, interactive polyline with color modes and ghost marker interaction
- `brouter-integration`: BRouter requests include `tiledesc=true`; response data
(elevation, surface tags, segment boundaries) preserved in EnrichedRoute
## Impact
- **PlannerMap.tsx**: Route polyline becomes interactive via RouteInteraction
component with ghost marker, map click suppression
- **brouter.ts**: EnrichedRoute with per-point 3D coordinates, segment
boundaries, and surface types extracted from BRouter tiledesc WayTags
- **use-routing.ts**: Stores enriched data (coordinates, boundaries, surfaces)
in Yjs routeData map
- **New components**: RouteInteraction (ghost marker + drag), ColoredRoute
(segmented polyline renderer)
- **ElevationChart**: Uses same `elevationColor()` gradient as route in
elevation mode
- **Dependencies**: No new dependencies (uses Leaflet's built-in L.Draggable)
- **i18n**: New keys for color mode labels (en + de)

View file

@ -1,11 +1,11 @@
## MODIFIED Requirements
### Requirement: Route computation from waypoints
The Planner SHALL compute a route between ordered waypoints by calling the BRouter HTTP API and returning the result as GeoJSON, preserving per-point elevation and segment boundary data.
The Planner SHALL compute a route between ordered waypoints by calling the BRouter HTTP API with tiledesc enabled and returning the result as an EnrichedRoute, preserving per-point elevation, surface data, and segment boundary indices.
#### Scenario: Compute route with two waypoints
- **WHEN** the routing host submits two waypoints (start, end) with profile "trekking"
- **THEN** the BRouter API returns a GeoJSON route within 2 seconds
- **THEN** the BRouter API returns a route within 2 seconds
#### Scenario: Compute route with via points
- **WHEN** the routing host submits three or more waypoints
@ -18,3 +18,7 @@ The Planner SHALL compute a route between ordered waypoints by calling the BRout
#### Scenario: Segment boundaries tracked
- **WHEN** a route with N waypoints is computed (N-1 segments)
- **THEN** the response SHALL include an array of coordinate indices marking where each waypoint-to-waypoint segment begins
#### Scenario: Surface data extracted
- **WHEN** BRouter returns tiledesc messages with WayTags containing surface information
- **THEN** the response SHALL include a surface type string per coordinate point extracted from the WayTags (e.g., "asphalt", "gravel", "path")

View file

@ -11,10 +11,6 @@ The map SHALL display the computed route as an interactive, optionally color-cod
- **WHEN** a waypoint is added, moved, or deleted
- **THEN** the route polyline updates after BRouter recomputes the route
#### Scenario: Route is clickable
- **WHEN** a user clicks on the route polyline
- **THEN** a new waypoint is inserted at the clicked position (see route-splitting spec)
#### Scenario: Route has midpoint handles
- **WHEN** a route with two or more waypoints is displayed
- **THEN** draggable midpoint handles appear on each route segment (see route-drag-reshape spec)
#### Scenario: Ghost marker on hover
- **WHEN** the cursor is within 15 pixels of the route polyline
- **THEN** a transient ghost marker appears at the nearest route point, which can be clicked or dragged to insert a waypoint (see route-splitting and route-drag-reshape specs)

View file

@ -12,20 +12,31 @@ The Planner SHALL support multiple route color modes that visualize per-point da
- **THEN** the route polyline is colored with a gradient from green (low) through yellow (mid) to red (high), based on the elevation at each point
#### Scenario: Surface color mode
- **WHEN** a user selects the "Surface" color mode and surface data is available
- **WHEN** a user selects the "Surface" color mode and surface data is available from BRouter tiledesc
- **THEN** the route polyline is colored by surface type (e.g., asphalt=gray, gravel=brown, path=green, track=orange)
#### Scenario: Surface data unavailable
- **WHEN** a user selects "Surface" color mode but BRouter did not return surface data
- **THEN** the route falls back to plain color mode and a brief message indicates surface data is not available
- **THEN** the route falls back to plain color mode
### Requirement: Color mode toggle
The Planner SHALL provide a UI control to switch between route color modes.
#### Scenario: Toggle control location
- **WHEN** a route is displayed
- **THEN** a color mode selector is visible in the session header or map controls
- **THEN** a color mode select dropdown is visible in the session header
#### Scenario: Color mode persists in session
- **WHEN** a user changes the color mode
- **THEN** the selection is stored in the Yjs routeData map and synced to all participants
### Requirement: Elevation chart color sync
The elevation profile chart SHALL use the same color gradient as the route when in elevation mode.
#### Scenario: Elevation mode chart coloring
- **WHEN** the color mode is set to "Elevation"
- **THEN** the elevation chart line and fill use the same green→yellow→red gradient as the route polyline
#### Scenario: Plain mode chart coloring
- **WHEN** the color mode is "Plain" or "Surface"
- **THEN** the elevation chart uses the default blue color

View file

@ -0,0 +1,28 @@
## ADDED Requirements
### Requirement: Ghost marker on route hover
The Planner SHALL display a transient ghost marker when the cursor is near the route polyline, allowing users to reshape the route by dragging.
#### Scenario: Ghost marker appears on hover
- **WHEN** the cursor moves within 15 pixels of the route polyline
- **THEN** a ghost marker (small blue circle) appears at the nearest route coordinate point
#### Scenario: Ghost marker disappears on leave
- **WHEN** the cursor moves more than 15 pixels away from the route polyline
- **THEN** the ghost marker disappears
#### Scenario: Drag ghost marker to reshape
- **WHEN** a user drags the ghost marker to a new position
- **THEN** a new waypoint is inserted between the two adjacent waypoints of the hovered segment, and the route recomputes through the new point
#### Scenario: Trailer lines during drag
- **WHEN** a user is dragging the ghost marker
- **THEN** dashed guide lines are shown connecting the ghost marker to the adjacent waypoints
#### Scenario: No text selection during drag
- **WHEN** a user drags the ghost marker
- **THEN** text selection is disabled on the page (via Leaflet's built-in L.Draggable)
#### Scenario: Reshape syncs to other participants
- **WHEN** a user reshapes the route by dragging the ghost marker
- **THEN** all other participants see the new waypoint and recomputed route via Yjs sync

View file

@ -0,0 +1,20 @@
## ADDED Requirements
### Requirement: Insert waypoint by clicking on route
The Planner SHALL allow users to insert a new waypoint by clicking the ghost marker that appears when hovering near the route.
#### Scenario: Click ghost marker to split
- **WHEN** a ghost marker is visible on the route and the user clicks it
- **THEN** a new waypoint is inserted at the ghost marker position between the appropriate adjacent waypoints, and the route recomputes
#### Scenario: Waypoint snaps to route
- **WHEN** the ghost marker appears near the route
- **THEN** it is positioned at the closest coordinate point on the existing route geometry, not at the raw cursor position
#### Scenario: No duplicate waypoint from map click
- **WHEN** a user clicks the ghost marker to insert a waypoint
- **THEN** the map click handler is suppressed so only one waypoint is inserted (not an additional one appended at the end)
#### Scenario: Split syncs to other participants
- **WHEN** a user inserts a waypoint by clicking the ghost marker
- **THEN** all other participants see the new waypoint appear in the waypoint list and on the map via Yjs sync

View file

@ -0,0 +1,36 @@
## 1. BRouter Data Pipeline
- [x] 1.1 Modify `mergeGeoJsonSegments` to preserve 3D coordinates (lon, lat, ele) and track segment boundary indices
- [x] 1.2 Add `EnrichedRoute` interface with coordinates, segmentBoundaries, surfaces, and stats
- [x] 1.3 Store enriched route data (including segment boundaries and surfaces) in Yjs routeData map
- [x] 1.4 Add `tiledesc=true` to BRouter requests and extract surface types from WayTags messages
- [x] 1.5 Write unit test for segment boundary tracking across multi-waypoint routes
## 2. Route Interaction (ghost marker)
- [x] 2.1 Create `RouteInteraction` component — single persistent draggable Marker following brouter-web pattern
- [x] 2.2 Listen on `map.mousemove`, snap ghost marker to nearest route point within 15px tolerance (distance-based, not polyline events)
- [x] 2.3 On click, insert waypoint at snapped position using segment boundary lookup
- [x] 2.4 On drag, insert waypoint at drop position with trailer lines to adjacent waypoints
- [x] 2.5 Suppress map click handler after ghost insert to prevent duplicate waypoints
- [x] 2.6 Guard against flickering: `draggingRef` freezes snap updates during drag, distance-based mouseout
- [x] 2.7 Disable route interaction when no-go area drawing mode is active
## 3. Route Coloring
- [x] 3.1 Create `ColoredRoute` component — renders segmented L.Polyline instances with per-point colors
- [x] 3.2 Implement elevation gradient: normalize elevation to 0-1 range, map to green→yellow→red
- [x] 3.3 Implement surface coloring: extract surface types from BRouter tiledesc, map to color palette
- [x] 3.4 Fall back to plain mode when surface data is unavailable
- [x] 3.5 Add color mode state (`colorMode`) to Yjs routeData map, synced across participants
- [x] 3.6 Add color mode select dropdown to session header
- [x] 3.7 Sync elevation chart colors with route — use same `elevationColor()` gradient in ElevationChart
- [x] 3.8 Add i18n keys for color mode labels (en + de)
## 4. Verify
- [x] 4.1 E2E test: click near route inserts waypoint at correct position
- [x] 4.2 E2E test: color mode toggle switches between plain/elevation/surface
- [x] 4.3 E2E test: enriched route response includes segment boundaries and coordinates
- [x] 4.4 Unit test: segment boundary computation with 1, 2, and 4 segments
- [x] 4.5 Visual verification: ghost marker, drag with trailers, all color modes (cmux browser)

View file

@ -1,132 +0,0 @@
## Context
The Planner currently renders routes as a single-color `L.Polyline` with no
interactivity — users can't click on the route or drag it. Waypoints can only
be appended (click on map) or reordered in the sidebar. BRouter returns
per-point elevation in coordinates (`[lon, lat, ele]`) and can return surface
tags, but `mergeGeoJsonSegments` currently discards all per-point data and
only keeps track-level totals.
bikerouter.de and komoot both support click-to-split and drag-to-reshape as
primary editing interactions. These are the most natural way to refine a route
after the initial waypoints are placed.
## Goals / Non-Goals
**Goals:**
- Click on the route polyline to insert a waypoint at that position
- Drag midpoint handles between waypoints to reshape the route
- Color the route by elevation gradient (green→yellow→red) or surface type
- Preserve per-point data from BRouter through the merge pipeline
- All interactions synced via Yjs (collaborative)
**Non-Goals:**
- Undo/redo system (future change)
- Route alternatives (show multiple options)
- Custom color gradient configuration
- Surface type legend or detailed surface info panel
- Offline route coloring (requires BRouter data)
## Decisions
### D1: Click-to-split via Leaflet polyline event
Listen for `click` on the route polyline. On click, find the closest point on
the route geometry, determine which waypoint segment it falls in (between
waypoint N and N+1), and insert a new waypoint at that position using
`Y.Array.insert(N+1, [newWaypoint])`.
To find the segment index: the route is computed segment-by-segment (one per
consecutive waypoint pair). Track the coordinate count per segment in the
merged GeoJSON so we can map any route point index back to a waypoint segment.
**Alternative considered**: Using a separate invisible polyline for click
detection. Unnecessary — Leaflet's built-in polyline click events work fine
with appropriate `weight` for hit detection.
### D2: Midpoint handles as draggable CircleMarkers
For each consecutive pair of waypoints, render a small, semi-transparent
`L.CircleMarker` at the geographic midpoint of the route segment (not the
straight-line midpoint — use the actual route geometry midpoint). On drag
start, the handle becomes opaque and turns into a waypoint drag. On drag end,
insert a new waypoint at the dropped position.
Handles are only visible on hover or at higher zoom levels to avoid clutter.
They reposition after each route computation.
**Alternative considered**: Handles at the straight-line midpoint between
waypoints. Bad UX — on a winding route, the midpoint may be far from the
actual route.
### D3: Per-point data preservation in BRouter response
BRouter GeoJSON coordinates are `[lon, lat, ele]` — elevation is already
present but currently unused beyond the ElevationChart. For surface data,
BRouter supports a `tiledesc` parameter that includes waytype/surface tags
per point in the `properties.messages` array.
Modify `mergeGeoJsonSegments` to:
1. Preserve the full 3-element coordinates (already done, but not exposed)
2. Track segment boundaries (array of indices where each waypoint segment starts)
3. Optionally parse `properties.messages` for surface tags
Store the enriched data in `routeData` Y.Map so all participants have it.
### D4: Colored route rendering with segmented polylines
Use multiple `L.Polyline` instances, each covering a short segment of the
route with a color based on the data value at that point. For elevation:
normalize elevation values to 0-1 range across the route, map to a
green→yellow→red gradient. For surface: map surface type strings to a fixed
color palette (asphalt=gray, gravel=brown, path=green, etc.).
Three rendering modes, toggled by a button in the header:
1. **Plain**: Current single-color blue polyline (default)
2. **Elevation**: Gradient by elevation
3. **Surface**: Colored by surface type
**Alternative considered**: `leaflet-hotline` for smooth canvas-based gradient
rendering. Better visual quality but adds a dependency and doesn't support
click events on the colored line. Since we need click-to-split on the route,
we need real Leaflet layers. Can revisit later if performance is an issue.
**Alternative considered**: Single Canvas renderer. Better performance for
very long routes but much more complex, and breaks Leaflet's event model.
Not needed at current route lengths (<1000 points typical).
### D5: Segment boundary tracking
The key data structure bridging BRouter output and map interactions:
```typescript
interface EnrichedRoute {
coordinates: [number, number, number][]; // [lon, lat, ele]
segmentBoundaries: number[]; // indices where each waypoint segment starts
surfaces?: string[]; // surface type per point (optional)
totalLength: number;
totalAscend: number;
totalTime: number;
}
```
`segmentBoundaries[i]` is the coordinate index where the route segment from
waypoint `i` to waypoint `i+1` starts. This enables:
- Click-to-split: find which segment a clicked point belongs to
- Midpoint handles: find the midpoint of each segment's geometry
- Per-segment coloring: color differently per waypoint pair if needed
## Risks / Trade-offs
- **Performance with many segments** → Hundreds of small `L.Polyline` instances
for colored rendering could be slow. Mitigate: batch updates, only re-render
changed segments, limit color segments to ~100 per route. Can switch to
canvas if needed later.
- **BRouter surface data availability** → Not all BRouter profiles return
surface tags. The `tiledesc` parameter may not work with all profiles.
Mitigate: surface coloring is optional; elevation always works (from coords).
- **Click precision on thin polylines** → Hard to click a 4px line on mobile.
Mitigate: use `L.Polyline` `weight` for rendering but a wider invisible
polyline for click detection.
- **Midpoint handle clutter** → Routes with many waypoints get cluttered.
Mitigate: only show handles on hover or at zoom level ≥ 12.

View file

@ -1,56 +0,0 @@
## Why
The Planner's route editing is click-only: users place waypoints and the route
auto-computes between them. But refining a route is tedious — you can't insert
a waypoint on the route itself, drag a route segment to reshape it, or see what
kind of terrain you're heading into. These are table-stakes features in modern
route planners (bikerouter.de, komoot, Ride with GPS) and the most common
feedback from early testers.
## What Changes
- **Route splitting**: Click on the route polyline to insert a new waypoint at
that position, splitting the segment. The waypoint snaps to the closest point
on the existing route.
- **Drag-to-reshape**: Invisible midpoint handles appear between waypoints on
the route. Dragging a midpoint inserts a new waypoint and triggers route
recomputation — the natural way to push a route onto a different path.
- **Colored route rendering**: Replace the single-color route polyline with
segments colored by elevation gradient or surface type (from BRouter's
per-point data). Users can toggle between plain, elevation, and surface
color modes.
## Capabilities
### New Capabilities
- `route-splitting`: Insert waypoints by clicking on the route polyline, with
snapping to the nearest route point
- `route-drag-reshape`: Drag midpoint handles on route segments to reshape the
route interactively
- `route-coloring`: Color-code the route polyline by elevation gradient or
surface type using BRouter track data
### Modified Capabilities
- `map-display`: Route visualization changes from a single-color polyline to
a segmented, interactive polyline with color modes and click/drag interactions
- `brouter-integration`: BRouter response data (elevation per point, surface
tags) must be preserved and exposed to the rendering layer
## Impact
- **PlannerMap.tsx**: Major changes — route polyline becomes interactive with
click-to-split and drag midpoint handles
- **brouter.ts**: Expose per-point elevation and surface data from GeoJSON
properties (currently only track-level stats are extracted)
- **use-routing.ts**: Support waypoint insertion at specific indices (not just
append)
- **use-yjs.ts**: Waypoint insertion at arbitrary positions (already supported
by Y.Array.insert)
- **New components**: ColoredRoute (segmented polyline renderer),
MidpointHandles (draggable reshape markers)
- **ElevationChart**: Add hover-to-highlight sync with colored route
- **Dependencies**: May need `leaflet-hotline` for smooth gradient rendering,
or custom canvas renderer
- **i18n**: New keys for color mode labels (en + de)

View file

@ -1,20 +0,0 @@
## ADDED Requirements
### Requirement: Midpoint drag handles on route segments
The Planner SHALL display draggable midpoint handles between consecutive waypoints on the route, allowing users to reshape the route by dragging.
#### Scenario: Midpoint handle visible
- **WHEN** a route is displayed with at least two waypoints
- **THEN** a small circular handle appears at the geographic midpoint of each route segment (using the actual route geometry, not straight-line distance)
#### Scenario: Drag midpoint to reshape
- **WHEN** a user drags a midpoint handle to a new position
- **THEN** a new waypoint is inserted at the dropped position between the two adjacent waypoints, and the route recomputes through the new point
#### Scenario: Handle visibility control
- **WHEN** the map zoom level is below 12
- **THEN** midpoint handles are hidden to reduce visual clutter
#### Scenario: Reshape syncs to other participants
- **WHEN** a user reshapes the route by dragging a midpoint
- **THEN** all other participants see the new waypoint and recomputed route via Yjs sync

View file

@ -1,16 +0,0 @@
## ADDED Requirements
### Requirement: Insert waypoint by clicking on route
The Planner SHALL allow users to insert a new waypoint by clicking on the rendered route polyline.
#### Scenario: Click on route between two waypoints
- **WHEN** a user clicks on the route polyline between waypoint 2 and waypoint 3
- **THEN** a new waypoint is inserted at position 3 (between the original waypoints 2 and 3) at the clicked location, and the route recomputes
#### Scenario: Waypoint snaps to route
- **WHEN** a user clicks near the route polyline
- **THEN** the new waypoint is placed at the closest point on the existing route geometry, not at the raw click position
#### Scenario: Split syncs to other participants
- **WHEN** a user inserts a waypoint by clicking on the route
- **THEN** all other participants see the new waypoint appear in the waypoint list and on the map via Yjs sync

View file

@ -1,45 +0,0 @@
## 1. BRouter Data Pipeline
- [x] 1.1 Modify `mergeGeoJsonSegments` to preserve 3D coordinates (lon, lat, ele) and track segment boundary indices
- [x] 1.2 Add `EnrichedRoute` interface with coordinates, segmentBoundaries, surfaces, and stats
- [x] 1.3 Store enriched route data (including segment boundaries) in Yjs routeData map
- [x] 1.4 Write unit test for segment boundary tracking across multi-waypoint routes
## 2. Route Splitting (click-to-insert)
- [x] 2.1 Make the route polyline clickable — add click event handler to route Polyline in PlannerMap
- [x] 2.2 On click, find nearest point on route geometry and determine which waypoint segment it belongs to (using segment boundaries)
- [x] 2.3 Insert new waypoint at the clicked position using `Y.Array.insert(segmentIndex + 1, [waypoint])`
- [x] 2.4 Add wider invisible polyline behind the route for easier click targeting (hit area)
## 3. Drag-to-Reshape (midpoint handles)
- [x] 3.1 Create MidpointHandles component — renders a CircleMarker at the geographic midpoint of each route segment
- [x] 3.2 Compute geographic midpoint from actual route geometry (not straight-line between waypoints)
- [x] 3.3 On drag end, insert waypoint at dropped position between the two adjacent waypoints
- [x] 3.4 Hide midpoint handles below zoom level 12
- [x] 3.5 Style handles: semi-transparent by default, opaque on hover, match waypoint color scheme
## 4. Route Coloring
- [x] 4.1 Create ColoredRoute component — renders multiple short L.Polyline segments with per-point colors
- [x] 4.2 Implement elevation gradient: normalize elevation values to 0-1 range, map to green→yellow→red
- [x] 4.3 Implement surface coloring: map BRouter surface type strings to a fixed color palette
- [x] 4.4 Add color mode state to Yjs routeData map ("plain" | "elevation" | "surface")
- [x] 4.5 Add color mode toggle button to session header (icon or dropdown)
- [x] 4.6 Fall back to plain mode when surface data is unavailable
- [x] 4.7 Add i18n keys for color mode labels (en + de)
## 5. Integration
- [x] 5.1 Replace single Polyline in PlannerMap with ColoredRoute + click handler + MidpointHandles
- [x] 5.2 Ensure click-to-split and drag-to-reshape work alongside no-go area drawing mode (disable route interactions when drawing)
- [x] 5.3 Update ElevationChart hover sync to work with the new colored route
## 6. Verify
- [x] 6.1 E2E test: click on route inserts waypoint at correct position
- [x] 6.2 E2E test: color mode toggle switches between plain/elevation/surface
- [x] 6.3 Unit test: segment boundary computation with 2, 3, and 5 waypoints
- [x] 6.4 Verify midpoint handles reposition after route recomputation
- [x] 6.5 Verify all interactions sync correctly between two browser tabs

View file

@ -1,16 +1,28 @@
## ADDED Requirements
### Requirement: Route computation from waypoints
The Planner SHALL compute a route between ordered waypoints by calling the BRouter HTTP API and returning the result as GeoJSON.
The Planner SHALL compute a route between ordered waypoints by calling the BRouter HTTP API with tiledesc enabled and returning the result as an EnrichedRoute, preserving per-point elevation, surface data, and segment boundary indices.
#### Scenario: Compute route with two waypoints
- **WHEN** the routing host submits two waypoints (start, end) with profile "trekking"
- **THEN** the BRouter API returns a GeoJSON route within 2 seconds
- **THEN** the BRouter API returns a route within 2 seconds
#### Scenario: Compute route with via points
- **WHEN** the routing host submits three or more waypoints
- **THEN** the BRouter API returns a route passing through all waypoints in order
#### Scenario: Per-point elevation preserved
- **WHEN** BRouter returns GeoJSON with 3D coordinates [lon, lat, ele]
- **THEN** the merged route response SHALL preserve elevation values for every coordinate point
#### Scenario: Segment boundaries tracked
- **WHEN** a route with N waypoints is computed (N-1 segments)
- **THEN** the response SHALL include an array of coordinate indices marking where each waypoint-to-waypoint segment begins
#### Scenario: Surface data extracted
- **WHEN** BRouter returns tiledesc messages with WayTags containing surface information
- **THEN** the response SHALL include a surface type string per coordinate point extracted from the WayTags (e.g., "asphalt", "gravel", "path")
### Requirement: Routing host election
The Planner SHALL elect one participant per session as the "routing host" who is responsible for sending waypoint changes to BRouter. Only the host SHALL make BRouter API calls.
@ -56,3 +68,10 @@ BRouter SHALL run as a separate Docker container with Germany RD5 segments mount
#### Scenario: BRouter container starts
- **WHEN** the Docker Compose stack starts
- **THEN** the BRouter container is reachable at its internal HTTP port and can compute routes using the mounted RD5 segments
### Requirement: BRouter routing with constraints
Route computation SHALL include no-go area polygons as avoidance constraints.
#### Scenario: Route with no-go areas
- **WHEN** the routing host computes a route and no-go areas exist
- **THEN** the BRouter request includes nogo parameters for each polygon

View file

@ -0,0 +1,12 @@
## Requirements
### Requirement: localStorage crash recovery
The Planner SHALL periodically save Yjs state to localStorage and recover it on reconnect.
#### Scenario: Browser crash recovery
- **WHEN** a user's browser crashes and they reopen the session
- **THEN** unsaved changes from localStorage are merged with the server state
#### Scenario: Clean exit
- **WHEN** a session syncs successfully
- **THEN** the localStorage backup is cleared

View file

@ -87,3 +87,14 @@ The Journal SHALL NOT support password-based authentication. All authentication
#### Scenario: No password field
- **WHEN** a user views the registration or login page
- **THEN** there is no password field
### Requirement: Magic link login
The magic link login flow SHALL deliver the link via email in production instead of logging to console.
#### Scenario: Production email delivery
- **WHEN** a user requests a magic link in production
- **THEN** the link is emailed to the user (not just logged to console)
#### Scenario: Dev mode unchanged
- **WHEN** a user requests a magic link in development
- **THEN** the link is returned directly for auto-redirect (existing behavior preserved)

View file

@ -34,16 +34,20 @@ The Planner map SHALL allow users to add, move, and delete waypoints by interact
- **THEN** the waypoint is removed and the change syncs via Yjs
### Requirement: Route visualization
The map SHALL display the computed route as a polyline on the map.
The map SHALL display the computed route as an interactive, optionally color-coded polyline on the map.
#### Scenario: Display route
- **WHEN** BRouter returns a route GeoJSON
- **THEN** the route is rendered as a colored polyline on the map
- **THEN** the route is rendered as a polyline on the map, colored according to the active color mode
#### Scenario: Route updates on waypoint change
- **WHEN** a waypoint is added, moved, or deleted
- **THEN** the route polyline updates after BRouter recomputes the route
#### Scenario: Ghost marker on hover
- **WHEN** the cursor is within 15 pixels of the route polyline
- **THEN** a transient ghost marker appears at the nearest route point, which can be clicked or dragged to insert a waypoint (see route-splitting and route-drag-reshape specs)
### Requirement: Elevation profile display
The Planner SHALL display an elevation profile chart for the current route.
@ -72,3 +76,14 @@ Other participants' cursors on the map SHALL be clearly visible with proper styl
#### Scenario: Cursor does not obscure map controls
- **WHEN** cursors are rendered
- **THEN** they appear below map controls (zoom, layer switcher) in z-index
### Requirement: Map polygon drawing
The Planner map SHALL support drawing and displaying no-go area polygons.
#### Scenario: Polygon tool
- **WHEN** a user activates the no-go area tool
- **THEN** they can draw a polygon by clicking points on the map
#### Scenario: Polygon display
- **WHEN** no-go areas exist in the session
- **THEN** they are rendered as semi-transparent red polygons on the map

View file

@ -0,0 +1,16 @@
## Requirements
### Requirement: Draw no-go areas
Users SHALL be able to draw polygons on the map that BRouter avoids when computing routes.
#### Scenario: Draw polygon
- **WHEN** a user activates the no-go area tool and draws a polygon on the map
- **THEN** the polygon is added to the Yjs doc and visible to all participants
#### Scenario: Route avoids no-go area
- **WHEN** a route is computed and a no-go area intersects the direct path
- **THEN** BRouter routes around the no-go area
#### Scenario: Delete no-go area
- **WHEN** a user deletes a no-go area polygon
- **THEN** it is removed from the Yjs doc and the route is recomputed

View file

@ -121,3 +121,10 @@ Users in a planning session SHALL see who else is present and be able to identif
#### Scenario: Leave notification
- **WHEN** a participant leaves the session
- **THEN** a brief toast shows "[name] left"
### Requirement: Planner session data model
The Yjs document SHALL include noGoAreas and notes fields alongside waypoints and routeData.
#### Scenario: Session with all fields
- **WHEN** a Planner session is active
- **THEN** the Yjs doc contains: waypoints (Y.Array), routeData (Y.Map), noGoAreas (Y.Array), notes (Y.Text)

View file

@ -0,0 +1,15 @@
## Requirements
### Requirement: Session creation rate limit
The Planner SHALL limit session creation to 10 per IP per hour.
#### Scenario: Rate limit exceeded
- **WHEN** an IP creates more than 10 sessions in one hour
- **THEN** the server responds with 429 Too Many Requests
### Requirement: BRouter call rate limit
The Planner SHALL limit route computations to 60 per session per hour.
#### Scenario: Routing rate limit exceeded
- **WHEN** a session exceeds 60 BRouter calls in one hour
- **THEN** the server responds with 429 and the client shows a "slow down" message

View file

@ -0,0 +1,42 @@
## Requirements
### Requirement: Route color modes
The Planner SHALL support multiple route color modes that visualize per-point data along the route.
#### Scenario: Default plain mode
- **WHEN** a route is first displayed
- **THEN** it renders as a single-color blue polyline (current behavior)
#### Scenario: Elevation color mode
- **WHEN** a user selects the "Elevation" color mode
- **THEN** the route polyline is colored with a gradient from green (low) through yellow (mid) to red (high), based on the elevation at each point
#### Scenario: Surface color mode
- **WHEN** a user selects the "Surface" color mode and surface data is available from BRouter tiledesc
- **THEN** the route polyline is colored by surface type (e.g., asphalt=gray, gravel=brown, path=green, track=orange)
#### Scenario: Surface data unavailable
- **WHEN** a user selects "Surface" color mode but BRouter did not return surface data
- **THEN** the route falls back to plain color mode
### Requirement: Color mode toggle
The Planner SHALL provide a UI control to switch between route color modes.
#### Scenario: Toggle control location
- **WHEN** a route is displayed
- **THEN** a color mode select dropdown is visible in the session header
#### Scenario: Color mode persists in session
- **WHEN** a user changes the color mode
- **THEN** the selection is stored in the Yjs routeData map and synced to all participants
### Requirement: Elevation chart color sync
The elevation profile chart SHALL use the same color gradient as the route when in elevation mode.
#### Scenario: Elevation mode chart coloring
- **WHEN** the color mode is set to "Elevation"
- **THEN** the elevation chart line and fill use the same green→yellow→red gradient as the route polyline
#### Scenario: Plain mode chart coloring
- **WHEN** the color mode is "Plain" or "Surface"
- **THEN** the elevation chart uses the default blue color

View file

@ -0,0 +1,28 @@
## Requirements
### Requirement: Ghost marker on route hover
The Planner SHALL display a transient ghost marker when the cursor is near the route polyline, allowing users to reshape the route by dragging.
#### Scenario: Ghost marker appears on hover
- **WHEN** the cursor moves within 15 pixels of the route polyline
- **THEN** a ghost marker (small blue circle) appears at the nearest route coordinate point
#### Scenario: Ghost marker disappears on leave
- **WHEN** the cursor moves more than 15 pixels away from the route polyline
- **THEN** the ghost marker disappears
#### Scenario: Drag ghost marker to reshape
- **WHEN** a user drags the ghost marker to a new position
- **THEN** a new waypoint is inserted between the two adjacent waypoints of the hovered segment, and the route recomputes through the new point
#### Scenario: Trailer lines during drag
- **WHEN** a user is dragging the ghost marker
- **THEN** dashed guide lines are shown connecting the ghost marker to the adjacent waypoints
#### Scenario: No text selection during drag
- **WHEN** a user drags the ghost marker
- **THEN** text selection is disabled on the page (via Leaflet's built-in L.Draggable)
#### Scenario: Reshape syncs to other participants
- **WHEN** a user reshapes the route by dragging the ghost marker
- **THEN** all other participants see the new waypoint and recomputed route via Yjs sync

View file

@ -0,0 +1,20 @@
## Requirements
### Requirement: Insert waypoint by clicking on route
The Planner SHALL allow users to insert a new waypoint by clicking the ghost marker that appears when hovering near the route.
#### Scenario: Click ghost marker to split
- **WHEN** a ghost marker is visible on the route and the user clicks it
- **THEN** a new waypoint is inserted at the ghost marker position between the appropriate adjacent waypoints, and the route recomputes
#### Scenario: Waypoint snaps to route
- **WHEN** the ghost marker appears near the route
- **THEN** it is positioned at the closest coordinate point on the existing route geometry, not at the raw cursor position
#### Scenario: No duplicate waypoint from map click
- **WHEN** a user clicks the ghost marker to insert a waypoint
- **THEN** the map click handler is suppressed so only one waypoint is inserted (not an additional one appended at the end)
#### Scenario: Split syncs to other participants
- **WHEN** a user inserts a waypoint by clicking the ghost marker
- **THEN** all other participants see the new waypoint appear in the waypoint list and on the map via Yjs sync

View file

@ -0,0 +1,12 @@
## Requirements
### Requirement: Collaborative session notes
Planner sessions SHALL have a shared text area for participants to write notes.
#### Scenario: Write notes
- **WHEN** a user types in the notes area
- **THEN** the text syncs in real-time to all other participants via Yjs
#### Scenario: Notes persist
- **WHEN** a user leaves and rejoins a session
- **THEN** the notes are still there (stored in Yjs doc)

View file

@ -0,0 +1,44 @@
## Requirements
### Requirement: Email sending interface
The system SHALL provide a provider-agnostic email sending function that supports HTML and plain-text content.
#### Scenario: Send email in production
- **WHEN** the system sends a transactional email in production
- **THEN** the email is delivered via the configured provider (Resend) to the recipient
#### Scenario: Dev mode skips sending
- **WHEN** the system sends a transactional email in development
- **THEN** the email content is logged to console and no external API is called
### Requirement: Magic link email
The system SHALL send an email containing the magic link when a user requests passwordless login.
#### Scenario: Magic link delivered
- **WHEN** a user requests a magic link login
- **THEN** an email is sent with a clickable link that logs them in
#### Scenario: Email content
- **WHEN** a magic link email is sent
- **THEN** it includes: the link, expiry time (15 minutes), and plain-text fallback
### Requirement: Welcome email
The system SHALL send a welcome email after successful registration.
#### Scenario: Welcome on registration
- **WHEN** a user completes passkey registration
- **THEN** a welcome email is sent to their registered email address
### Requirement: Email templates
Each email type SHALL have an HTML template with a plain-text fallback.
#### Scenario: Extensible template pattern
- **WHEN** a new email type is needed in the future
- **THEN** it can be added by creating a template function and calling sendEmail
### Requirement: Privacy disclosure
Email sending SHALL be documented in the privacy manifest.
#### Scenario: Privacy manifest updated
- **WHEN** transactional emails are enabled
- **THEN** the /privacy page documents: what emails are sent, that email addresses are shared with the email provider, and which provider is used