Skip to content

Commit ecfeb70

Browse files
committed
feat(roads): extract more typed fields across all feed parsers
Audited the live feeds for the four non-DATEX formats (open511, wzdx, autobahn, digitraffic). All set sourceRaw to the full record, so nothing was lost — but several fields with a typed home stayed buried there. Promote them: - digitraffic: regions (municipality/province — 100% of live records), description from roadWorkPhases comments, externalRefs.tmc from the Alert-C location, per-restriction validity windows, and a features[] fallback so phase-less TRAFFIC_ANNOUNCEMENT records still carry restrictions/speed. - autobahn: validTo and width parsed from the German description prose (the feed exposes no structured end time, so events never expired before — 54% of live warning/closure records now get one), plus delaySeconds from the congestion delayTimeValue. - wzdx: relatedEvents keeps each related_road_events relationship type (relatedIds kept only the bare id); confidence from the date- verification flags. - open511: externalRefs.external from the GeoNames area id; roads milepost from the +linear_reference_km extension (guarding -1/0). Adds Restriction.validFrom/validTo and RoadEvent.relatedEvents to the model. Verified against the live feeds; +47 parser tests (475 total).
1 parent f6015f9 commit ecfeb70

9 files changed

Lines changed: 1192 additions & 10 deletions

File tree

