Add route interactions: click-to-split, drag-to-reshape, colored route rendering
- 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) <noreply@anthropic.com>
This commit is contained in:
parent
fe3621aab3
commit
b080a15fb1
22 changed files with 1259 additions and 170 deletions
132
openspec/changes/planner-route-interactions/design.md
Normal file
132
openspec/changes/planner-route-interactions/design.md
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
## 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue