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>
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.