import { describe, expect, it } from 'vitest'; import { clearingSpeedFor, computeIntergreen } from '@/domain/rilsa/intergreen'; import { RILSA_DEFAULTS, SAFETY_FLOORS, type RilsaDefaults } from '@/domain/rilsa/constants'; import { resolveRilsaSettings } from '@/domain/rilsa/settings'; import { ueberfahrzeitHerkunft } from '@/domain/rilsa/ansaetze'; import type { ClearingStream, EnteringStream } from '@/domain/rilsa/types'; /** * Geschwindigkeiten und Schranken, behoben in Fassung 5.9.0. * * Befund 23 Zwischen "Raeumgeschwindigkeit Fussgaenger" und "Raeumgeschwindig- * keit Fussgaenger mit erhoehtem Zeitbedarf" bestand keine Schranke. * Wer den allgemeinen Wert unter den des Merkmals senkte, liess den * mobilitaetseingeschraenkten Fussgaenger rechnerisch SCHNELLER * raeumen als den Regelfall: Das angehakte Merkmal VERKUERZTE die * Zwischenzeit. Dieselbe Luecke bestand zwischen "Kfz geradeaus" und * "Kfz abbiegend", obwohl der Quelltext daneben zusagt, abbiegende * Kraftfahrzeuge raeumten langsamer. * Befund 28 Eine von Hand eingetragene Raeumgeschwindigkeit wurde nur gegen * die Schranke der VERKEHRSART geprueft, nicht gegen den Regelwert * der Fahrbeziehung. * Befund 29 Zwischen Regelwert und Schranke - abbiegendes Kfz 7,0 bis 10,0 m/s, * Furt mit erhoehtem Zeitbedarf 1,0 bis 1,2 m/s - lief die Eingabe * ohne eine einzige Meldung durch und verkuerzte die Zwischenzeit. * Befund 58 Eine verworfene Ueberfahrzeit-Handeingabe wurde im Rechenweg * weiterhin als "von Hand eingetragen" ausgewiesen, obwohl mit dem * Regelwert gerechnet wurde. * Befund 59 Die Meldung zum OePNV mit Vmax ueber 70 km/h behauptete auch im * Haltefall eine Ueberfahrzeit aus der obersten Staffelstufe. */ function fussRaeumend(extra: Partial = {}): ClearingStream { return { mode: 'fuss', movement: 'geradeaus', clearingDistance: 12, vehicleClass: 'keine', vZul: 0, ...extra, }; } function abbiegerRaeumend(extra: Partial = {}): ClearingStream { return { mode: 'kfz', movement: 'rechts', clearingDistance: 40, vehicleClass: 'pkw', vZul: 50, ...extra, }; } function bahnRaeumend(extra: Partial = {}): ClearingStream { return { mode: 'oepnv', movement: 'geradeaus', clearingDistance: 30, vehicleClass: 'strassenbahn', vZul: 80, ...extra, }; } function kfzEinfahrend(enteringDistance = 8): EnteringStream { return { mode: 'kfz', movement: 'geradeaus', enteringDistance }; } /** Kennwertsatz mit abgesenkter Raeumgeschwindigkeit der Fussgaenger. */ function mitLangsamenFussgaengern(fuss: number): RilsaDefaults { return { ...RILSA_DEFAULTS, clearingSpeed: { ...RILSA_DEFAULTS.clearingSpeed, fuss }, }; } describe('Befund 23 - erhoehter Zeitbedarf darf die Zwischenzeit nie verkuerzen', () => { it('setzt fuer die Furt mit erhoehtem Zeitbedarf hoechstens die Raeumgeschwindigkeit des Regelfalls an', () => { // Ohne Riegel raeumte der mobilitaetseingeschraenkte Fussgaenger mit // 1,0 m/s, waehrend der Regelfall nur 0,8 m/s bekam. const defaults = mitLangsamenFussgaengern(0.8); expect(clearingSpeedFor({ mode: 'fuss', movement: 'geradeaus' }, defaults)).toBe(0.8); expect( clearingSpeedFor({ mode: 'fuss', movement: 'geradeaus', reducedMobility: true }, defaults), ).toBe(0.8); }); it('verkuerzt die Zwischenzeit der Furt mit erhoehtem Zeitbedarf nicht', () => { const defaults = mitLangsamenFussgaengern(0.8); const ohne = computeIntergreen(fussRaeumend(), kfzEinfahrend(6), defaults); const mit = computeIntergreen( fussRaeumend({ reducedMobility: true }), kfzEinfahrend(6), defaults, ); // 12 m / 0,8 m/s = 15 s in beiden Faellen; zuvor 12 m / 1,0 m/s = 12 s mit // dem Merkmal - drei Sekunden kuerzer fuer die schutzbeduerftigste // Verkehrsart. expect(ohne.clearingTime).toBe(15); expect(mit.clearingTime).toBe(15); expect(mit.intergreen).toBeGreaterThanOrEqual(ohne.intergreen); }); it('meldet den Widerspruch am Kennwertsatz, statt ihn still zu kappen', () => { const ergebnis = computeIntergreen( fussRaeumend({ reducedMobility: true }), kfzEinfahrend(6), mitLangsamenFussgaengern(0.8), ); const notiz = ergebnis.notes.find( (n) => n.code === 'erhoehter-zeitbedarf-schneller-als-regelfall', ); expect(notiz?.severity).toBe('fehler'); expect(notiz?.message).toContain('erhöhtem Zeitbedarf'); }); it('kappt die Vorgabe "erhoehter Zeitbedarf" auf den Fussgaengerwert und weist die Abweichung aus', () => { const { defaults, notes, deviations } = resolveRilsaSettings({ clearingSpeedFuss: 0.8 }); expect(defaults.clearingSpeed.fuss).toBe(0.8); expect(defaults.clearingSpeed.fussReducedMobility).toBe(0.8); const notiz = notes.find( (n) => n.code === 'vorgabe-ueberschritten:clearingSpeedFussReducedMobility', ); expect(notiz?.severity).toBe('fehler'); const zeile = deviations.find((d) => d.key === 'clearingSpeedFussReducedMobility'); expect(zeile?.appliedValue).toBe(0.8); }); it('haelt die Obergrenze des Feldes an der Sicherheitsschranke aus constants.ts', () => { const { defaults, notes } = resolveRilsaSettings({ clearingSpeedFussReducedMobility: SAFETY_FLOORS.maxClearingSpeedFuss + 0.3, }); expect(defaults.clearingSpeed.fussReducedMobility).toBe(SAFETY_FLOORS.maxClearingSpeedFuss); expect( notes.some((n) => n.code === 'vorgabe-ueberschritten:clearingSpeedFussReducedMobility'), ).toBe(true); }); it('laesst abbiegende Kraftfahrzeuge nie schneller raeumen als geradeaus fahrende', () => { const defaults: RilsaDefaults = { ...RILSA_DEFAULTS, clearingSpeed: { ...RILSA_DEFAULTS.clearingSpeed, kfzStraight: 6 }, }; expect(clearingSpeedFor({ mode: 'kfz', movement: 'rechts' }, defaults)).toBe(6); const { defaults: aufgeloest, notes } = resolveRilsaSettings({ clearingSpeedKfzStraight: 6 }); expect(aufgeloest.clearingSpeed.kfzTurning).toBe(6); expect( notes.some( (n) => n.code === 'vorgabe-ueberschritten:clearingSpeedKfzTurning' && n.severity === 'fehler', ), ).toBe(true); }); }); describe('Befunde 28 und 29 - Handeingabe ueber dem Regelwert des Stroms', () => { it('meldet eine Raeumgeschwindigkeit oberhalb des Regelwerts der Fahrbeziehung', () => { // Regelwert des Abbiegers 7,0 m/s, Schranke der Verkehrsart 10,0 m/s. // 9 m/s lag dazwischen und lief bislang ohne ein Wort durch: tz fiel von // 9 s auf 8 s. const ergebnis = computeIntergreen( abbiegerRaeumend({ clearingSpeedOverride: 9 }), kfzEinfahrend(), ); expect(ergebnis.clearingSpeed).toBe(9); expect(ergebnis.intergreen).toBe(8); const notiz = ergebnis.notes.find((n) => n.code === 'raeumgeschwindigkeit-ueber-regelwert'); expect(notiz?.severity).toBe('warnung'); expect(notiz?.message).toContain('9'); expect(notiz?.message).toContain('7'); expect(notiz?.message).toContain('zu begründen'); }); it('meldet eine Handeingabe, die den erhoehten Zeitbedarf aufhebt', () => { // Regelwert der Furt mit erhoehtem Zeitbedarf 1,0 m/s, Schranke 1,2 m/s. const ergebnis = computeIntergreen( fussRaeumend({ reducedMobility: true, clearingSpeedOverride: 1.2 }), kfzEinfahrend(6), ); expect(ergebnis.clearingSpeed).toBe(1.2); const notiz = ergebnis.notes.find((n) => n.code === 'raeumgeschwindigkeit-ueber-regelwert'); expect(notiz?.severity).toBe('warnung'); expect(notiz?.message).toContain('1'); }); it('meldet auch die gekappte Eingabe, wenn die Obergrenze ueber dem Regelwert liegt', () => { const ergebnis = computeIntergreen( abbiegerRaeumend({ clearingSpeedOverride: 25 }), kfzEinfahrend(), ); expect(ergebnis.clearingSpeed).toBe(SAFETY_FLOORS.maxClearingSpeedKfz); expect(ergebnis.notes.some((n) => n.code === 'raeumgeschwindigkeit-zu-hoch')).toBe(true); expect(ergebnis.notes.some((n) => n.code === 'raeumgeschwindigkeit-ueber-regelwert')).toBe( true, ); }); it('schweigt bei einer Eingabe unterhalb des Regelwerts', () => { const ergebnis = computeIntergreen( abbiegerRaeumend({ clearingSpeedOverride: 5 }), kfzEinfahrend(), ); expect(ergebnis.notes.some((n) => n.code === 'raeumgeschwindigkeit-ueber-regelwert')).toBe( false, ); }); it('nennt bei gesetztem engem Innenradius den Regelwert des Stroms, nicht den Praxisansatz', () => { /* * Der Praxisansatz "enger Innenradius" (5,0 m/s) ist ausdruecklich kein * Regelwerkswert - quellen.ts fuehrt ihn unter 'praxis-enger-radius' mit * dem Pruefstand "abzugleichen". Die Meldung nannte ihn trotzdem * "Regelwert 5 m/s dieses Stroms", waehrend das Feld im selben Fenster * korrekt "ohne diesen Eintrag 5 m/s" sagt. Der wirkliche Regelwert des * abbiegenden Kfz-Stroms (7,0 m/s) kam in der Meldung nicht vor, obwohl * sie die begruendungspflichtige Abweichung ausweisen soll. */ const ergebnis = computeIntergreen( abbiegerRaeumend({ engerRadius: true, clearingSpeedOverride: 9 }), kfzEinfahrend(), ); const notiz = ergebnis.notes.find((n) => n.code === 'raeumgeschwindigkeit-ueber-regelwert'); expect(notiz?.severity).toBe('warnung'); expect(notiz?.message).toContain('Regelwert 7 m/s'); expect(notiz?.message).not.toContain('Regelwert 5 m/s'); }); it('schweigt bei engem Innenradius, solange die Handeingabe unter dem Regelwert bleibt', () => { /* * 6 m/s liegen unter dem Regelwert des Abbiegers (7,0 m/s) und damit auf * der sicheren Seite - kein Regelwertverstoss. Dass die Handeingabe das * Merkmal aushebelt, meldet 'enger-radius-durch-vorgabe-ersetzt' samt * Richtung; zwei Warnungen zu einer Tatsache stuenden sonst * nebeneinander in Pruefbericht und Planunterlage. */ const ergebnis = computeIntergreen( abbiegerRaeumend({ engerRadius: true, clearingSpeedOverride: 6 }), kfzEinfahrend(), ); expect(ergebnis.notes.some((n) => n.code === 'raeumgeschwindigkeit-ueber-regelwert')).toBe( false, ); const ersetzt = ergebnis.notes.find((n) => n.code === 'enger-radius-durch-vorgabe-ersetzt'); expect(ersetzt?.message).toContain('6 m/s'); expect(ersetzt?.message).toContain('5 m/s'); }); it('schweigt, wo Regelwert und Schranke zusammenfallen', () => { const ergebnis = computeIntergreen( abbiegerRaeumend({ movement: 'geradeaus', clearingSpeedOverride: 10 }), kfzEinfahrend(), ); expect(ergebnis.notes.some((n) => n.code === 'raeumgeschwindigkeit-ueber-regelwert')).toBe( false, ); }); }); /* * NACHGEZOGEN (Fassung 5.43.0): `ueberfahrzeitHerkunft` nimmt das * VERFAHREN als erstes Argument, weil an einer Engstelle nicht der Kfz-Ansatz * gilt, sondern die feste Ueberfahrzeit des Abschnitts 5.2.2. Diese drei Faelle * fragen nach dem Knotenpunkt und geben es deshalb als 'knotenpunkt' mit; ihre * Aussage ist unveraendert. Den neuen Zweig prueft * tests/domain/bezugswerteVerfahren.test.ts. */ describe('Befund 58 - Herkunft der Ueberfahrzeit am angesetzten Wert', () => { it('nennt eine uebernommene Handeingabe weiterhin als solche', () => { expect( ueberfahrzeitHerkunft('knotenpunkt', 'kfz', 'gelbzeit', { vorgegeben: true, vorgabewert: 4, angesetzt: 4, }), ).toBe('an dieser Beziehung von Hand eingetragen'); }); it('weist eine verworfene Handeingabe nicht mehr als angesetzten Wert aus', () => { // Eingetragen waren 0 s, gerechnet wurde mit der Gelbzeit von 3 s. const text = ueberfahrzeitHerkunft('knotenpunkt', 'kfz', 'gelbzeit', { vorgegeben: true, vorgabewert: 0, angesetzt: 3, }); expect(text).toContain('verworfen'); expect(text).not.toBe('an dieser Beziehung von Hand eingetragen'); }); it('nennt im verworfenen Fall die Herleitung des angesetzten Werts', () => { const text = ueberfahrzeitHerkunft('knotenpunkt', 'fuss', 'gelbzeit', { vorgegeben: true, vorgabewert: -1, angesetzt: 1, }); expect(text).toContain('Fußgänger'); expect(text).toContain('verworfen'); }); }); describe('Befund 59 - OePNV ueber 70 km/h im Haltefall', () => { it('nennt im Haltefall den Anfahransatz und die Ueberfahrzeit 0 s', () => { const ergebnis = computeIntergreen(bahnRaeumend({ haltVorKnoten: true }), kfzEinfahrend(10)); expect(ergebnis.crossingTime).toBe(0); const notiz = ergebnis.notes.find((n) => n.code === 'oepnv-vmax-ueber-70'); expect(notiz?.message).not.toContain('Staffelstufe'); expect(notiz?.message).toContain('Anfahransatz'); expect(notiz?.message).toContain('0 s'); }); it('nennt ohne Halt weiterhin die oberste Staffelstufe', () => { const ergebnis = computeIntergreen(bahnRaeumend(), kfzEinfahrend(10)); expect(ergebnis.crossingTime).toBe(7); const notiz = ergebnis.notes.find((n) => n.code === 'oepnv-vmax-ueber-70'); expect(notiz?.message).toContain('obersten Staffelstufe'); }); });