import { describe, expect, it } from 'vitest'; import { engstellenFreigabezeit, engstellenRaeumgeschwindigkeit, engstellenUmlaufzeit, engstellenZwischenzeit, } from '@/domain/rilsa/engstellensignalisierung'; import { computeEngstellenIntergreen, computeIntergreen } from '@/domain/rilsa/intergreen'; import { RILSA_ENGSTELLE } from '@/domain/rilsa/constants'; import { buildSignalPlan } from '@/domain/plan/signalPlan'; import { validateProject } from '@/domain/validation'; import { createConflict, createEmptyProject, createPhase, createSignalGroup, } from '@/domain/model/factory'; import type { Project } from '@/domain/model/project'; /** * Das eigene Rechenverfahren der Engstellensignalisierung * (RiLSA 2015, Abschnitt 5.2), eingebaut mit der Fassung 5.27.0. * * WAS DIESE WACHE HAELT, ist nicht nur ein Satz Zahlen, sondern die * Entscheidung dahinter: An einer einstreifigen Verkehrsfuehrung gilt ein * ANDERES Verfahren als am Knotenpunkt, und zwar fuer den * Kraftfahrzeugverkehr. Bis 5.26.0 rechnete das Programm dort die * Knotenpunktformel. Jeder Fall nennt deshalb beide Zahlen - die von heute und * die von damals -, sonst laesst sich nicht erkennen, was sich geaendert hat. */ const T = new Date('2026-01-01T00:00:00Z'); const OHNE_EINFAHRWEG = { mode: 'kfz', movement: 'geradeaus', enteringDistance: 0 } as const; const raeumt = (clearingDistance: number, vZul: number) => ({ mode: 'kfz', movement: 'geradeaus', clearingDistance, vehicleClass: 'pkw', vZul }) as const; describe('Raeumgeschwindigkeit der Engstelle (Abschnitt 5.2.2)', () => { it.each([ [60, 50], [70, 50], [50, 40], [40, 30], ])('setzt bei zulaessig %s km/h eine Raeumgeschwindigkeit von %s km/h an', (vZul, erwartet) => { const ergebnis = engstellenRaeumgeschwindigkeit(vZul); expect(ergebnis.vrKmh).toBe(erwartet); expect(ergebnis.grund).toBe('vzul'); }); it('nimmt unterhalb der Staffel die kleinste Stufe', () => { // Die Staffel endet bei 40 km/h; darunter nennt die RiLSA keinen Wert. expect(engstellenRaeumgeschwindigkeit(35)).toEqual({ vrKmh: 30, grund: 'unter-staffel' }); expect(engstellenRaeumgeschwindigkeit(30)).toEqual({ vrKmh: 30, grund: 'unter-staffel' }); }); it('raeumt nie schneller, als gefahren werden darf', () => { /* * Der Deckel aus 5.22.0, hier noch einmal: Bei Tempo 20 laege die * kleinste Stufe der Staffel (30 km/h) ueber der zulaessigen * Hoechstgeschwindigkeit. Ein Ansatz DIESES Programms - die RiLSA nennt * fuer diesen Fall nichts - und die sichere Richtung. */ expect(engstellenRaeumgeschwindigkeit(20)).toEqual({ vrKmh: 20, grund: 'vmax-deckel' }); }); it('setzt bei schlechter Fahrbahn 30 km/h an, unabhaengig von der Vzul', () => { expect(engstellenRaeumgeschwindigkeit(60, { schlechteFahrbahn: true })).toEqual({ vrKmh: 30, grund: 'fahrbahn', }); }); it('macht den Radverkehr mit 18 km/h massgebend, wo er es ist', () => { expect(engstellenRaeumgeschwindigkeit(60, { radverkehrMassgebend: true })).toEqual({ vrKmh: 18, grund: 'radverkehr', }); }); it('nimmt bei zwei Sonderfaellen den langsameren', () => { // Eine Engstelle wird nicht dadurch schneller, dass zwei Gruende fuer // Langsamkeit zusammentreffen. expect( engstellenRaeumgeschwindigkeit(60, { schlechteFahrbahn: true, radverkehrMassgebend: true }) .vrKmh, ).toBe(18); }); }); describe('Zwischenzeit der Engstelle (Abschnitt 5.2.2)', () => { it('rechnet tz = tue + sr/Vr * 3,6 mit fester Ueberfahrzeit von 4 s', () => { const ergebnis = engstellenZwischenzeit(400, 50); expect(ergebnis.ueberfahrzeit).toBe(RILSA_ENGSTELLE.ueberfahrzeit); expect(ergebnis.ueberfahrzeit).toBe(4); expect(ergebnis.raeumzeit).toBe(36); expect(ergebnis.roh).toBe(40); expect(ergebnis.zwischenzeit).toBe(40); }); it('nimmt den Raeumweg OHNE Fahrzeuglaenge', () => { /* * Der Unterschied, der am leichtesten uebersehen wird: Am Knotenpunkt ist * sr = s0 + lFZ, an der Engstelle ist sr der Abstand der Haltlinien. Die * Gegenprobe rechnet dieselbe Geometrie nach beiden Verfahren. */ const engstelle = computeEngstellenIntergreen(raeumt(400, 50), OHNE_EINFAHRWEG); const knotenpunkt = computeIntergreen(raeumt(400, 50), OHNE_EINFAHRWEG); expect(engstelle.clearingPath, 'ohne Fahrzeuglaenge').toBe(400); expect(knotenpunkt.clearingPath, 'mit 6 m Pkw').toBe(406); expect(engstelle.vehicleLength).toBe(0); expect(knotenpunkt.vehicleLength).toBe(6); }); it('zieht keine Einfahrzeit ab und sagt es', () => { // "Die Zwischenzeit tz wird ohne Beruecksichtigung einer Einfahrzeit // ermittelt." Ein erfasster Einfahrweg geht damit in keine Zahl ein. const mitWeg = computeEngstellenIntergreen(raeumt(400, 50), { mode: 'kfz', movement: 'geradeaus', enteringDistance: 20, }); expect(mitWeg.enteringTime).toBe(0); expect(mitWeg.intergreen).toBe(40); const meldung = mitWeg.notes.find((n) => n.code === 'engstelle-einfahrweg-ohne-wirkung'); expect(meldung?.severity).toBe('hinweis'); expect(meldung?.message).toContain('5.2.2'); }); it('kennzeichnet das Verfahren im Ergebnis', () => { expect(computeEngstellenIntergreen(raeumt(400, 50), OHNE_EINFAHRWEG).verfahren).toBe( 'engstelle', ); expect(computeIntergreen(raeumt(400, 50), OHNE_EINFAHRWEG).verfahren).toBe('knotenpunkt'); }); it('faellt bei 40 km/h laenger aus als die Knotenpunktrechnung, bei 50 km/h kuerzer', () => { /* * DIE RICHTUNG HAENGT AN DER ZULAESSIGEN HOECHSTGESCHWINDIGKEIT, und das * ist der Grund, warum diese Umstellung nicht pauschal "sicherer" oder * "unsicherer" ist: * * 50 km/h: Vr 40 km/h = 11,11 m/s gegen 10,0 m/s - schneller, und der * Raeumweg ist 6 m kuerzer. Oberhalb von 40 m Engstelle faellt * die Zwischenzeit deshalb KUERZER aus. * 40 km/h: Vr 30 km/h = 8,33 m/s gegen 10,0 m/s - langsamer. Die * Zwischenzeit faellt LAENGER aus. */ expect(computeEngstellenIntergreen(raeumt(200, 50), OHNE_EINFAHRWEG).intergreen).toBe(22); expect(computeIntergreen(raeumt(200, 50), OHNE_EINFAHRWEG).intergreen).toBe(24); expect(computeEngstellenIntergreen(raeumt(200, 40), OHNE_EINFAHRWEG).intergreen).toBe(28); expect(computeIntergreen(raeumt(200, 40), OHNE_EINFAHRWEG).intergreen).toBe(24); }); it('trifft bei 40 m und 50 km/h dieselbe Sekunde wie die Knotenpunktrechnung', () => { // Ein nachgerechneter angeordneter Plan. Beide Wege ergeben 7,6 s -> 8 s. expect(computeEngstellenIntergreen(raeumt(40, 50), OHNE_EINFAHRWEG).intergreen).toBe(8); expect(computeIntergreen(raeumt(40, 50), OHNE_EINFAHRWEG).intergreen).toBe(8); }); }); describe('Die Handeingabe wirkt an der Engstelle nur nach unten', () => { it('nimmt eine langsamere Raeumgeschwindigkeit an der Beziehung an', () => { const langsam = computeEngstellenIntergreen( { ...raeumt(400, 50), clearingSpeedOverride: 8 }, OHNE_EINFAHRWEG, ); expect(langsam.clearingSpeed).toBe(8); expect(langsam.intergreen).toBe(54); }); it('verwirft eine schnellere und meldet das', () => { const schnell = computeEngstellenIntergreen( { ...raeumt(400, 50), clearingSpeedOverride: 13 }, OHNE_EINFAHRWEG, ); expect(schnell.intergreen).toBe(40); const meldung = schnell.notes.find( (n) => n.code === 'engstelle-raeumgeschwindigkeit-ueber-regelwert', ); expect(meldung?.severity).toBe('warnung'); }); }); describe('Umlaufzeit und Freigabezeit der Engstelle (Abschnitt 5.2.3)', () => { it('rechnet tU = (1,3 * TZ + 4) / (1 - Summe der Auslastungsgrade)', () => { // TZ = 2 x 20 s, q1/qS1 + q2/qS2 = 0,4: (1,3 * 40 + 4) / 0,6 = 93,33 s. expect(engstellenUmlaufzeit(40, 0.4)).toBeCloseTo(93.333, 3); // Andere Beiwerte als Webster (1,5 und 5) und eine andere Bezugsgroesse: // TZ ist die Summe der ZWISCHENzeiten, nicht die Verlustzeit je Umlauf. expect(RILSA_ENGSTELLE.umlaufzeitFaktor).toBe(1.3); expect(RILSA_ENGSTELLE.umlaufzeitZuschlag).toBe(4); }); it('liefert unendlich, wo die Summe der Auslastungsgrade 1 erreicht', () => { // "Dabei muss (q1/qS1 + q2/qS2) < 1 sein." expect(engstellenUmlaufzeit(40, 1)).toBe(Number.POSITIVE_INFINITY); expect(engstellenUmlaufzeit(40, 1.2)).toBe(Number.POSITIVE_INFINITY); }); it('verteilt die verbleibende Zeit im Verhaeltnis der Auslastungsgrade', () => { // tU 100 s, TZ 40 s, Auslastungsgrade 0,3 und 0,1: 45 s und 15 s. expect(engstellenFreigabezeit(0.3, 0.4, 100, 40)).toBeCloseTo(45, 6); expect(engstellenFreigabezeit(0.1, 0.4, 100, 40)).toBeCloseTo(15, 6); }); it('nennt 1500 Kfz/h als ueberschlaegliche Saettigungsverkehrsstaerke', () => { // Am Knotenpunkt rechnet dieses Programm mit 2000 Kfz/h nach HBS; fuer die // Engstelle nennt die RiLSA diesen kleineren Anhaltswert. expect(RILSA_ENGSTELLE.saettigungUeberschlaeglich).toBe(1500); expect(RILSA_ENGSTELLE.maxUmlaufzeit).toBe(300); }); }); describe('Der Plan waehlt das Verfahren nach der Anlagenart', () => { function baustelle(anlagenart: 'einstreifig' | 'knotenpunkt'): Project { const basis = createEmptyProject('Baustelle', T, anlagenart); 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]); return { ...basis, signalGroups: [a, b], conflicts: [ { ...createConflict(a.id, b.id), clearingDistance: 200, enteringDistance: 0 }, { ...createConflict(b.id, a.id), clearingDistance: 200, enteringDistance: 0 }, ], phases: [p1, p2], program: { ...basis.program, phaseOrder: [p1.id, p2.id] }, }; } it('rechnet die einstreifige Verkehrsfuehrung nach Abschnitt 5.2', () => { const plan = buildSignalPlan(baustelle('einstreifig')); for (const uebergang of plan.transitions) expect(uebergang.duration).toBe(22); for (const zwischenzeit of plan.intergreens.values()) { expect(zwischenzeit.calculation?.verfahren).toBe('engstelle'); } }); it('laesst den Knotenpunkt bei Abschnitt 2', () => { // Dieselbe Geometrie, andere Anlagenart: 3 + 206/10 = 23,6 -> 24 s. const plan = buildSignalPlan(baustelle('knotenpunkt')); for (const uebergang of plan.transitions) expect(uebergang.duration).toBe(24); for (const zwischenzeit of plan.intergreens.values()) { expect(zwischenzeit.calculation?.verfahren).toBe('knotenpunkt'); } }); it('nennt der Pruefbericht das Verfahren und die beiden ungeprueften Annahmen', () => { /* * Das Ergebnis haengt an zwei Aussagen ueber die Oertlichkeit, die dieses * Programm nicht pruefen kann. Beide senken die Raeumgeschwindigkeit; wer * sie uebersieht, schaltet zu kurz. Der Bericht nennt sie mit den Zahlen * DIESER Planung. */ const bericht = validateProject(baustelle('einstreifig'), undefined, T); const befund = bericht.findings.find((f) => f.rule === 'einstreifig.verfahren-abschnitt-5-2'); expect(befund?.severity).toBe('hinweis'); expect(befund?.message).toContain('40 km/h'); // Schlechte Fahrbahn: 30 km/h -> 4 + 200/30 * 3,6 = 28 s. expect(befund?.message).toContain('30 km/h'); expect(befund?.message).toContain('28 s'); // Radverkehr massgebend: 18 km/h -> 4 + 200/18 * 3,6 = 44 s. expect(befund?.message).toContain('18 km/h'); expect(befund?.message).toContain('44 s'); expect(befund?.reference).toContain('5.2.2'); }); it('meldet das Verfahren am Knotenpunkt nicht', () => { const bericht = validateProject(baustelle('knotenpunkt'), undefined, T); expect(bericht.findings.some((f) => f.rule === 'einstreifig.verfahren-abschnitt-5-2')).toBe( false, ); }); it('haelt die Untergrenze aus Abschnitt 2.5.2 von der Engstelle fern', () => { /* * Die Untergrenze tue + tr >= tG + 1 s gilt den Faellen 1 und 2 des * Abschnitts 2.5.2 - also der Knotenpunktrechnung. Das Engstellenverfahren * hat eine eigene, laengere Ueberfahrzeit und keine solche Bedingung; sie * dorthin zu uebertragen hiesse, das Regelwerk zu ergaenzen. */ const kurz = computeEngstellenIntergreen(raeumt(1, 70), OHNE_EINFAHRWEG); expect(kurz.ueberfahrRaeumzeitUntergrenze).toBeNull(); // 4 + 1/50 * 3,6 = 4,07 -> 5 s. Mit der Untergrenze waeren es 6 s. expect(kurz.intergreen).toBe(5); }); });