packages/roads/src/__tests__/autobahn.test.ts

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,3 +315,186 @@ describe("parseAutobahn — extended fields", () => {
315315
expect(ev!.sourceRaw?.["identifier"]).toBe("w9");
316316
});
317317
});
318+
319+
describe("parseAutobahn — delaySeconds from congestion delayTimeValue", () => {
320+
it("converts the minute-valued delayTimeValue to delaySeconds on the congestion path", () => {
321+
const events = parseAutobahn(readFileSync(FIXTURE_PATH, "utf8"), AUTOBAHN_SOURCE, "warning");
322+
const flowItem = events.find((ev) =>
323+
ev.id.includes("INRIX--vi-avl.2026-06-22_18-23-00-000_001.de0")
324+
);
325+
expect(flowItem).toBeDefined();
326+
expect(flowItem!.type).toBe("congestion");
327+
expect(flowItem!.delaySeconds).toBe(18 * 60);
328+
});
329+
330+
it("leaves delaySeconds undefined when delayTimeValue is absent", () => {
331+
const feed = {
332+
warning: [
333+
{
334+
identifier: "no-delay",
335+
title: "A1 | Test",
336+
averageSpeed: "20",
337+
coordinate: { lat: 50.0, long: 7.0 },
338+
description: ["Stau"],
339+
},
340+
],
341+
};
342+
const [ev] = parseAutobahn(feed, AUTOBAHN_SOURCE, "warning");
343+
expect(ev!.type).toBe("congestion");
344+
expect(ev!.delaySeconds).toBeUndefined();
345+
});
346+
347+
it("ignores non-finite or non-positive delayTimeValue", () => {
348+
const feed = {
349+
warning: [
350+
{
351+
identifier: "bad-delay",
352+
title: "A1 | Test",
353+
delayTimeValue: "n/a",
354+
coordinate: { lat: 50.0, long: 7.0 },
355+
description: ["Stau"],
356+
},
357+
{
358+
identifier: "zero-delay",
359+
title: "A2 | Test",
360+
delayTimeValue: "0",
361+
coordinate: { lat: 51.0, long: 8.0 },
362+
description: ["Stau"],
363+
},
364+
],
365+
};
366+
const events = parseAutobahn(feed, AUTOBAHN_SOURCE, "warning");
367+
const bad = events.find((ev) => ev.id.includes("bad-delay"));
368+
const zero = events.find((ev) => ev.id.includes("zero-delay"));
369+
expect(bad!.delaySeconds).toBeUndefined();
370+
expect(zero!.delaySeconds).toBeUndefined();
371+
});
372+
});
373+
374+
describe("parseAutobahn — validTo from prose end time", () => {
375+
it("parses 'Ende: DD.MM.YY um HH:MM Uhr' from the description into validTo (ISO)", () => {
376+
const feed = {
377+
warning: [
378+
{
379+
identifier: "ende-test",
380+
title: "A4 | Köln - Aachen",
381+
coordinate: { lat: 50.8, long: 6.5 },
382+
description: [
383+
"Baustelle",
384+
"Beginn: 22.06.26 um 08:00 Uhr",
385+
"Ende: 06.07.26 um 05:00 Uhr",
386+
],
387+
},
388+
],
389+
};
390+
const [ev] = parseAutobahn(feed, AUTOBAHN_SOURCE, "warning");
391+
expect(ev!.validTo).toBe(new Date("2026-07-06T05:00:00").toISOString());
392+
});
393+
394+
it("leaves validTo null when no end-time prose is present", () => {
395+
const feed = {
396+
warning: [
397+
{
398+
identifier: "no-ende",
399+
title: "A4 | Köln - Aachen",
400+
coordinate: { lat: 50.8, long: 6.5 },
401+
description: ["Baustelle", "Beginn: 22.06.26 um 08:00 Uhr"],
402+
},
403+
],
404+
};
405+
const [ev] = parseAutobahn(feed, AUTOBAHN_SOURCE, "warning");
406+
expect(ev!.validTo).toBeNull();
407+
});
408+
});
409+
410+
describe("parseAutobahn — restrictions from prose", () => {
411+
it("parses 'Durchfahrtsbreite: N m' into a width restriction (dot decimal)", () => {
412+
const feed = {
413+
warning: [
414+
{
415+
identifier: "width-dot",
416+
title: "A4 | Köln - Aachen",
417+
coordinate: { lat: 50.8, long: 6.5 },
418+
description: ["Baustelle", "Durchfahrtsbreite: 3.25 m"],
419+
},
420+
],
421+
};
422+
const [ev] = parseAutobahn(feed, AUTOBAHN_SOURCE, "warning");
423+
expect(ev!.restrictions).toEqual([{ type: "width", value: 3.25, unit: "m" }]);
424+
});
425+
426+
it("parses width with a comma decimal", () => {
427+
const feed = {
428+
warning: [
429+
{
430+
identifier: "width-comma",
431+
title: "A4 | Köln - Aachen",
432+
coordinate: { lat: 50.8, long: 6.5 },
433+
description: ["Durchfahrtsbreite: 2,75 m"],
434+
},
435+
],
436+
};
437+
const [ev] = parseAutobahn(feed, AUTOBAHN_SOURCE, "warning");
438+
expect(ev!.restrictions).toEqual([{ type: "width", value: 2.75, unit: "m" }]);
439+
});
440+
441+
it("parses 'Durchfahrtshöhe' (and the 'Durchfahrtshoehe' spelling) into a height restriction", () => {
442+
const umlaut = {
443+
warning: [
444+
{
445+
identifier: "height-umlaut",
446+
title: "A4 | Köln - Aachen",
447+
coordinate: { lat: 50.8, long: 6.5 },
448+
description: ["Durchfahrtshöhe: 3,8 m"],
449+
},
450+
],
451+
};
452+
const ascii = {
453+
warning: [
454+
{
455+
identifier: "height-ascii",
456+
title: "A4 | Köln - Aachen",
457+
coordinate: { lat: 50.8, long: 6.5 },
458+
description: ["Durchfahrtshoehe: 4.0 m"],
459+
},
460+
],
461+
};
462+
const [umlautEv] = parseAutobahn(umlaut, AUTOBAHN_SOURCE, "warning");
463+
const [asciiEv] = parseAutobahn(ascii, AUTOBAHN_SOURCE, "warning");
464+
expect(umlautEv!.restrictions).toEqual([{ type: "height", value: 3.8, unit: "m" }]);
465+
expect(asciiEv!.restrictions).toEqual([{ type: "height", value: 4.0, unit: "m" }]);
466+
});
467+
468+
it("emits both width and height restrictions when both are present", () => {
469+
const feed = {
470+
warning: [
471+
{
472+
identifier: "width-and-height",
473+
title: "A4 | Köln - Aachen",
474+
coordinate: { lat: 50.8, long: 6.5 },
475+
description: ["Durchfahrtsbreite: 3.0 m", "Durchfahrtshöhe: 3,5 m"],
476+
},
477+
],
478+
};
479+
const [ev] = parseAutobahn(feed, AUTOBAHN_SOURCE, "warning");
480+
expect(ev!.restrictions).toEqual([
481+
{ type: "width", value: 3.0, unit: "m" },
482+
{ type: "height", value: 3.5, unit: "m" },
483+
]);
484+
});
485+
486+
it("leaves restrictions undefined when no dimension prose is present", () => {
487+
const feed = {
488+
warning: [
489+
{
490+
identifier: "no-restriction",
491+
title: "A4 | Köln - Aachen",
492+
coordinate: { lat: 50.8, long: 6.5 },
493+
description: ["Baustelle"],
494+
},
495+
],
496+
};
497+
const [ev] = parseAutobahn(feed, AUTOBAHN_SOURCE, "warning");
498+
expect(ev!.restrictions).toBeUndefined();
499+
});
500+
});

0 commit comments

Comments
 (0)