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>
This commit is contained in:
parent
9a46a7eac0
commit
3345ef127e
6 changed files with 369 additions and 6 deletions
69
openspec/changes/elevation-map-interaction/design.md
Normal file
69
openspec/changes/elevation-map-interaction/design.md
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
## 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.
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Route hover highlights elevation chart
|
||||
Hovering over the route polyline on the map SHALL highlight the corresponding position on the elevation chart.
|
||||
|
||||
#### Scenario: Hover route on map
|
||||
- **WHEN** a user hovers over the route polyline on the map
|
||||
- **THEN** the elevation chart shows a crosshair at the corresponding distance along the route
|
||||
|
||||
#### Scenario: Leave route on map
|
||||
- **WHEN** a user moves the mouse away from the route polyline
|
||||
- **THEN** the elevation chart crosshair disappears
|
||||
|
||||
### Requirement: Chart click pans map
|
||||
Clicking on the elevation chart SHALL pan the map to center on that point along the route.
|
||||
|
||||
#### Scenario: Click chart
|
||||
- **WHEN** a user clicks on the elevation chart
|
||||
- **THEN** the map pans to center on the corresponding route coordinate
|
||||
|
||||
### Requirement: Chart drag-select zooms map
|
||||
Dragging a range on the elevation chart SHALL zoom the map to fit that section of the route.
|
||||
|
||||
#### Scenario: Drag select range
|
||||
- **WHEN** a user clicks and drags horizontally on the elevation chart
|
||||
- **THEN** a visual highlight shows the selected range
|
||||
- **AND** on mouse release, the map zooms to fit the route coordinates within that range
|
||||
|
||||
#### Scenario: Reset zoom
|
||||
- **WHEN** the map has been zoomed via chart drag-select
|
||||
- **THEN** a reset button appears to return to the full route view
|
||||
25
openspec/changes/elevation-map-interaction/tasks.md
Normal file
25
openspec/changes/elevation-map-interaction/tasks.md
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
## 1. Route Hover → Chart Highlight
|
||||
|
||||
- [x] 1.1 Add invisible interactive overlay polyline on the route in PlannerMap for hover detection (avoids adding listeners to every ColoredRoute segment)
|
||||
- [x] 1.2 On polyline hover, find closest route coordinate index and compute cumulative distance
|
||||
- [x] 1.3 Pass `highlightChartDistance` up from PlannerMap to SessionView
|
||||
- [x] 1.4 Accept `highlightDistance` prop in ElevationChart, find closest point by distance, draw crosshair
|
||||
|
||||
## 2. Chart Click → Map Pan
|
||||
|
||||
- [x] 2.1 Add click handler to ElevationChart canvas — convert x-position to route coordinate
|
||||
- [x] 2.2 Call `onClickPosition([lat, lon])` callback to pan the map
|
||||
- [x] 2.3 Wire the callback through SessionView to call `map.panTo()` via the exposed map ref
|
||||
|
||||
## 3. Chart Drag-Select → Map Zoom
|
||||
|
||||
- [x] 3.1 Add mousedown/mousemove/mouseup handlers for drag selection on ElevationChart canvas
|
||||
- [x] 3.2 Draw semi-transparent overlay rectangle during drag
|
||||
- [x] 3.3 On mouseup, compute bounding box of route coordinates in the selected distance range
|
||||
- [x] 3.4 Call `onDragSelect(bounds)` callback to zoom the map via `map.fitBounds()`
|
||||
- [x] 3.5 Show "Reset zoom" button after drag-zoom, clicking it returns to full route bounds
|
||||
|
||||
## 4. Testing
|
||||
|
||||
- [x] 4.1 E2E test: hover chart → map dot appears (existing, verify still works)
|
||||
- [x] 4.2 E2E test: click chart → map pans (verify map center changes)
|
||||
Loading…
Add table
Add a link
Reference in a new issue