lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests domain raeumgeschwindigkeitVmaxDeckel.test.ts
| 1 | import { describe, expect, it } from 'vitest'; |
| 2 | import { computeIntergreen, regelRaeumgeschwindigkeit } from '@/domain/rilsa/intergreen'; |
| 3 | import { RILSA_DEFAULTS } from '@/domain/rilsa/constants'; |
| 4 | import { buildSignalPlan } from '@/domain/plan/signalPlan'; |
| 5 | import { validateProject } from '@/domain/validation'; |
| 6 | import { ASSISTENT_VORGABE, projektAusAngaben } from '@/ui/assistent'; |
| 7 | import type { ClearingStream, EnteringStream } from '@/domain/rilsa/types'; |
| 8 | |
| 9 | /** |
| 10 | * Nicht schneller raeumen, als gefahren werden darf. |
| 11 | * |
| 12 | * DER BEFUND KAM AUS EINEM ANGEORDNETEN PLAN, nicht aus dem Quelltext. Am |
| 13 | * 10.09.2026 sind zwei reale, behoerdlich abgesegnete Anordnungen gegen dieses |
| 14 | * Programm gerechnet worden: |
| 15 | * |
| 16 | * - Plan A: einstreifige Fuehrung, 100 m Engstelle, Raeumung mit 30 km/h, |
| 17 | * gerechnet mit fremder Software: |
| 18 | * **16 s**. Dieses Programm rechnete **14 s** - zwei Sekunden KUERZER als |
| 19 | * die Unterlage, die die Behoerde abgesegnet hat. |
| 20 | * - Plan B: 40 m Raeumweg, 50 km/h, angeordnet mit |
| 21 | * 6 s. Dieses Programm rechnet 8 s - zwei Sekunden laenger, und das ist die |
| 22 | * gewollte Korrektur gegenueber der Fassung 4.x. |
| 23 | * |
| 24 | * BEIDE ABWEICHUNGEN HABEN DIESELBE URSACHE, und sie zeigen in |
| 25 | * entgegengesetzte Richtungen: Die fremde Software und die Fassung 4.x rechnen |
| 26 | * mit vr = Vmax/3,6, dieses Programm mit dem festen Regelwert 10,0 m/s. Ueber |
| 27 | * 36 km/h ist der feste Wert der kleinere und damit der sichere - das ist die |
| 28 | * Korrektur, die 5.0 ausdruecklich eingefuehrt hat. UNTER 36 km/h kehrt sich |
| 29 | * das um: Bei 30 km/h raeumte ein Fahrzeug rechnerisch mit 36 km/h, also |
| 30 | * schneller, als es fahren darf, und die Zwischenzeit fiel zu kurz aus. |
| 31 | * |
| 32 | * DER GRUNDSATZ STAND SCHON IM PROGRAMM, nur nicht an dieser Verkehrsart: |
| 33 | * `SAFETY_FLOORS.maxClearingSpeedOepnv` traegt den Satz "nicht schneller |
| 34 | * raeumen, als gefahren werden darf" im Kommentar, und fuer den OePNV wird er |
| 35 | * mit `Math.min(..., vZul / 3.6)` auch angewandt. Fuer Kraftfahrzeuge galt |
| 36 | * allein der feste Deckel von 10,0 m/s, ohne Bezug zur Vmax des Stroms. |
| 37 | * |
| 38 | * WAS DIESE DATEI BEWACHT: dass der Deckel gilt, dass er nur nach unten wirkt, |
| 39 | * dass er die Einfahrgeschwindigkeit NICHT anfasst und dass er sich meldet. |
| 40 | */ |
| 41 | |
| 42 | const D = RILSA_DEFAULTS; |
| 43 | |
| 44 | function raeumt(teil: Partial<ClearingStream> = {}): ClearingStream { |
| 45 | return { |
| 46 | mode: 'kfz', |
| 47 | movement: 'geradeaus', |
| 48 | clearingDistance: 100, |
| 49 | vehicleClass: 'pkw', |
| 50 | vZul: 30, |
| 51 | ...teil, |
| 52 | }; |
| 53 | } |
| 54 | |
| 55 | function faehrtEin(teil: Partial<EnteringStream> = {}): EnteringStream { |
| 56 | return { mode: 'kfz', movement: 'geradeaus', enteringDistance: 0, ...teil }; |
| 57 | } |
| 58 | |
| 59 | describe('Der Deckel an der zulaessigen Hoechstgeschwindigkeit', () => { |
| 60 | it('rechnet den angeordneten Plan A nach', () => { |
| 61 | /* |
| 62 | * DER FALL, DER DEN BEFUND AUSGELOEST HAT. 100 m Engstelle, 30 km/h. |
| 63 | * |
| 64 | * Rechenweg mit dem Deckel: sr = 100 m + 6 m Laengenzuschlag des Pkw = |
| 65 | * 106 m; vr = 30 / 3,6 = 8,333 m/s; tr = 12,72 s; te = 0 s (der |
| 66 | * Gegenverkehr steht an der Haltlinie); tue = 3 s (Gelbzeit bei 30 km/h). |
| 67 | * tz = 15,72 s, aufgerundet 16 s - genau die angeordnete Zahl. |
| 68 | * |
| 69 | * Ohne den Deckel waeren es 10,6 s Raeumzeit und 14 s Zwischenzeit. |
| 70 | */ |
| 71 | const r = computeIntergreen(raeumt(), faehrtEin(), D); |
| 72 | expect(r.clearingSpeed, 'geraeumt wird mit der Vmax des Stroms').toBeCloseTo(30 / 3.6, 6); |
| 73 | expect(r.intergreen, 'die angeordnete Zwischenzeit').toBe(16); |
| 74 | }); |
| 75 | |
| 76 | it('laesst den Regelwert stehen, wo er ohnehin der kleinere ist', () => { |
| 77 | /* |
| 78 | * Die Gegenprobe zur Fassung 4.x. Bei 50 km/h waere Vmax/3,6 = 13,9 m/s - |
| 79 | * deutlich ueber dem Regelwert. Der Deckel darf dort NICHTS tun, sonst |
| 80 | * naehme er die Korrektur zurueck, die 5.0 eingefuehrt hat: Der |
| 81 | * angeordnete Plan B traegt 6 s, dieses Programm rechnet 8 s, und |
| 82 | * das soll so bleiben. |
| 83 | */ |
| 84 | const r = computeIntergreen(raeumt({ clearingDistance: 40, vZul: 50 }), faehrtEin(), D); |
| 85 | expect(r.clearingSpeed, 'der Regelwert gilt weiter').toBe(D.clearingSpeed.kfzStraight); |
| 86 | expect(r.intergreen).toBe(8); |
| 87 | }); |
| 88 | |
| 89 | it('greift genau unterhalb von 36 km/h', () => { |
| 90 | // 10,0 m/s entsprechen 36 km/h. Darueber ist der Regelwert der kleinere, |
| 91 | // darunter die Vmax - die Grenze liegt also dort, wo beide gleich sind. |
| 92 | expect(regelRaeumgeschwindigkeit({ mode: 'kfz', movement: 'geradeaus', vZul: 36 }, D)).toBe(10); |
| 93 | expect( |
| 94 | regelRaeumgeschwindigkeit({ mode: 'kfz', movement: 'geradeaus', vZul: 35 }, D), |
| 95 | ).toBeCloseTo(35 / 3.6, 6); |
| 96 | expect(regelRaeumgeschwindigkeit({ mode: 'kfz', movement: 'geradeaus', vZul: 40 }, D)).toBe(10); |
| 97 | }); |
| 98 | |
| 99 | it('ruehrt den abbiegenden Strom nicht an, der ohnehin langsamer raeumt', () => { |
| 100 | // 7,0 m/s liegen unter 8,33 m/s; der Deckel darf nicht nach OBEN wirken. |
| 101 | expect(regelRaeumgeschwindigkeit({ mode: 'kfz', movement: 'links', vZul: 30 }, D)).toBe( |
| 102 | D.clearingSpeed.kfzTurning, |
| 103 | ); |
| 104 | }); |
| 105 | |
| 106 | it('ruehrt Fussgaenger und Radverkehr nicht an', () => { |
| 107 | /* |
| 108 | * Ihre Raeumgeschwindigkeit hat mit der zulaessigen Hoechstgeschwindigkeit |
| 109 | * des Fahrverkehrs nichts zu tun: Ein Fussgaenger geht nicht schneller, |
| 110 | * weil dort 50 gefahren werden darf, und nicht langsamer, weil dort 30 |
| 111 | * gilt. |
| 112 | * |
| 113 | * GEMESSEN AN EINER MUTATION: Mit 30 km/h bewies dieser Fall gar nichts - |
| 114 | * der Deckel laege bei 8,33 m/s, und 1,2 bzw. 4,0 m/s liegen ohnehin |
| 115 | * darunter. Wer die Verkehrsartabfrage aus `vmaxDeckel` entfernte, blieb |
| 116 | * gruen. Geprueft wird deshalb bei Schrittgeschwindigkeit: 4 km/h ergaeben |
| 117 | * einen Deckel von 1,11 m/s, und der laege UNTER der Gehgeschwindigkeit. |
| 118 | * Zeichen 325 gibt es wirklich; hier steht es fuer den Grenzfall, an dem |
| 119 | * sich die Abfrage ueberhaupt erst zeigt. |
| 120 | */ |
| 121 | expect(regelRaeumgeschwindigkeit({ mode: 'fuss', movement: 'querung', vZul: 4 }, D)).toBe( |
| 122 | D.clearingSpeed.fuss, |
| 123 | ); |
| 124 | expect(regelRaeumgeschwindigkeit({ mode: 'rad', movement: 'querung', vZul: 4 }, D)).toBe( |
| 125 | D.clearingSpeed.rad, |
| 126 | ); |
| 127 | // Und beim gewoehnlichen Tempo 30 selbstverstaendlich auch. |
| 128 | expect(regelRaeumgeschwindigkeit({ mode: 'fuss', movement: 'querung', vZul: 30 }, D)).toBe( |
| 129 | D.clearingSpeed.fuss, |
| 130 | ); |
| 131 | }); |
| 132 | |
| 133 | it('faellt ohne eingetragene Vmax auf den Regelwert zurueck', () => { |
| 134 | // Ohne Vmax gibt es keinen Deckel. Der Regelwert ist dann das Beste, was |
| 135 | // das Programm weiss - und er ist konservativer als jede Annahme. |
| 136 | expect(regelRaeumgeschwindigkeit({ mode: 'kfz', movement: 'geradeaus', vZul: 0 }, D)).toBe( |
| 137 | D.clearingSpeed.kfzStraight, |
| 138 | ); |
| 139 | }); |
| 140 | |
| 141 | it('deckelt die EINFAHRgeschwindigkeit ausdruecklich nicht', () => { |
| 142 | /* |
| 143 | * DIE SCHRANKE LIEGT DORT UNTEN, NICHT OBEN, und wer die Symmetrie |
| 144 | * herstellen wollte, kehrte die Sicherheitsrichtung um: In |
| 145 | * tz = tue + tr − te wird die Einfahrzeit ABGEZOGEN. Eine niedrigere |
| 146 | * Einfahrgeschwindigkeit verlaengert te und VERKUERZT damit die |
| 147 | * Zwischenzeit. Bei 30 km/h bleibt es deshalb bei den 11,1 m/s. |
| 148 | */ |
| 149 | const r = computeIntergreen( |
| 150 | raeumt({ clearingDistance: 20 }), |
| 151 | faehrtEin({ enteringDistance: 10 }), |
| 152 | D, |
| 153 | ); |
| 154 | expect(r.enteringSpeed, 'die Einfahrgeschwindigkeit ist gedeckelt worden').toBe( |
| 155 | D.enteringSpeed.kfz, |
| 156 | ); |
| 157 | }); |
| 158 | |
| 159 | it('sagt im Rechenweg, dass gedeckelt wurde', () => { |
| 160 | /* |
| 161 | * Eine stille Ersetzung waere hier besonders schlecht: Das |
| 162 | * Kennwerteverzeichnis der Planunterlage nennt fuer den geradeaus |
| 163 | * fahrenden Kfz-Strom 10,0 m/s, im Rechenweg derselben Unterlage stuenden |
| 164 | * 8,33 m/s, und nichts erklaerte den Unterschied. |
| 165 | */ |
| 166 | const r = computeIntergreen(raeumt(), faehrtEin(), D); |
| 167 | const notiz = r.notes.find((n) => n.code === 'raeumgeschwindigkeit-auf-vmax-gedeckelt'); |
| 168 | expect(notiz, 'die Kappung meldet sich nicht').toBeDefined(); |
| 169 | expect(notiz?.severity, 'das ist der Regelfall und kein Fehler').toBe('hinweis'); |
| 170 | expect(notiz?.message, 'der Regelwert fehlt in der Meldung').toContain('10 m/s'); |
| 171 | expect(notiz?.message, 'der angesetzte Wert fehlt in der Meldung').toContain('8,33 m/s'); |
| 172 | expect(notiz?.message, 'die Vmax fehlt in der Meldung').toContain('30 km/h'); |
| 173 | }); |
| 174 | |
| 175 | it('meldet dabei keinen Fehler ueber die Vorgaben', () => { |
| 176 | /* |
| 177 | * Die Kappung darf nicht in die Meldung "Räumgeschwindigkeit aus den |
| 178 | * Vorgaben über der Obergrenze" laufen. Die waere doppelt falsch: Der Wert |
| 179 | * kommt nicht aus den Vorgaben, sondern aus dem Regelwerk, und er ist kein |
| 180 | * Fehler, sondern der Regelfall an jeder Anlage mit Tempo 30. |
| 181 | */ |
| 182 | const r = computeIntergreen(raeumt(), faehrtEin(), D); |
| 183 | expect(r.notes.filter((n) => n.severity === 'fehler')).toEqual([]); |
| 184 | expect(r.notes.map((n) => n.code)).not.toContain('raeumgeschwindigkeit-vorgabe-zu-hoch'); |
| 185 | }); |
| 186 | |
| 187 | it('steht die Kappung einmal je Projekt im Pruefbericht', () => { |
| 188 | /* |
| 189 | * DER RECHENWEG ALLEIN GENUEGT NICHT. Die Hinweise aus |
| 190 | * `computeIntergreen` verwirft der Planaufbau, bevor sie in `plan.notes` |
| 191 | * gelangen - zu Recht, denn an einer Anlage mit Tempo 30 traefe die |
| 192 | * Meldung jede einzelne Beziehung. Gedruckt wird der Pruefbericht aber |
| 193 | * aus `plan.notes` und den Regeln; ohne diesen Befund stuende im |
| 194 | * Kennwertnachweis der Unterlage 10,0 m/s und im Rechenweg daneben ein |
| 195 | * kleinerer Wert, ohne ein Wort dazu. |
| 196 | */ |
| 197 | /* |
| 198 | * AM KNOTENPUNKT GEMESSEN (nachgezogen mit 5.27.0): Bis dahin stand hier |
| 199 | * eine einstreifige Verkehrsfuehrung mit Tempo 30. Die rechnet das |
| 200 | * Programm seit 5.27.0 nach RiLSA 2015, Abschnitt 5.2.2, und dort kommt |
| 201 | * die Raeumgeschwindigkeit aus der eigenen Staffel des Abschnitts - der |
| 202 | * Deckel aus 5.22.0 greift dann nicht mehr, weil es nichts zu deckeln |
| 203 | * gibt. Er gilt unveraendert am Knotenpunkt und an der |
| 204 | * Fussgaengerschutzanlage, und genau dort wird er jetzt gemessen. Fuer die |
| 205 | * Engstelle hat Abschnitt 5.2 einen eigenen Deckel bekommen (siehe |
| 206 | * `engstellenRaeumgeschwindigkeit`); er greift erst unter 30 km/h. |
| 207 | */ |
| 208 | const anlage = projektAusAngaben( |
| 209 | { ...ASSISTENT_VORGABE, form: 'kreuzung', vZul: 30 }, |
| 210 | 'Tempo 30', |
| 211 | new Date('2025-03-11T12:00:00Z'), |
| 212 | ); |
| 213 | const plan = buildSignalPlan(anlage); |
| 214 | const bericht = validateProject(anlage, plan, new Date('2025-03-11T12:00:00Z')); |
| 215 | const befunde = bericht.findings.filter( |
| 216 | (f) => f.rule === 'zwischenzeiten.raeumgeschwindigkeit-auf-vmax-gedeckelt', |
| 217 | ); |
| 218 | expect(befunde, 'genau ein Befund, nicht einer je Beziehung').toHaveLength(1); |
| 219 | expect(befunde[0]?.severity).toBe('hinweis'); |
| 220 | expect(befunde[0]?.message, 'die betroffenen Gruppen fehlen').toContain('"K1"'); |
| 221 | expect(befunde[0]?.message, 'die betroffenen Gruppen fehlen').toContain('"K2"'); |
| 222 | |
| 223 | // Und bei 50 km/h steht er nicht da. |
| 224 | const schnell = projektAusAngaben( |
| 225 | { ...ASSISTENT_VORGABE, form: 'einstreifig', vZul: 50, engstellenlaenge: 100 }, |
| 226 | 'Vergleich', |
| 227 | new Date('2025-03-11T12:00:00Z'), |
| 228 | ); |
| 229 | const planSchnell = buildSignalPlan(schnell); |
| 230 | expect( |
| 231 | validateProject(schnell, planSchnell, new Date('2025-03-11T12:00:00Z')).findings.map( |
| 232 | (f) => f.rule, |
| 233 | ), |
| 234 | 'bei 50 km/h wird nichts gedeckelt', |
| 235 | ).not.toContain('zwischenzeiten.raeumgeschwindigkeit-auf-vmax-gedeckelt'); |
| 236 | }); |
| 237 | |
| 238 | it('laesst eine ueberhoehte Vorgabe weiter auffallen', () => { |
| 239 | /* |
| 240 | * Die Gegenrichtung: Ein Kennwertsatz mit 12 m/s liegt sowohl ueber der |
| 241 | * harten Schranke von 10 m/s als auch ueber der Vmax. Beides sind eigene |
| 242 | * Sachverhalte, und der Fehler zur Vorgabe darf nicht dadurch verschwinden, |
| 243 | * dass der Deckel schon gegriffen hat. |
| 244 | */ |
| 245 | const ueberhoeht = { |
| 246 | ...D, |
| 247 | clearingSpeed: { ...D.clearingSpeed, kfzStraight: 12 }, |
| 248 | }; |
| 249 | const r = computeIntergreen(raeumt(), faehrtEin(), ueberhoeht); |
| 250 | expect(r.notes.map((n) => n.code)).toContain('raeumgeschwindigkeit-vorgabe-zu-hoch'); |
| 251 | expect(r.clearingSpeed, 'am Ende gilt trotzdem die Vmax').toBeCloseTo(30 / 3.6, 6); |
| 252 | }); |
| 253 | }); |