trails/openspec/changes/elevation-map-interaction/design.md
Ullrich Schäfer 3345ef127e
Add bidirectional elevation chart ↔ map interaction
Three new interactions:

1. Route hover → chart: Invisible interactive polyline (weight 20,
   opacity 0) detects mouse hover on the route, computes cumulative
   distance, and highlights the corresponding position on the chart.

2. Chart click → map pan: Clicking the elevation chart pans the map
   to center on that point along the route. Uses 5px threshold to
   distinguish from drag.

3. Chart drag-select → map zoom: Dragging a range on the chart shows
   a blue selection overlay, then zooms the map to fit the route
   coordinates in that range. "Reset zoom" button appears to restore
   the full route view.

State flow avoids feedback loops: external highlights (from map hover)
set the chart crosshair without re-emitting onHover back to the map.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 19:23:37 +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.