trails/openspec/changes/archive/2026-04-12-elevation-map-interaction/design.md
Ullrich Schäfer 471ecb2309
Archive elevation-map-interaction, sync spec to main
Synced new spec (chart ↔ map interaction, mobile touch, iOS safe area,
responsive header) to openspec/specs/elevation-map-interaction/.

All 4 artifacts complete. All 14 tasks complete.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 19:43:03 +02:00

2.9 KiB

Context

The Planner has one-directional interaction: hovering the elevation chart shows a red dot on the map via highlightPosition state in SessionView. The reverse direction (map → chart) and click/drag interactions don't exist.

The route coordinates are stored in Yjs routeData as a JSON array of [lon, lat, ele] points. The elevation chart extracts these into ElevationPoint[] with cumulative distance. The map renders the route via ColoredRoute as Leaflet polylines.

Decisions

D1: Route hover → chart highlight

Add a mousemove handler on the ColoredRoute polyline segments. On hover, find the closest route coordinate index, compute the cumulative distance at that index, and pass it up to SessionView as highlightChartDistance. The ElevationChart receives this distance and draws the crosshair at that position — reusing the existing drawChart(highlightIdx) mechanism.

To avoid expensive per-pixel distance calculations on every mousemove, use Leaflet's closestLayerPoint or project to screen coordinates and find the nearest point in the coordinate array.

D2: Chart click → map pan

Add an onClick handler to the elevation chart canvas. Convert the click x-position to a distance along the route, find the corresponding coordinate, and call map.panTo([lat, lon]) via a callback. The map reference is exposed via window.__leafletMap (already used by E2E tests).

D3: Chart drag-select → map zoom

Add mousedown/mousemove/mouseup handlers to the chart canvas for range selection. While dragging, draw a semi-transparent overlay on the selected range. On mouseup, compute the route coordinates within the selected distance range and call map.fitBounds() on their bounding box.

A visual "reset zoom" button appears after drag-zoom to return to the full route view.

D4: State flow

SessionView
├── highlightPosition: [lat, lon] | null  (chart → map, existing)
├── highlightChartDistance: number | null  (map → chart, new)
└── onMapFitBounds: (bounds) => void      (chart → map, new)

ElevationChart
├── onHover(position)          — existing, chart → map
├── onClick(position)          — new, chart → map pan
├── onDragSelect(bounds)       — new, chart → map zoom
└── highlightDistance           — new, map → chart

PlannerMap / ColoredRoute
└── onRouteHover(distance)     — new, map → chart

Risks / Trade-offs

  • Performance: Route hover on the map triggers distance lookup on every mousemove. Mitigate by throttling and using screen-space projection.
  • Polyline interactivity: ColoredRoute renders many small polyline segments. Making them all interactive adds event listeners. Alternative: use a single invisible overlay polyline for hover detection.
  • Drag conflict: Chart drag-select must not conflict with chart hover. Use a minimum drag distance threshold (5px) before entering drag mode.