Covers routes/activities that enter the journal without BRouter waytags (imports, uploads, pre-existing rows): - overpass-ways.server.ts: server-side Overpass client (way[highway] + geom in a bbox, configurable OVERPASS_URLS, timeout + oversized-bbox guard). - surface-match.server.ts: pure nearest-way map-matcher → per-segment surface/highway (unmatched → unknown). Unit-tested. - surface-backfill pg-boss job: load geom → skip if breakdown exists → Overpass → match → computeSurfaceBreakdown → store → emit `surface_breakdown` SSE to the owner. Idempotent, retry-safe, best-effort; registered in server.ts. - Enqueued from createActivity (imports/uploads) + owner-on-open in the route & activity detail loaders (non-Planner routes, old rows), deduped via singletonKey. - useSurfaceBackfillUpdates: detail pages subscribe to /api/events and revalidate() when their row's backfill lands (live bars, no reload). - Privacy manifest updated (DE + EN) for the Overpass bbox lookup. Tests: matchSurfaces unit; surface-backfill job (mocked Overpass/db/events: store + emit, skip-if-present, skip-if-no-ways). Verified the real Overpass→match→breakdown pipeline against a live Berlin bbox (509 ways → plausible asphalt/paving_stones/footway mix). typecheck + lint + unit (journal 333) green. Note: the worker runs via server.ts (prod/staging), not `react-router dev`, so the live job + SSE exercise on a deployed instance. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
50 lines
1.6 KiB
TypeScript
50 lines
1.6 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { matchSurfaces } from "./surface-match.server.ts";
|
|
import type { OverpassWay } from "./overpass-ways.server.ts";
|
|
|
|
// Two parallel ways: one along lat 0 (asphalt road), one along lat 1 (gravel track).
|
|
const ways: OverpassWay[] = [
|
|
{
|
|
highway: "residential",
|
|
surface: "asphalt",
|
|
geometry: [
|
|
{ lat: 0, lon: 0 },
|
|
{ lat: 0, lon: 10 },
|
|
],
|
|
},
|
|
{
|
|
highway: "track",
|
|
surface: "gravel",
|
|
geometry: [
|
|
{ lat: 1, lon: 0 },
|
|
{ lat: 1, lon: 10 },
|
|
],
|
|
},
|
|
];
|
|
|
|
describe("matchSurfaces", () => {
|
|
it("assigns a segment near the lat-0 way its asphalt/residential tags", () => {
|
|
const { surfaces, highways } = matchSurfaces([[1, 0.1], [2, 0.1]], ways);
|
|
expect(surfaces).toEqual(["asphalt"]);
|
|
expect(highways).toEqual(["residential"]);
|
|
});
|
|
|
|
it("assigns a segment near the lat-1 way its gravel/track tags", () => {
|
|
const { surfaces, highways } = matchSurfaces([[1, 0.9], [2, 0.9]], ways);
|
|
expect(surfaces).toEqual(["gravel"]);
|
|
expect(highways).toEqual(["track"]);
|
|
});
|
|
|
|
it("returns unknown when there are no candidate ways", () => {
|
|
const { surfaces, highways } = matchSurfaces([[0, 0], [1, 0]], []);
|
|
expect(surfaces).toEqual(["unknown"]);
|
|
expect(highways).toEqual(["unknown"]);
|
|
});
|
|
|
|
it("buckets a missing surface tag as unknown but keeps the highway", () => {
|
|
const untagged: OverpassWay[] = [{ highway: "path", geometry: [{ lat: 0, lon: 0 }, { lat: 0, lon: 10 }] }];
|
|
const { surfaces, highways } = matchSurfaces([[1, 0], [2, 0]], untagged);
|
|
expect(surfaces).toEqual(["unknown"]);
|
|
expect(highways).toEqual(["path"]);
|
|
});
|
|
});
|