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>
This commit is contained in:
Ullrich Schäfer 2026-04-12 19:43:03 +02:00
parent a9a576081e
commit 471ecb2309
No known key found for this signature in database
GPG key ID: A32FF691A0F752D9
6 changed files with 79 additions and 0 deletions

View file

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-11

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

View file

@ -0,0 +1,21 @@
## Why
The elevation chart and map currently have one-directional interaction: hovering the chart shows a red dot on the map. But hovering the route on the map doesn't highlight the chart, clicking the chart doesn't pan the map, and there's no way to zoom into a section of the route from the chart. This makes it hard to correlate what you see on the map with what you see in the elevation profile.
## What Changes
- **Route hover → chart marker**: Hovering over the route polyline on the map shows the corresponding position on the elevation chart with a crosshair
- **Chart click → map pan**: Clicking on the elevation chart pans the map to center on that point along the route
- **Chart drag → map zoom**: Dragging/selecting a range on the elevation chart zooms the map to fit that section of the route
## Capabilities
### Modified Capabilities
- `route-coloring`: Elevation chart gains bidirectional interaction with the map
- `map-display`: Route polyline hover reports position back to the chart
## Impact
- `ElevationChart.tsx`: Add click handler (pan map), drag-select handler (zoom map), accept highlighted index from map hover
- `PlannerMap.tsx` / `ColoredRoute.tsx`: Route polyline hover finds closest point and reports index/position back to SessionView
- `SessionView.tsx`: Wire bidirectional state between chart and map

View file

@ -0,0 +1,79 @@
## 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
### Requirement: Mobile touch interaction
The elevation chart SHALL support touch-based interaction on mobile devices.
#### Scenario: Single touch scrub
- **WHEN** a user touches and drags on the chart with one finger
- **THEN** the crosshair follows the finger position along the chart
- **AND** the map highlight dot follows in real-time
#### Scenario: Tap to pan
- **WHEN** a user taps the chart (touch without significant movement)
- **THEN** the map pans to that point along the route
#### Scenario: Two-finger range select
- **WHEN** a user places two fingers on the chart
- **THEN** the area between the fingers is highlighted as a selection range
- **AND** on release, the map zooms to fit the route coordinates in that range
#### Scenario: No page scroll on touch
- **WHEN** a user touches the elevation chart
- **THEN** page scrolling is prevented (touch-none CSS + preventDefault)
### Requirement: iOS safe area support
The Planner layout SHALL respect iOS safe area insets.
#### Scenario: Notch/Dynamic Island clearance
- **WHEN** the Planner is viewed on an iOS device with a notch or Dynamic Island
- **THEN** the header bar is padded below the safe area inset
#### Scenario: Home indicator clearance
- **WHEN** the Planner is viewed on an iOS device without a physical home button
- **THEN** the elevation chart area is padded above the home indicator
#### Scenario: No accidental page zoom
- **WHEN** a user pinches on the Planner UI
- **THEN** the browser-level page zoom does not trigger (viewport user-scalable=no)
### Requirement: Responsive mobile header
The Planner header SHALL adapt to small screens by hiding secondary controls.
#### Scenario: Small screen header
- **WHEN** the Planner is viewed on a mobile device
- **THEN** only the profile selector and export button are visible
- **AND** participant list, undo/redo, connection status, and computing text are hidden
#### Scenario: Desktop header
- **WHEN** the Planner is viewed on a desktop screen
- **THEN** all header controls are visible

View 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)