Synchronous path + rendering for the surface/waytype breakdown: - map-core `computeSurfaceBreakdown(coords, surfaces, highways)` → distance- weighted metres per surface + waytype category (unit-tested); - `SurfaceBreakdownSchema` in @trails-cool/api; - nullable `surfaceBreakdown` jsonb on routes + activities; - Planner `SaveToJournalButton` computes it from the BRouter waytags already in routeData and sends it; `api.save-to-journal` forwards it; the journal route callback validates + persists (journal is the authoritative validator); - `SurfaceBreakdown` component (stacked bars per dimension, map-core palettes, legend category · % · km largest-first, unknown → "other", hidden when empty) on route + activity detail; journal gains a @trails-cool/map-core dep; - i18n journal.surface.* (en + de). Phase 2 (async Overpass backfill + SSE for imports/uploads, and the e2e that seeds a breakdown) follows in a separate PR. Tests: computeSurfaceBreakdown unit (map-core 36); SurfaceBreakdown component (jsdom, journal 326). typecheck + lint green; verified in the browser. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2.9 KiB
2.9 KiB
1. Shared derivation + shape
- 1.1
computeSurfaceBreakdown(coordinates, surfaces, highways)in@trails-cool/map-core→{ surface, highway }metres; unit-tested. - 1.2
SurfaceBreakdownSchema(Zod) in@trails-cool/api.
2. Schema
- 2.1 Nullable
surfaceBreakdownjsonb onjournal.routesandjournal.activities; pushed to dev + e2e DBs.
3. Path 1 — synchronous (Planner routes)
- 3.1
SaveToJournalButtoncomputes the breakdown fromreadComputedRouteand sends it;api.save-to-journalforwards it (journal is the authoritative validator — planner has no@trails-cool/apidep). - 3.2 Journal route callback validates the optional field with
SurfaceBreakdownSchemaand persists viaupdateRoute(overwrite on re-save).
4. Render
- 4.1 Route + activity detail loaders expose
surfaceBreakdown. - 4.2
SurfaceBreakdowncomponent (stacked bar per dimension,map-corepalettes, legend category · % · km largest-first, unknown → "other", hidden when empty); mounted onroutes.$id.tsxandactivities.$id.tsx. - 4.3 i18n
journal.surface.*(surface/waytype labels, common categories, "other") en + de.
5. Path 2 — async backfill + SSE (PHASE 2 — follow-up PR)
- 5.1 Journal-side Overpass client:
way[highway]in bbox + parse (mirror the planner'sbuildQuery/parseResponse+ rate-limit handling; route via the/api/overpassproxy). - 5.2 Map-match helper: nearest OSM way per route segment → surface/highway; unmatched → unknown. Unit-test the matcher on a small fixture.
- 5.3 Typed
surface-backfillpg-boss job ({ kind, id }): load geometry → skip if breakdown exists → Overpass → map-match → store. Idempotent, retry-safe, best-effort. - 5.4 Enqueue the job after GPX import (Komoot/Garmin) and manual upload; add a one-off sweep entrypoint for pre-existing rows.
- 5.5 Emit a
surface_breakdownSSE event ({ kind, id }) on completion; detail page subscribes (EventSource hook) and re-fetches the breakdown when it matches; optional "computing…" state.
6. Privacy
- 6.1 Document the backfill's Overpass lookup in the privacy manifest; route via the proxy (self-hostable Overpass is the long-term answer — see
docs/ideas/self-host-overpass).
7. Tests & checks
- 7.1 Unit:
computeSurfaceBreakdownweighting + unknown bucket (map-matcher unit is Phase 2). - 7.2 Component (jsdom): bars/proportions, largest-first, hidden when empty/null.
- 7.3 E2E: a route/activity with a stored breakdown shows the bars — Phase 2 (needs a seed path; deferred to land with the backfill that creates the data). Phase 1 render verified in the browser + component test.
- 7.4 Phase 1: typecheck + lint + unit (map-core 36, journal 326) green; verified in the browser.