import { describe, expect, it } from 'vitest'; import { buildSignalPlan } from '@/domain/plan/signalPlan'; import { wartezeitBefunde } from '@/domain/validation/rules'; import { createConflict, createEmptyProject, createPhase, createSignalGroup, } from '@/domain/model/factory'; import type { Project } from '@/domain/model/project'; /** * Befund 29, behoben in Fassung 5.10.0. * * Folge und naechster Schritt der Wartezeitmeldung wurden allein aus * `plan.anlagenart` gewaehlt; die Verkehrsart der beanstandeten Signalgruppe * ging nicht ein. Der gedruckte Pruefbericht behauptete dadurch ueber einen * Kraftfahrzeugstrom, hier gehe jemand "als Fußgänger bei Rot" und die Furt * verliere ihre Schutzwirkung - eine falsche Sachaussage in einer Unterlage, * die einer Behoerde vorgelegt wird. Spiegelbildlich bekam eine Furt an einer * einstreifigen Verkehrsfuehrung den Satz ueber das Einfahren in die Engstelle. * * Die Schwelle selbst bleibt die der Anlagenart: Fuer den Fahrverkehr wirkt sie * eng, und das ist die sichere Seite. Gesagt wird das jetzt aber auch. */ const DATUM = new Date('2026-01-01T00:00:00Z'); /** Fussgaengerschutzanlage mit Fahrverkehr K1 und Furt F1, feste Umlaufzeit. */ function schutzanlage(umlaufzeit: number, gruenFurt: number): Project { const basis = createEmptyProject('Furt', DATUM, 'fussgaengerschutzanlage'); const kfz = createSignalGroup({ name: 'K1', mode: 'kfz', index: 0 }); const fuss = createSignalGroup({ name: 'F1', mode: 'fuss', index: 1 }); const phase1 = createPhase('Fahrverkehr', [kfz.id]); const phase2 = createPhase('Furt', [fuss.id]); return { ...basis, signalGroups: [kfz, fuss], conflicts: [ { ...createConflict(kfz.id, fuss.id), clearingDistance: 25, enteringDistance: 2 }, { ...createConflict(fuss.id, kfz.id), clearingDistance: 12, enteringDistance: 5 }, ], phases: [phase1, { ...phase2, manualGreen: gruenFurt }], program: { ...basis.program, phaseOrder: [phase1.id, phase2.id], method: 'manuell', manualCycleTime: umlaufzeit, }, }; } /** Einstreifige Verkehrsfuehrung ohne Furt - zwei Richtungen, ein Fahrstreifen. */ function engstelle(laenge: number): Project { const basis = createEmptyProject('Arbeitsstelle', DATUM, 'einstreifig'); const nord = createSignalGroup({ name: 'A1', mode: 'kfz', index: 0 }); const sued = createSignalGroup({ name: 'A2', mode: 'kfz', index: 1 }); const p1 = createPhase('Richtung Nord', [nord.id]); const p2 = createPhase('Richtung Sued', [sued.id]); return { ...basis, signalGroups: [nord, sued], conflicts: [ { ...createConflict(nord.id, sued.id), clearingDistance: laenge, enteringDistance: 0 }, { ...createConflict(sued.id, nord.id), clearingDistance: laenge, enteringDistance: 0 }, ], phases: [p1, p2], program: { ...basis.program, phaseOrder: [p1.id, p2.id] }, }; } /** Einstreifige Verkehrsfuehrung mit einer Furt ueber die Engstelle. */ function engstelleMitFurt(laenge: number): Project { const basis = createEmptyProject('Arbeitsstelle', DATUM, 'einstreifig'); const nord = createSignalGroup({ name: 'A1', mode: 'kfz', index: 0 }); const sued = createSignalGroup({ name: 'A2', mode: 'kfz', index: 1 }); const furt = createSignalGroup({ name: 'F1', mode: 'fuss', index: 2 }); const p1 = createPhase('Richtung Nord', [nord.id]); const p2 = createPhase('Richtung Sued', [sued.id]); const p3 = createPhase('Furt', [furt.id]); return { ...basis, signalGroups: [nord, sued, furt], conflicts: [ { ...createConflict(nord.id, sued.id), clearingDistance: laenge, enteringDistance: 0 }, { ...createConflict(sued.id, nord.id), clearingDistance: laenge, enteringDistance: 0 }, { ...createConflict(nord.id, furt.id), clearingDistance: 20, enteringDistance: 2 }, { ...createConflict(furt.id, nord.id), clearingDistance: 8, enteringDistance: 5 }, { ...createConflict(sued.id, furt.id), clearingDistance: 20, enteringDistance: 2 }, { ...createConflict(furt.id, sued.id), clearingDistance: 8, enteringDistance: 5 }, ], phases: [p1, p2, { ...p3, manualGreen: 10 }], program: { ...basis.program, phaseOrder: [p1.id, p2.id, p3.id] }, }; } /** * Einstreifige Verkehrsfuehrung mit einer Radsignalgruppe, die DURCH die * Engstelle faehrt. * * R1 traegt die Vorbelegung `movement: 'geradeaus'` (factory.ts, * defaultMovement) - nach dem Modell faehrt die Gruppe also dieselbe Strecke * wie die beiden Kfz-Richtungen und quert nichts. Eine eigene Phase bekommt * sie, weil sie mit 4 m/s raeumt und dafuer ein Vielfaches der Zeit braucht. */ function engstelleMitRad(laenge: number): Project { const basis = createEmptyProject('Arbeitsstelle', DATUM, 'einstreifig'); const nord = createSignalGroup({ name: 'A1', mode: 'kfz', index: 0 }); const sued = createSignalGroup({ name: 'A2', mode: 'kfz', index: 1 }); const rad = createSignalGroup({ name: 'R1', mode: 'rad', index: 2 }); const p1 = createPhase('Richtung Nord', [nord.id]); const p2 = createPhase('Richtung Sued', [sued.id]); const p3 = createPhase('Radverkehr', [rad.id]); const durch = (vonId: string, nachId: string) => ({ ...createConflict(vonId, nachId), clearingDistance: laenge, enteringDistance: 0, }); return { ...basis, signalGroups: [nord, sued, rad], conflicts: [ durch(nord.id, sued.id), durch(sued.id, nord.id), durch(nord.id, rad.id), durch(rad.id, nord.id), durch(sued.id, rad.id), durch(rad.id, sued.id), ], phases: [p1, p2, p3], program: { ...basis.program, phaseOrder: [p1.id, p2.id, p3.id] }, }; } describe('Wartezeitmeldung: Folge und naechster Schritt nach der Verkehrsart', () => { it('sagt ueber den Fahrverkehr an der Schutzanlage nichts von Fussgaengern', () => { // 100 s Umlauf, 40 s Furtfreigabe: K1 wartet 55 s, F1 60 s - beide ueber // der Schwelle von 45 s. Der Befund zu K1 ist der, der bisher den falschen // Satz trug. const befunde = wartezeitBefunde(buildSignalPlan(schutzanlage(100, 40))); const kfz = befunde.find((f) => f.title.includes('K1')); expect( kfz, 'Der Fahrverkehr muss die Schwelle reissen, sonst prueft der Fall nichts', ).toBeDefined(); expect(kfz!.message).not.toContain('als Fußgänger'); expect(kfz!.message).not.toContain('Schutzwirkung'); expect(kfz!.message).toContain('Rotlichtverstöße'); // Der naechste Schritt darf nicht mit der Wartezeit der Querenden begruendet // werden - beanstandet ist die des Fahrverkehrs. expect(kfz!.suggestion).not.toContain('nicht die Leistungsfähigkeit der Fahrbahn das Maß'); }); it('behaelt fuer die Furt an derselben Anlage den Fussgaengertext', () => { const befunde = wartezeitBefunde(buildSignalPlan(schutzanlage(100, 40))); const furt = befunde.find((f) => f.title.includes('F1')); expect(furt).toBeDefined(); expect(furt!.message).toContain('geht bei Rot'); expect(furt!.message).toContain('Schutzwirkung'); expect(furt!.suggestion).toContain('Wartezeit der Querenden'); }); it('nennt fuer den Fahrverkehr die Herkunft der engen Schwelle', () => { // Die Schwelle bleibt die der Anlagenart (45/90 s) - sie ist an der // Wartezeit der Querenden geeicht und damit fuer den Fahrverkehr eng. Das // ist die sichere Seite, gehoert aber in die Unterlage. const kfz = wartezeitBefunde(buildSignalPlan(schutzanlage(100, 40))).find((f) => f.title.includes('K1'), )!; expect(kfz.message).toContain('Querenden'); }); it('sagt ueber eine Furt an der Engstelle nichts vom Einfahren in die Engstelle', () => { /* * 1000 m Engstelle, 10 s Furtfreigabe und 40 s je Fahrtrichtung: F1 wartet * ueber 180 s und reisst die Schwelle der Anlagenart. Ein zu Fuss Gehender * faehrt nicht in die Engstelle ein, und ihm kommt dort auch kein * Gegenverkehr entgegen. * * DIE 20 S JE RICHTUNG SIND MIT 5.27.0 HINZUGEKOMMEN: Seit die Engstelle * nach RiLSA 2015, Abschnitt 5.2.2 gerechnet wird, ist die Zwischenzeit * bei 1000 m um 10 s kuerzer (94 statt 104 s), und mit den kuerzest * zulaessigen Freigabezeiten blieb die Wartezeit knapp unter der Schwelle. * Der Fall pruefte dann nichts mehr - deshalb eine Freigabezeit, die man * an einer Baustellenampel tatsaechlich anordnen wuerde. */ const roh = engstelleMitFurt(1000); const mitFreigabe: Project = { ...roh, phases: roh.phases.map((phase, index) => (index < 2 ? { ...phase, manualGreen: 40 } : phase)), }; const befunde = wartezeitBefunde(buildSignalPlan(mitFreigabe)); const furt = befunde.find((f) => f.title.includes('F1')); expect(furt, 'Die Furt muss die Schwelle reissen, sonst prueft der Fall nichts').toBeDefined(); expect(furt!.message).not.toContain('fährt bei Rot in die Engstelle ein'); expect(furt!.message).not.toContain('Gegenverkehr'); expect(furt!.message).toContain('quert bei Rot'); }); it('sagt ueber eine durchfahrende Radgruppe nichts vom Queren', () => { // Nachtrag zu Befund 29: Die Verzweigung kennt nur die Verkehrsart, nicht // die Fahrbeziehung. R1 faehrt nach dem Modell geradeaus durch die // Engstelle - "quert bei Rot" waere eine Behauptung ueber eine Bewegung, // die das Projekt nicht kennt, und "trifft auf einen Fahrverkehr, der die // Engstelle in einem Zug durchfaehrt" stellt die Gruppe neben einen // Verkehr, in dem sie selbst faehrt. const befunde = wartezeitBefunde(buildSignalPlan(engstelleMitRad(400))); const rad = befunde.find((f) => f.title.includes('R1')); expect( rad, 'Die Radgruppe muss die Schwelle reissen, sonst prueft der Fall nichts', ).toBeDefined(); expect(rad!.message).not.toContain('quert bei Rot'); expect(rad!.message).toContain('Engstelle'); expect(rad!.message).toContain('Gegenverkehr'); }); it('behaelt fuer den Fahrverkehr an der Engstelle den Engstellensatz', () => { const kfz = wartezeitBefunde(buildSignalPlan(engstelle(800))).find((f) => f.title.includes('A1'), ); expect(kfz).toBeDefined(); expect(kfz!.message).toContain('Engstelle'); expect(kfz!.message).toContain('Gegenverkehr'); }); });