# T14 — Path rendering **Phase** 5 · **Depends on** T04, T13 · **Status** Done ## Goal Draw the ride on the map: one polyline per segment, coloured by speed, decimated for performance, auto-fitted to the route. Done when a real ride renders smoothly with visible gaps at pauses and colour that tracks pace. ## Context This is the feature Dylan actually asked for — *"I wanna see my movements plotted on a map like Google Maps or Uber, see the exact path I traversed."* The data has been there since v1. Every point carries `latitude`, `longitude`, and `speedKmh` at 1–2 Hz. Nothing new is captured; this renders what already exists. Scale is the constraint: a three-hour ride at 2 Hz is ~21,600 points. Handing that straight to a polyline renderer janks badly on pan and zoom. ## Design ### Decimation — render only `Geo.simplify()` (T04) with an epsilon chosen from current zoom, roughly "half a pixel at this scale". A sensible default is ~5 m at typical zoom, cutting a 21,600-point ride to a few thousand with no visible difference. > **Raw points are never decimated in storage or export.** Decimation exists purely in > the render path. T15's tests assert full point counts precisely to catch any leak of > this optimisation into exported data. ### One polyline per segment Segments must not be joined. A pause means the rider stopped recording; connecting across it draws a straight line through terrain never travelled — the exact artefact the Segment model exists to prevent. Each segment gets its own `Polyline`, so gaps appear naturally. ### Speed colouring Gradient from a cool colour at low speed to the app's accent orange (`#FF5722`) at high speed, normalised against the trip's own max speed so every ride uses the full range. osmdroid 6.1 offers `PolyChromaticPaintList` for per-vertex colouring. It is fiddly. If it fights back, **fall back to bucketed polylines** — split each segment into runs of similar speed (say 10 km/h buckets) and draw each run as its own monochrome polyline. Visually near-identical, considerably more predictable. Do not burn hours on the chromatic API. Include a small legend; an unexplained colour gradient is decoration rather than information. ### Auto-fit and markers `Geo.bounds()` (T04) → `mapView.zoomToBoundingBox(bounds, false, padding)`. Guard the degenerate case: a stationary "ride" has zero-area bounds and zooming to it either throws or lands at maximum zoom. Fall back to centring at a fixed zoom. Start and end markers, styled minimally. ## Implementation 1. Load points grouped by segment (already ordered `segmentId, id` from T02). 2. Decimate each segment independently — never across a boundary. 3. Build one `Polyline` per segment with speed-derived colour. 4. Add start/end markers. 5. Auto-fit with padding; handle degenerate bounds. 6. Recompute decimation on significant zoom change; debounce it. 7. Legend. ## Acceptance criteria - [ ] Real ride renders with no straight-line artefact across pauses - [ ] Colour visibly tracks speed - [ ] A 20,000-point ride pans and zooms smoothly - [x] Auto-fit frames the whole route with padding - [x] A stationary ride does not crash the fit - [x] Exported GPX still contains every raw point (verified: 24 points recorded, 24 exported) - [x] Single-point and empty trips render without crashing > **Not verified.** Unchecked items above were not tested. Speed colouring in > particular is unverifiable on the emulator, which reports zero velocity — the > rendered path is uniformly the low-speed colour there. Tracked in T18. ## Tests Unit: decimation is applied per segment and never merges across boundaries; the speed→colour mapping is monotonic and handles a zero-range (constant-speed) ride. Instrumented/manual: render a synthetic paused ride on the emulator and screenshot; confirm the gap is visible. Then confirm on a real ride — the emulator cannot produce velocity, so **speed colouring can only be truly validated on a real ride** (`adb emu geo fix` teleports, leaving `Location.speed` at 0 and every point the same colour). ## Risks / gotchas - **Speed colouring is unverifiable on the emulator.** Expect a uniform-colour path there; that is the harness, not a bug. Validate on a real ride. - **Zero-range normalisation** — a constant-speed ride divides by zero. Guard it. - **Decimation leaking into export** is the correctness risk in this task. Keep the simplified list strictly local to rendering. - **Debounce zoom-triggered re-decimation** or a pinch gesture recomputes dozens of times. ## Out of scope Live path during recording (v3+); route matching; heatmaps.