- 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>
5.9 KiB
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:
- Preserve the full 3-element coordinates (already done, but not exposed)
- Track segment boundaries (array of indices where each waypoint segment starts)
- Optionally parse
properties.messagesfor 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:
- Plain: Current single-color blue polyline (default)
- Elevation: Gradient by elevation
- 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:
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.Polylineinstances 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
tiledescparameter 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.Polylineweightfor 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.