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>
15 lines
501 B
TypeScript
15 lines
501 B
TypeScript
import { z } from "zod";
|
|
|
|
/**
|
|
* Distance-weighted surface/waytype breakdown (metres per category). Shared
|
|
* wire shape for the planner→journal handoff and the route/activity read APIs.
|
|
* Mirrors `SurfaceBreakdown` in `@trails-cool/map-core`.
|
|
*/
|
|
const MetresByCategory = z.record(z.string(), z.number().min(0));
|
|
|
|
export const SurfaceBreakdownSchema = z.object({
|
|
surface: MetresByCategory,
|
|
highway: MetresByCategory,
|
|
});
|
|
|
|
export type SurfaceBreakdown = z.infer<typeof SurfaceBreakdownSchema>;
|