import { describe, expect, it } from 'vitest'; import { createConflict, createEmptyProject, createPhase, createSignalGroup, createStandardIntersectionProject, } from '@/domain/model/factory'; import { buildSignalPlan } from '@/domain/plan/signalPlan'; import { ueberfahrzeitAnsatzOhneWirkung, verfahrenDerKfzRaeumstroeme, } from '@/domain/rilsa/ansaetze'; import { validateProject } from '@/domain/validation/engine'; import type { Project } from '@/domain/model/project'; /** * Fassung 5.43.0: Der Kfz-Ueberfahrzeit-Ansatz wird nur dort genannt, wo er * gerechnet hat - und dieselbe Antwort tragen alle Ausgaben. * * DER BEFUND. Der Absatz unter der Zwischenzeitenmatrix war eingegrenzt, drei * Stellen derselben Aussage blieben unbedingt: die Deckblattzeile * "Ueberfahrzeit Kfz (Rechenansatz)", die Tafel "Abweichungen von den * Regelwerten" und die Meldung 'zwischenzeiten.ueberfahrzeit-ansatz-fest' des * Pruefberichts. An einer einstreifigen Verkehrsfuehrung rechnet * `buildSignalPlan` jeden raeumenden Kfz-Strom nach RiLSA 2015, Abschnitt 5.2.2 * mit einer festen Ueberfahrzeit von 4 s; der Bericht meldete gleichwohl, die * Ueberfahrzeit werde "mit 3 s geradeaus und 2 s abbiegend angesetzt", und * verlangte dafuer eine Begruendung - waehrend die Planunterlage desselben Plans * seit dieser Fassung das Verfahren nennt, das gerechnet hat. * * WAS HIER GEPRUEFT WIRD: die Antwort des Fachkerns * (`verfahrenDerKfzRaeumstroeme`, `ueberfahrzeitAnsatzOhneWirkung`) und der * Pruefbericht, der sie liest. Dass die Planunterlage sie an drei Stellen * dieselbe liest, prueft tests/export/matrixVerfahrenssatz.test.ts. * * DIE DRITTE LAGE HAT KEINE ANLAGENART: Ist jede Zwischenzeit eines raeumenden * Kraftfahrzeugs von Hand vorgegeben, hat der Ansatz auch am Knotenpunkt nicht * gerechnet (`calculation: null`). Deshalb liest die Antwort den PLAN und nicht * `project.anlagenart`. */ const DATUM = new Date('2026-01-01T12:00:00Z'); /** Zwei Richtungen einer Baustellenampel, 200 m Engstelle, 50 km/h. */ function baustelle(): Project { const basis = createEmptyProject('Baustelle', DATUM, 'einstreifig'); const a = createSignalGroup({ name: 'K1', mode: 'kfz', vZul: 50, index: 0 }); const b = createSignalGroup({ name: 'K2', mode: 'kfz', vZul: 50, index: 1 }); const p1 = createPhase('Seite A', [a.id]); const p2 = createPhase('Seite B', [b.id]); const beziehung = (von: string, nach: string) => ({ ...createConflict(von, nach), clearingDistance: 200, enteringDistance: 0, }); return { ...basis, signalGroups: [a, b], conflicts: [beziehung(a.id, b.id), beziehung(b.id, a.id)], phases: [p1, p2], program: { ...basis.program, phaseOrder: [p1.id, p2.id] }, }; } /** Eine Fussgaengerschutzanlage: geraeumt wird nur von der Furt. */ function nurFussRaeumt(): Project { const basis = createEmptyProject('Furt', DATUM, 'fussgaengerschutzanlage'); const f = createSignalGroup({ name: 'F1', mode: 'fuss', vZul: 50, index: 0 }); const k = createSignalGroup({ name: 'K1', mode: 'kfz', vZul: 50, index: 1 }); const p1 = createPhase('Furt', [f.id]); const p2 = createPhase('Fahrbahn', [k.id]); return { ...basis, signalGroups: [f, k], conflicts: [{ ...createConflict(f.id, k.id), clearingDistance: 10, enteringDistance: 0 }], phases: [p1, p2], program: { ...basis.program, phaseOrder: [p1.id, p2.id] }, }; } /** Jede Beziehung mit raeumendem Kraftfahrzeug von Hand vorgegeben. */ function mitHandvorgaben(project: Project, wert = 8): Project { return { ...project, conflicts: project.conflicts.map((c) => project.signalGroups.find((g) => g.id === c.fromId)?.mode === 'kfz' ? { ...c, manualIntergreen: wert } : c, ), }; } function mitFestemAnsatz(project: Project): Project { return { ...project, settings: { ...project.settings, ueberfahrzeitAnsatz: 'fest' } }; } describe('Der Fachkern sagt, welches Verfahren die Kfz-Zwischenzeiten gerechnet hat', () => { it('nennt am Knotenpunkt die Knotenpunktformel und keine Wirkungslosigkeit', () => { const projekt = createStandardIntersectionProject('Knotenpunkt', DATUM); const plan = buildSignalPlan(projekt); expect([...verfahrenDerKfzRaeumstroeme(projekt, plan)]).toEqual(['knotenpunkt']); expect(ueberfahrzeitAnsatzOhneWirkung(projekt, plan)).toBeNull(); }); it('nennt an der einstreifigen Verkehrsfuehrung das Verfahren des Abschnitts 5.2.2', () => { const projekt = baustelle(); const plan = buildSignalPlan(projekt); expect([...verfahrenDerKfzRaeumstroeme(projekt, plan)]).toEqual(['engstelle']); const ohneWirkung = ueberfahrzeitAnsatzOhneWirkung(projekt, plan); expect(ohneWirkung, 'der Ansatz rechnet dort nicht').not.toBeNull(); expect(ohneWirkung!.vermerk).toContain('ohne Wirkung an dieser Anlage'); expect(ohneWirkung!.begruendung).toContain('RiLSA 2015, Abschnitt 5.2.2'); expect(ohneWirkung!.begruendung, 'die feste Ueberfahrzeit des Abschnitts').toContain( 'tü = 4 s', ); expect(ohneWirkung!.zutun, 'zu begruenden ist nichts').toContain('nichts zu begründen'); }); it('erkennt die Handvorgabe auch am Knotenpunkt, wo keine Anlagenart sie verraet', () => { // Die Lage, die kein Blick auf project.anlagenart findet: Die Formel des // Abschnitts 2 gilt, aber sie ist an keiner Beziehung gelaufen. const projekt = mitHandvorgaben(createStandardIntersectionProject('Vorgaben', DATUM)); const plan = buildSignalPlan(projekt); expect([...verfahrenDerKfzRaeumstroeme(projekt, plan)]).toEqual(['vorgegeben']); const ohneWirkung = ueberfahrzeitAnsatzOhneWirkung(projekt, plan); expect(ohneWirkung, 'auch hier hat der Ansatz nicht gerechnet').not.toBeNull(); expect(ohneWirkung!.vermerk).toContain('von Hand vorgegeben'); expect(ohneWirkung!.begruendung, 'die Gegenprobe ist die Ausnahme').toContain('Gegenprobe'); }); it('nennt die Wirkungslosigkeit auch, wo gar kein Kraftfahrzeug raeumt', () => { const projekt = nurFussRaeumt(); const plan = buildSignalPlan(projekt); expect([...verfahrenDerKfzRaeumstroeme(projekt, plan)], 'keine Kfz-Raeumbeziehung').toEqual([]); expect(ueberfahrzeitAnsatzOhneWirkung(projekt, plan)?.vermerk).toContain( 'kein räumendes Kraftfahrzeug erfasst', ); }); it('bleibt bei "es rechnet", wo eine einzige Beziehung nach Abschnitt 2 gerechnet ist', () => { // Die Richtung der Antwort: Eine zu viel verlangte Begruendung ist harmlos, // die Behauptung "ohne Wirkung" ueber einen rechnenden Ansatz nicht. const alle = createStandardIntersectionProject('Gemischt', DATUM); const eineFrei = { ...mitHandvorgaben(alle), conflicts: mitHandvorgaben(alle).conflicts.map((c, i) => i === 0 ? { ...c, manualIntergreen: null } : c, ), }; const plan = buildSignalPlan(eineFrei); expect(verfahrenDerKfzRaeumstroeme(eineFrei, plan).has('knotenpunkt')).toBe(true); expect(ueberfahrzeitAnsatzOhneWirkung(eineFrei, plan)).toBeNull(); }); }); describe('Der Pruefbericht verlangt keine Begruendung fuer eine Wahl ohne Wirkung', () => { function befund(project: Project) { const plan = buildSignalPlan(project); return validateProject(project, plan, DATUM).findings.find( (f) => f.rule === 'zwischenzeiten.ueberfahrzeit-ansatz-fest', ); } it('meldet am Knotenpunkt weiter die Warnung mit der Fehlerrichtung', () => { const treffer = befund(mitFestemAnsatz(createStandardIntersectionProject('Knoten', DATUM))); expect(treffer?.severity).toBe('warnung'); expect(treffer?.message).toContain('VERKÜRZT'); expect(treffer?.suggestion).toContain('Begründen Sie die Wahl'); }); it('meldet an der Engstelle einen Hinweis und nennt das Verfahren, das gerechnet hat', () => { const treffer = befund(mitFestemAnsatz(baustelle())); expect(treffer, 'die Meldung bleibt - die Einstellung steht ja da').toBeDefined(); expect(treffer?.severity, 'zu begruenden ist nichts').toBe('hinweis'); expect(treffer?.message).toContain('RiLSA 2015, Abschnitt 5.2.2'); expect(treffer?.message, 'die Rechnung, die dort nicht stattfand').not.toContain( 'angesetzt, unabhängig von der zulässigen Höchstgeschwindigkeit', ); expect(treffer?.message, 'und keine Fehlerrichtung ohne Rechnung').not.toContain('VERKÜRZT'); expect(treffer?.suggestion).toContain('nichts zu begründen'); }); it('meldet den Hinweis auch, wo jede Kfz-Zwischenzeit von Hand vorgegeben ist', () => { const treffer = befund( mitFestemAnsatz(mitHandvorgaben(createStandardIntersectionProject('Vorgaben', DATUM))), ); expect(treffer?.severity).toBe('hinweis'); expect(treffer?.message).toContain('von Hand'); }); it('schweigt beim Regelansatz weiter ganz', () => { const projekt = baustelle(); expect(projekt.settings.ueberfahrzeitAnsatz, 'Vorbedingung: die Vorgabe gilt').toBe('gelbzeit'); expect(befund(projekt)).toBeUndefined(); }); });