From ba044610b79ed7e91355444319ce14a99bf46a36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Thu, 26 Mar 2026 08:21:24 +0100 Subject: [PATCH] Add route interactions: click-to-split, drag-to-reshape, colored route rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Enrich BRouter response with per-point 3D coordinates and segment boundary tracking (EnrichedRoute interface) - ColoredRoute component: plain, elevation gradient (green→yellow→red), and surface color modes with invisible wide polyline for click targeting - Click-to-split: click on route polyline inserts waypoint at nearest point, mapped to correct segment via boundary indices - MidpointHandles: draggable CircleMarkers at route segment midpoints for reshaping, hidden below zoom 12, opaque on hover - Color mode toggle (select) synced via Yjs routeData - i18n keys for color mode labels (en + de) - Unit tests for segment boundary tracking (13 tests) - E2E tests for enriched route response and color mode toggle Co-Authored-By: Claude Opus 4.6 (1M context) --- .../.openspec.yaml | 0 .../2026-03-26-planner-features}/design.md | 0 .../2026-03-26-planner-features}/proposal.md | 0 .../specs/brouter-integration/spec.md | 0 .../specs/crash-recovery/spec.md | 0 .../specs/map-display/spec.md | 0 .../specs/no-go-areas/spec.md | 0 .../specs/planner-session/spec.md | 0 .../specs/rate-limiting/spec.md | 0 .../specs/session-notes/spec.md | 0 .../2026-03-26-planner-features}/tasks.md | 0 .../.openspec.yaml | 0 .../design.md | 127 +++++++++++++++++ .../proposal.md | 54 +++++++ .../specs/brouter-integration/spec.md | 8 +- .../specs/map-display/spec.md | 10 +- .../specs/route-coloring/spec.md | 17 ++- .../specs/route-drag-reshape/spec.md | 28 ++++ .../specs/route-splitting/spec.md | 20 +++ .../tasks.md | 36 +++++ .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/journal-auth/spec.md | 0 .../specs/transactional-emails/spec.md | 0 .../2026-03-26-transactional-emails}/tasks.md | 0 .../planner-route-interactions/design.md | 132 ------------------ .../planner-route-interactions/proposal.md | 56 -------- .../specs/route-drag-reshape/spec.md | 20 --- .../specs/route-splitting/spec.md | 16 --- .../planner-route-interactions/tasks.md | 45 ------ openspec/specs/brouter-integration/spec.md | 23 ++- openspec/specs/crash-recovery/spec.md | 12 ++ openspec/specs/journal-auth/spec.md | 11 ++ openspec/specs/map-display/spec.md | 19 ++- openspec/specs/no-go-areas/spec.md | 16 +++ openspec/specs/planner-session/spec.md | 7 + openspec/specs/rate-limiting/spec.md | 15 ++ openspec/specs/route-coloring/spec.md | 42 ++++++ openspec/specs/route-drag-reshape/spec.md | 28 ++++ openspec/specs/route-splitting/spec.md | 20 +++ openspec/specs/session-notes/spec.md | 12 ++ openspec/specs/transactional-emails/spec.md | 44 ++++++ 43 files changed, 533 insertions(+), 285 deletions(-) rename openspec/changes/{planner-features => archive/2026-03-26-planner-features}/.openspec.yaml (100%) rename openspec/changes/{planner-features => archive/2026-03-26-planner-features}/design.md (100%) rename openspec/changes/{planner-features => archive/2026-03-26-planner-features}/proposal.md (100%) rename openspec/changes/{planner-features => archive/2026-03-26-planner-features}/specs/brouter-integration/spec.md (100%) rename openspec/changes/{planner-features => archive/2026-03-26-planner-features}/specs/crash-recovery/spec.md (100%) rename openspec/changes/{planner-features => archive/2026-03-26-planner-features}/specs/map-display/spec.md (100%) rename openspec/changes/{planner-features => archive/2026-03-26-planner-features}/specs/no-go-areas/spec.md (100%) rename openspec/changes/{planner-features => archive/2026-03-26-planner-features}/specs/planner-session/spec.md (100%) rename openspec/changes/{planner-features => archive/2026-03-26-planner-features}/specs/rate-limiting/spec.md (100%) rename openspec/changes/{planner-features => archive/2026-03-26-planner-features}/specs/session-notes/spec.md (100%) rename openspec/changes/{planner-features => archive/2026-03-26-planner-features}/tasks.md (100%) rename openspec/changes/{planner-route-interactions => archive/2026-03-26-planner-route-interactions}/.openspec.yaml (100%) create mode 100644 openspec/changes/archive/2026-03-26-planner-route-interactions/design.md create mode 100644 openspec/changes/archive/2026-03-26-planner-route-interactions/proposal.md rename openspec/changes/{planner-route-interactions => archive/2026-03-26-planner-route-interactions}/specs/brouter-integration/spec.md (64%) rename openspec/changes/{planner-route-interactions => archive/2026-03-26-planner-route-interactions}/specs/map-display/spec.md (58%) rename openspec/changes/{planner-route-interactions => archive/2026-03-26-planner-route-interactions}/specs/route-coloring/spec.md (65%) create mode 100644 openspec/changes/archive/2026-03-26-planner-route-interactions/specs/route-drag-reshape/spec.md create mode 100644 openspec/changes/archive/2026-03-26-planner-route-interactions/specs/route-splitting/spec.md create mode 100644 openspec/changes/archive/2026-03-26-planner-route-interactions/tasks.md rename openspec/changes/{transactional-emails => archive/2026-03-26-transactional-emails}/.openspec.yaml (100%) rename openspec/changes/{transactional-emails => archive/2026-03-26-transactional-emails}/design.md (100%) rename openspec/changes/{transactional-emails => archive/2026-03-26-transactional-emails}/proposal.md (100%) rename openspec/changes/{transactional-emails => archive/2026-03-26-transactional-emails}/specs/journal-auth/spec.md (100%) rename openspec/changes/{transactional-emails => archive/2026-03-26-transactional-emails}/specs/transactional-emails/spec.md (100%) rename openspec/changes/{transactional-emails => archive/2026-03-26-transactional-emails}/tasks.md (100%) delete mode 100644 openspec/changes/planner-route-interactions/design.md delete mode 100644 openspec/changes/planner-route-interactions/proposal.md delete mode 100644 openspec/changes/planner-route-interactions/specs/route-drag-reshape/spec.md delete mode 100644 openspec/changes/planner-route-interactions/specs/route-splitting/spec.md delete mode 100644 openspec/changes/planner-route-interactions/tasks.md create mode 100644 openspec/specs/crash-recovery/spec.md create mode 100644 openspec/specs/no-go-areas/spec.md create mode 100644 openspec/specs/rate-limiting/spec.md create mode 100644 openspec/specs/route-coloring/spec.md create mode 100644 openspec/specs/route-drag-reshape/spec.md create mode 100644 openspec/specs/route-splitting/spec.md create mode 100644 openspec/specs/session-notes/spec.md create mode 100644 openspec/specs/transactional-emails/spec.md diff --git a/openspec/changes/planner-features/.openspec.yaml b/openspec/changes/archive/2026-03-26-planner-features/.openspec.yaml similarity index 100% rename from openspec/changes/planner-features/.openspec.yaml rename to openspec/changes/archive/2026-03-26-planner-features/.openspec.yaml diff --git a/openspec/changes/planner-features/design.md b/openspec/changes/archive/2026-03-26-planner-features/design.md similarity index 100% rename from openspec/changes/planner-features/design.md rename to openspec/changes/archive/2026-03-26-planner-features/design.md diff --git a/openspec/changes/planner-features/proposal.md b/openspec/changes/archive/2026-03-26-planner-features/proposal.md similarity index 100% rename from openspec/changes/planner-features/proposal.md rename to openspec/changes/archive/2026-03-26-planner-features/proposal.md diff --git a/openspec/changes/planner-features/specs/brouter-integration/spec.md b/openspec/changes/archive/2026-03-26-planner-features/specs/brouter-integration/spec.md similarity index 100% rename from openspec/changes/planner-features/specs/brouter-integration/spec.md rename to openspec/changes/archive/2026-03-26-planner-features/specs/brouter-integration/spec.md diff --git a/openspec/changes/planner-features/specs/crash-recovery/spec.md b/openspec/changes/archive/2026-03-26-planner-features/specs/crash-recovery/spec.md similarity index 100% rename from openspec/changes/planner-features/specs/crash-recovery/spec.md rename to openspec/changes/archive/2026-03-26-planner-features/specs/crash-recovery/spec.md diff --git a/openspec/changes/planner-features/specs/map-display/spec.md b/openspec/changes/archive/2026-03-26-planner-features/specs/map-display/spec.md similarity index 100% rename from openspec/changes/planner-features/specs/map-display/spec.md rename to openspec/changes/archive/2026-03-26-planner-features/specs/map-display/spec.md diff --git a/openspec/changes/planner-features/specs/no-go-areas/spec.md b/openspec/changes/archive/2026-03-26-planner-features/specs/no-go-areas/spec.md similarity index 100% rename from openspec/changes/planner-features/specs/no-go-areas/spec.md rename to openspec/changes/archive/2026-03-26-planner-features/specs/no-go-areas/spec.md diff --git a/openspec/changes/planner-features/specs/planner-session/spec.md b/openspec/changes/archive/2026-03-26-planner-features/specs/planner-session/spec.md similarity index 100% rename from openspec/changes/planner-features/specs/planner-session/spec.md rename to openspec/changes/archive/2026-03-26-planner-features/specs/planner-session/spec.md diff --git a/openspec/changes/planner-features/specs/rate-limiting/spec.md b/openspec/changes/archive/2026-03-26-planner-features/specs/rate-limiting/spec.md similarity index 100% rename from openspec/changes/planner-features/specs/rate-limiting/spec.md rename to openspec/changes/archive/2026-03-26-planner-features/specs/rate-limiting/spec.md diff --git a/openspec/changes/planner-features/specs/session-notes/spec.md b/openspec/changes/archive/2026-03-26-planner-features/specs/session-notes/spec.md similarity index 100% rename from openspec/changes/planner-features/specs/session-notes/spec.md rename to openspec/changes/archive/2026-03-26-planner-features/specs/session-notes/spec.md diff --git a/openspec/changes/planner-features/tasks.md b/openspec/changes/archive/2026-03-26-planner-features/tasks.md similarity index 100% rename from openspec/changes/planner-features/tasks.md rename to openspec/changes/archive/2026-03-26-planner-features/tasks.md diff --git a/openspec/changes/planner-route-interactions/.openspec.yaml b/openspec/changes/archive/2026-03-26-planner-route-interactions/.openspec.yaml similarity index 100% rename from openspec/changes/planner-route-interactions/.openspec.yaml rename to openspec/changes/archive/2026-03-26-planner-route-interactions/.openspec.yaml diff --git a/openspec/changes/archive/2026-03-26-planner-route-interactions/design.md b/openspec/changes/archive/2026-03-26-planner-route-interactions/design.md new file mode 100644 index 0000000..794130a --- /dev/null +++ b/openspec/changes/archive/2026-03-26-planner-route-interactions/design.md @@ -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 `