trails/packages/map-core/src/surface-breakdown.test.ts
Ullrich Schäfer 3a1c34317d
route-surface-breakdown (Phase 1): Planner-path surface/waytype bars
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>
2026-06-14 18:46:49 +02:00

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: {} });
});
});