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>
34 lines
1.2 KiB
TypeScript
34 lines
1.2 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { computeSurfaceBreakdown } from "./surface-breakdown.ts";
|
|
|
|
// ~0.001° lon at the equator ≈ 111 m; exact value doesn't matter, only ratios.
|
|
const coords: [number, number][] = [
|
|
[0, 0],
|
|
[0.001, 0],
|
|
[0.002, 0],
|
|
[0.003, 0],
|
|
];
|
|
|
|
describe("computeSurfaceBreakdown", () => {
|
|
it("weights segments by distance into surface and waytype buckets", () => {
|
|
const { surface, highway } = computeSurfaceBreakdown(
|
|
coords,
|
|
["asphalt", "asphalt", "gravel"],
|
|
["residential", "residential", "track"],
|
|
);
|
|
// segments 0,1 asphalt; segment 2 gravel — equal lengths → 2:1
|
|
expect(surface.asphalt! / surface.gravel!).toBeCloseTo(2, 5);
|
|
expect(highway.residential! / highway.track!).toBeCloseTo(2, 5);
|
|
expect(Object.keys(surface)).toEqual(["asphalt", "gravel"]);
|
|
});
|
|
|
|
it("buckets missing/empty values as unknown", () => {
|
|
const { surface } = computeSurfaceBreakdown(coords, ["asphalt", "", "asphalt"], ["", "", ""]);
|
|
expect(surface.asphalt).toBeGreaterThan(0);
|
|
expect(surface.unknown).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("returns empty buckets for a degenerate track", () => {
|
|
expect(computeSurfaceBreakdown([[0, 0]], [], [])).toEqual({ surface: {}, highway: {} });
|
|
});
|
|
});
|