|
| 1 | +# Coverage playback controls |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +Add media-style playback controls to the coverage view, allowing users to animate coverage data over time (like Environment Canada's weather radar). Includes a follow-vehicle mode that tracks a selected vehicle during playback. |
| 6 | + |
| 7 | +## State |
| 8 | + |
| 9 | +`PlowApp` gains a `playback` object: |
| 10 | + |
| 11 | +```js |
| 12 | +{ |
| 13 | + playing: false, |
| 14 | + startVal: 0, // left handle position at play start |
| 15 | + endVal: 1000, // right handle position at play start |
| 16 | + speed: 15, // seconds to complete playback |
| 17 | + followVehicleId: null, |
| 18 | + startTime: null, // Date.now() when playback started |
| 19 | + animFrame: null, // requestAnimationFrame ID |
| 20 | +} |
| 21 | +``` |
| 22 | + |
| 23 | +## HTML controls |
| 24 | + |
| 25 | +Below `#time-slider`, a new `#playback-controls` div: |
| 26 | + |
| 27 | +- **Play** button -- starts playback, locks UI |
| 28 | +- **Stop** button -- exits playback, unlocks UI |
| 29 | +- **Speed** `<select>`: "15s" (default), "30s", "1m", "5m", "Realtime" |
| 30 | +- **Follow** `<select>`: "None" (default), then vehicle descriptions from coverage data |
| 31 | + |
| 32 | +## UI locking |
| 33 | + |
| 34 | +When playing: |
| 35 | +- Slider handles are disabled (`timeSliderEl.setAttribute("disabled", true)`) |
| 36 | +- Time presets, date picker, view toggle (lines/heatmap) are disabled |
| 37 | +- Speed dropdown is locked (can't change mid-playback) |
| 38 | +- Follow dropdown remains interactive (can start/stop following mid-playback) |
| 39 | + |
| 40 | +Stop unlocks everything. |
| 41 | + |
| 42 | +## Animation loop |
| 43 | + |
| 44 | +1. On play: record `startVal` (left handle), `endVal` (right handle). |
| 45 | +2. Set right handle to `startVal` (empty view -- start of playback). |
| 46 | +3. Compute `durationMs` from speed: 15000, 30000, 60000, 300000, or for "Realtime" the actual time span (`coverageUntil - coverageSince`) * `(endVal - startVal) / 1000`. |
| 47 | +4. `requestAnimationFrame` loop: |
| 48 | + - `elapsed = Date.now() - startTime` |
| 49 | + - `progress = Math.min(elapsed / durationMs, 1)` |
| 50 | + - `currentVal = startVal + progress * (endVal - startVal)` |
| 51 | + - `timeSliderEl.noUiSlider.set([startVal, currentVal])` -- triggers existing `renderCoverage` via the slider's `update` handler |
| 52 | + - If following a vehicle, interpolate position and `easeTo` |
| 53 | + - If `progress >= 1`: stop animation, set right handle to `endVal`, set `playing = false`, re-enable controls |
| 54 | + |
| 55 | +No auto-loop -- playback stops at the end, user must press play again. |
| 56 | + |
| 57 | +## Follow vehicle |
| 58 | + |
| 59 | +### Dropdown population |
| 60 | + |
| 61 | +Populated from `this.coverageData.features` when: |
| 62 | +- Coverage data loads |
| 63 | +- Type filter checkboxes change |
| 64 | + |
| 65 | +Extracts unique `(vehicle_id, description)` pairs, filtered by current type filter. Sorted alphabetically by description. |
| 66 | + |
| 67 | +### Camera behavior |
| 68 | + |
| 69 | +On each animation frame when following: |
| 70 | +1. Convert `currentVal` to a time via `sliderToTime`. |
| 71 | +2. Search the followed vehicle's trail segments for the one containing that time. |
| 72 | +3. Interpolate position between nearest coordinates using timestamp proportion. |
| 73 | +4. `map.easeTo({ center: [lng, lat], duration: 300 })` -- 300ms ease smooths frame-to-frame jumps. |
| 74 | +5. If vehicle has no position at current time (in a gap), skip the pan. |
| 75 | + |
| 76 | +Selecting "None" stops following; map stays wherever it is. |
| 77 | + |
| 78 | +## Implementation plan |
| 79 | + |
| 80 | +### Step 1: HTML + CSS |
| 81 | +- Add `#playback-controls` div below `#time-slider` in index.html |
| 82 | +- Style play/stop buttons as icon buttons, dropdowns match existing theme |
| 83 | +- Add a hint: "Animate coverage data over time" |
| 84 | + |
| 85 | +### Step 2: PlowApp playback state |
| 86 | +- Add `playback` object to constructor |
| 87 | +- Add `startPlayback()` method: lock UI, record positions, start rAF loop |
| 88 | +- Add `stopPlayback()` method: cancel rAF, unlock UI, restore handle |
| 89 | +- Add `playbackTick()` method: the per-frame logic |
| 90 | + |
| 91 | +### Step 3: Follow vehicle |
| 92 | +- Add `populateFollowDropdown()` method, called after coverage load and filter change |
| 93 | +- Add `interpolateVehiclePosition(vehicleId, time)` method |
| 94 | +- Integrate into `playbackTick()`: if following, interpolate + easeTo |
| 95 | + |
| 96 | +### Step 4: Event wiring |
| 97 | +- Play button calls `startPlayback()` |
| 98 | +- Stop button calls `stopPlayback()` |
| 99 | +- Speed dropdown reads value into `playback.speed` before play |
| 100 | +- Follow dropdown sets `playback.followVehicleId` |
| 101 | + |
| 102 | +### Step 5: UI locking |
| 103 | +- `lockPlaybackUI()` / `unlockPlaybackUI()` helpers |
| 104 | +- Disable: slider, presets, date picker, view toggle, speed dropdown |
| 105 | +- Keep interactive: follow dropdown, stop button |
0 commit comments