lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests domain geschwindigkeitsschranken.test.ts
| 1 | import { describe, expect, it } from 'vitest'; |
| 2 | import { clearingSpeedFor, computeIntergreen } from '@/domain/rilsa/intergreen'; |
| 3 | import { RILSA_DEFAULTS, SAFETY_FLOORS, type RilsaDefaults } from '@/domain/rilsa/constants'; |
| 4 | import { resolveRilsaSettings } from '@/domain/rilsa/settings'; |
| 5 | import { ueberfahrzeitHerkunft } from '@/domain/rilsa/ansaetze'; |
| 6 | import type { ClearingStream, EnteringStream } from '@/domain/rilsa/types'; |
| 7 | |
| 8 | /** |
| 9 | * Geschwindigkeiten und Schranken, behoben in Fassung 5.9.0. |
| 10 | * |
| 11 | * Befund 23 Zwischen "Raeumgeschwindigkeit Fussgaenger" und "Raeumgeschwindig- |
| 12 | * keit Fussgaenger mit erhoehtem Zeitbedarf" bestand keine Schranke. |
| 13 | * Wer den allgemeinen Wert unter den des Merkmals senkte, liess den |
| 14 | * mobilitaetseingeschraenkten Fussgaenger rechnerisch SCHNELLER |
| 15 | * raeumen als den Regelfall: Das angehakte Merkmal VERKUERZTE die |
| 16 | * Zwischenzeit. Dieselbe Luecke bestand zwischen "Kfz geradeaus" und |
| 17 | * "Kfz abbiegend", obwohl der Quelltext daneben zusagt, abbiegende |
| 18 | * Kraftfahrzeuge raeumten langsamer. |
| 19 | * Befund 28 Eine von Hand eingetragene Raeumgeschwindigkeit wurde nur gegen |
| 20 | * die Schranke der VERKEHRSART geprueft, nicht gegen den Regelwert |
| 21 | * der Fahrbeziehung. |
| 22 | * Befund 29 Zwischen Regelwert und Schranke - abbiegendes Kfz 7,0 bis 10,0 m/s, |
| 23 | * Furt mit erhoehtem Zeitbedarf 1,0 bis 1,2 m/s - lief die Eingabe |
| 24 | * ohne eine einzige Meldung durch und verkuerzte die Zwischenzeit. |
| 25 | * Befund 58 Eine verworfene Ueberfahrzeit-Handeingabe wurde im Rechenweg |
| 26 | * weiterhin als "von Hand eingetragen" ausgewiesen, obwohl mit dem |
| 27 | * Regelwert gerechnet wurde. |
| 28 | * Befund 59 Die Meldung zum OePNV mit Vmax ueber 70 km/h behauptete auch im |
| 29 | * Haltefall eine Ueberfahrzeit aus der obersten Staffelstufe. |
| 30 | */ |
| 31 | |
| 32 | function fussRaeumend(extra: Partial<ClearingStream> = {}): ClearingStream { |
| 33 | return { |
| 34 | mode: 'fuss', |
| 35 | movement: 'geradeaus', |
| 36 | clearingDistance: 12, |
| 37 | vehicleClass: 'keine', |
| 38 | vZul: 0, |
| 39 | ...extra, |
| 40 | }; |
| 41 | } |
| 42 | |
| 43 | function abbiegerRaeumend(extra: Partial<ClearingStream> = {}): ClearingStream { |
| 44 | return { |
| 45 | mode: 'kfz', |
| 46 | movement: 'rechts', |
| 47 | clearingDistance: 40, |
| 48 | vehicleClass: 'pkw', |
| 49 | vZul: 50, |
| 50 | ...extra, |
| 51 | }; |
| 52 | } |
| 53 | |
| 54 | function bahnRaeumend(extra: Partial<ClearingStream> = {}): ClearingStream { |
| 55 | return { |
| 56 | mode: 'oepnv', |
| 57 | movement: 'geradeaus', |
| 58 | clearingDistance: 30, |
| 59 | vehicleClass: 'strassenbahn', |
| 60 | vZul: 80, |
| 61 | ...extra, |
| 62 | }; |
| 63 | } |
| 64 | |
| 65 | function kfzEinfahrend(enteringDistance = 8): EnteringStream { |
| 66 | return { mode: 'kfz', movement: 'geradeaus', enteringDistance }; |
| 67 | } |
| 68 | |
| 69 | /** Kennwertsatz mit abgesenkter Raeumgeschwindigkeit der Fussgaenger. */ |
| 70 | function mitLangsamenFussgaengern(fuss: number): RilsaDefaults { |
| 71 | return { |
| 72 | ...RILSA_DEFAULTS, |
| 73 | clearingSpeed: { ...RILSA_DEFAULTS.clearingSpeed, fuss }, |
| 74 | }; |
| 75 | } |
| 76 | |
| 77 | describe('Befund 23 - erhoehter Zeitbedarf darf die Zwischenzeit nie verkuerzen', () => { |
| 78 | it('setzt fuer die Furt mit erhoehtem Zeitbedarf hoechstens die Raeumgeschwindigkeit des Regelfalls an', () => { |
| 79 | // Ohne Riegel raeumte der mobilitaetseingeschraenkte Fussgaenger mit |
| 80 | // 1,0 m/s, waehrend der Regelfall nur 0,8 m/s bekam. |
| 81 | const defaults = mitLangsamenFussgaengern(0.8); |
| 82 | expect(clearingSpeedFor({ mode: 'fuss', movement: 'geradeaus' }, defaults)).toBe(0.8); |
| 83 | expect( |
| 84 | clearingSpeedFor({ mode: 'fuss', movement: 'geradeaus', reducedMobility: true }, defaults), |
| 85 | ).toBe(0.8); |
| 86 | }); |
| 87 | |
| 88 | it('verkuerzt die Zwischenzeit der Furt mit erhoehtem Zeitbedarf nicht', () => { |
| 89 | const defaults = mitLangsamenFussgaengern(0.8); |
| 90 | const ohne = computeIntergreen(fussRaeumend(), kfzEinfahrend(6), defaults); |
| 91 | const mit = computeIntergreen( |
| 92 | fussRaeumend({ reducedMobility: true }), |
| 93 | kfzEinfahrend(6), |
| 94 | defaults, |
| 95 | ); |
| 96 | |
| 97 | // 12 m / 0,8 m/s = 15 s in beiden Faellen; zuvor 12 m / 1,0 m/s = 12 s mit |
| 98 | // dem Merkmal - drei Sekunden kuerzer fuer die schutzbeduerftigste |
| 99 | // Verkehrsart. |
| 100 | expect(ohne.clearingTime).toBe(15); |
| 101 | expect(mit.clearingTime).toBe(15); |
| 102 | expect(mit.intergreen).toBeGreaterThanOrEqual(ohne.intergreen); |
| 103 | }); |
| 104 | |
| 105 | it('meldet den Widerspruch am Kennwertsatz, statt ihn still zu kappen', () => { |
| 106 | const ergebnis = computeIntergreen( |
| 107 | fussRaeumend({ reducedMobility: true }), |
| 108 | kfzEinfahrend(6), |
| 109 | mitLangsamenFussgaengern(0.8), |
| 110 | ); |
| 111 | const notiz = ergebnis.notes.find( |
| 112 | (n) => n.code === 'erhoehter-zeitbedarf-schneller-als-regelfall', |
| 113 | ); |
| 114 | expect(notiz?.severity).toBe('fehler'); |
| 115 | expect(notiz?.message).toContain('erhöhtem Zeitbedarf'); |
| 116 | }); |
| 117 | |
| 118 | it('kappt die Vorgabe "erhoehter Zeitbedarf" auf den Fussgaengerwert und weist die Abweichung aus', () => { |
| 119 | const { defaults, notes, deviations } = resolveRilsaSettings({ clearingSpeedFuss: 0.8 }); |
| 120 | |
| 121 | expect(defaults.clearingSpeed.fuss).toBe(0.8); |
| 122 | expect(defaults.clearingSpeed.fussReducedMobility).toBe(0.8); |
| 123 | const notiz = notes.find( |
| 124 | (n) => n.code === 'vorgabe-ueberschritten:clearingSpeedFussReducedMobility', |
| 125 | ); |
| 126 | expect(notiz?.severity).toBe('fehler'); |
| 127 | const zeile = deviations.find((d) => d.key === 'clearingSpeedFussReducedMobility'); |
| 128 | expect(zeile?.appliedValue).toBe(0.8); |
| 129 | }); |
| 130 | |
| 131 | it('haelt die Obergrenze des Feldes an der Sicherheitsschranke aus constants.ts', () => { |
| 132 | const { defaults, notes } = resolveRilsaSettings({ |
| 133 | clearingSpeedFussReducedMobility: SAFETY_FLOORS.maxClearingSpeedFuss + 0.3, |
| 134 | }); |
| 135 | expect(defaults.clearingSpeed.fussReducedMobility).toBe(SAFETY_FLOORS.maxClearingSpeedFuss); |
| 136 | expect( |
| 137 | notes.some((n) => n.code === 'vorgabe-ueberschritten:clearingSpeedFussReducedMobility'), |
| 138 | ).toBe(true); |
| 139 | }); |
| 140 | |
| 141 | it('laesst abbiegende Kraftfahrzeuge nie schneller raeumen als geradeaus fahrende', () => { |
| 142 | const defaults: RilsaDefaults = { |
| 143 | ...RILSA_DEFAULTS, |
| 144 | clearingSpeed: { ...RILSA_DEFAULTS.clearingSpeed, kfzStraight: 6 }, |
| 145 | }; |
| 146 | expect(clearingSpeedFor({ mode: 'kfz', movement: 'rechts' }, defaults)).toBe(6); |
| 147 | |
| 148 | const { defaults: aufgeloest, notes } = resolveRilsaSettings({ clearingSpeedKfzStraight: 6 }); |
| 149 | expect(aufgeloest.clearingSpeed.kfzTurning).toBe(6); |
| 150 | expect( |
| 151 | notes.some( |
| 152 | (n) => |
| 153 | n.code === 'vorgabe-ueberschritten:clearingSpeedKfzTurning' && n.severity === 'fehler', |
| 154 | ), |
| 155 | ).toBe(true); |
| 156 | }); |
| 157 | }); |
| 158 | |
| 159 | describe('Befunde 28 und 29 - Handeingabe ueber dem Regelwert des Stroms', () => { |
| 160 | it('meldet eine Raeumgeschwindigkeit oberhalb des Regelwerts der Fahrbeziehung', () => { |
| 161 | // Regelwert des Abbiegers 7,0 m/s, Schranke der Verkehrsart 10,0 m/s. |
| 162 | // 9 m/s lag dazwischen und lief bislang ohne ein Wort durch: tz fiel von |
| 163 | // 9 s auf 8 s. |
| 164 | const ergebnis = computeIntergreen( |
| 165 | abbiegerRaeumend({ clearingSpeedOverride: 9 }), |
| 166 | kfzEinfahrend(), |
| 167 | ); |
| 168 | expect(ergebnis.clearingSpeed).toBe(9); |
| 169 | expect(ergebnis.intergreen).toBe(8); |
| 170 | |
| 171 | const notiz = ergebnis.notes.find((n) => n.code === 'raeumgeschwindigkeit-ueber-regelwert'); |
| 172 | expect(notiz?.severity).toBe('warnung'); |
| 173 | expect(notiz?.message).toContain('9'); |
| 174 | expect(notiz?.message).toContain('7'); |
| 175 | expect(notiz?.message).toContain('zu begründen'); |
| 176 | }); |
| 177 | |
| 178 | it('meldet eine Handeingabe, die den erhoehten Zeitbedarf aufhebt', () => { |
| 179 | // Regelwert der Furt mit erhoehtem Zeitbedarf 1,0 m/s, Schranke 1,2 m/s. |
| 180 | const ergebnis = computeIntergreen( |
| 181 | fussRaeumend({ reducedMobility: true, clearingSpeedOverride: 1.2 }), |
| 182 | kfzEinfahrend(6), |
| 183 | ); |
| 184 | expect(ergebnis.clearingSpeed).toBe(1.2); |
| 185 | const notiz = ergebnis.notes.find((n) => n.code === 'raeumgeschwindigkeit-ueber-regelwert'); |
| 186 | expect(notiz?.severity).toBe('warnung'); |
| 187 | expect(notiz?.message).toContain('1'); |
| 188 | }); |
| 189 | |
| 190 | it('meldet auch die gekappte Eingabe, wenn die Obergrenze ueber dem Regelwert liegt', () => { |
| 191 | const ergebnis = computeIntergreen( |
| 192 | abbiegerRaeumend({ clearingSpeedOverride: 25 }), |
| 193 | kfzEinfahrend(), |
| 194 | ); |
| 195 | expect(ergebnis.clearingSpeed).toBe(SAFETY_FLOORS.maxClearingSpeedKfz); |
| 196 | expect(ergebnis.notes.some((n) => n.code === 'raeumgeschwindigkeit-zu-hoch')).toBe(true); |
| 197 | expect(ergebnis.notes.some((n) => n.code === 'raeumgeschwindigkeit-ueber-regelwert')).toBe( |
| 198 | true, |
| 199 | ); |
| 200 | }); |
| 201 | |
| 202 | it('schweigt bei einer Eingabe unterhalb des Regelwerts', () => { |
| 203 | const ergebnis = computeIntergreen( |
| 204 | abbiegerRaeumend({ clearingSpeedOverride: 5 }), |
| 205 | kfzEinfahrend(), |
| 206 | ); |
| 207 | expect(ergebnis.notes.some((n) => n.code === 'raeumgeschwindigkeit-ueber-regelwert')).toBe( |
| 208 | false, |
| 209 | ); |
| 210 | }); |
| 211 | |
| 212 | it('nennt bei gesetztem engem Innenradius den Regelwert des Stroms, nicht den Praxisansatz', () => { |
| 213 | /* |
| 214 | * Der Praxisansatz "enger Innenradius" (5,0 m/s) ist ausdruecklich kein |
| 215 | * Regelwerkswert - quellen.ts fuehrt ihn unter 'praxis-enger-radius' mit |
| 216 | * dem Pruefstand "abzugleichen". Die Meldung nannte ihn trotzdem |
| 217 | * "Regelwert 5 m/s dieses Stroms", waehrend das Feld im selben Fenster |
| 218 | * korrekt "ohne diesen Eintrag 5 m/s" sagt. Der wirkliche Regelwert des |
| 219 | * abbiegenden Kfz-Stroms (7,0 m/s) kam in der Meldung nicht vor, obwohl |
| 220 | * sie die begruendungspflichtige Abweichung ausweisen soll. |
| 221 | */ |
| 222 | const ergebnis = computeIntergreen( |
| 223 | abbiegerRaeumend({ engerRadius: true, clearingSpeedOverride: 9 }), |
| 224 | kfzEinfahrend(), |
| 225 | ); |
| 226 | const notiz = ergebnis.notes.find((n) => n.code === 'raeumgeschwindigkeit-ueber-regelwert'); |
| 227 | expect(notiz?.severity).toBe('warnung'); |
| 228 | expect(notiz?.message).toContain('Regelwert 7 m/s'); |
| 229 | expect(notiz?.message).not.toContain('Regelwert 5 m/s'); |
| 230 | }); |
| 231 | |
| 232 | it('schweigt bei engem Innenradius, solange die Handeingabe unter dem Regelwert bleibt', () => { |
| 233 | /* |
| 234 | * 6 m/s liegen unter dem Regelwert des Abbiegers (7,0 m/s) und damit auf |
| 235 | * der sicheren Seite - kein Regelwertverstoss. Dass die Handeingabe das |
| 236 | * Merkmal aushebelt, meldet 'enger-radius-durch-vorgabe-ersetzt' samt |
| 237 | * Richtung; zwei Warnungen zu einer Tatsache stuenden sonst |
| 238 | * nebeneinander in Pruefbericht und Planunterlage. |
| 239 | */ |
| 240 | const ergebnis = computeIntergreen( |
| 241 | abbiegerRaeumend({ engerRadius: true, clearingSpeedOverride: 6 }), |
| 242 | kfzEinfahrend(), |
| 243 | ); |
| 244 | expect(ergebnis.notes.some((n) => n.code === 'raeumgeschwindigkeit-ueber-regelwert')).toBe( |
| 245 | false, |
| 246 | ); |
| 247 | const ersetzt = ergebnis.notes.find((n) => n.code === 'enger-radius-durch-vorgabe-ersetzt'); |
| 248 | expect(ersetzt?.message).toContain('6 m/s'); |
| 249 | expect(ersetzt?.message).toContain('5 m/s'); |
| 250 | }); |
| 251 | |
| 252 | it('schweigt, wo Regelwert und Schranke zusammenfallen', () => { |
| 253 | const ergebnis = computeIntergreen( |
| 254 | abbiegerRaeumend({ movement: 'geradeaus', clearingSpeedOverride: 10 }), |
| 255 | kfzEinfahrend(), |
| 256 | ); |
| 257 | expect(ergebnis.notes.some((n) => n.code === 'raeumgeschwindigkeit-ueber-regelwert')).toBe( |
| 258 | false, |
| 259 | ); |
| 260 | }); |
| 261 | }); |
| 262 | |
| 263 | /* |
| 264 | * NACHGEZOGEN (Fassung 5.43.0): `ueberfahrzeitHerkunft` nimmt das |
| 265 | * VERFAHREN als erstes Argument, weil an einer Engstelle nicht der Kfz-Ansatz |
| 266 | * gilt, sondern die feste Ueberfahrzeit des Abschnitts 5.2.2. Diese drei Faelle |
| 267 | * fragen nach dem Knotenpunkt und geben es deshalb als 'knotenpunkt' mit; ihre |
| 268 | * Aussage ist unveraendert. Den neuen Zweig prueft |
| 269 | * tests/domain/bezugswerteVerfahren.test.ts. |
| 270 | */ |
| 271 | describe('Befund 58 - Herkunft der Ueberfahrzeit am angesetzten Wert', () => { |
| 272 | it('nennt eine uebernommene Handeingabe weiterhin als solche', () => { |
| 273 | expect( |
| 274 | ueberfahrzeitHerkunft('knotenpunkt', 'kfz', 'gelbzeit', { |
| 275 | vorgegeben: true, |
| 276 | vorgabewert: 4, |
| 277 | angesetzt: 4, |
| 278 | }), |
| 279 | ).toBe('an dieser Beziehung von Hand eingetragen'); |
| 280 | }); |
| 281 | |
| 282 | it('weist eine verworfene Handeingabe nicht mehr als angesetzten Wert aus', () => { |
| 283 | // Eingetragen waren 0 s, gerechnet wurde mit der Gelbzeit von 3 s. |
| 284 | const text = ueberfahrzeitHerkunft('knotenpunkt', 'kfz', 'gelbzeit', { |
| 285 | vorgegeben: true, |
| 286 | vorgabewert: 0, |
| 287 | angesetzt: 3, |
| 288 | }); |
| 289 | expect(text).toContain('verworfen'); |
| 290 | expect(text).not.toBe('an dieser Beziehung von Hand eingetragen'); |
| 291 | }); |
| 292 | |
| 293 | it('nennt im verworfenen Fall die Herleitung des angesetzten Werts', () => { |
| 294 | const text = ueberfahrzeitHerkunft('knotenpunkt', 'fuss', 'gelbzeit', { |
| 295 | vorgegeben: true, |
| 296 | vorgabewert: -1, |
| 297 | angesetzt: 1, |
| 298 | }); |
| 299 | expect(text).toContain('Fußgänger'); |
| 300 | expect(text).toContain('verworfen'); |
| 301 | }); |
| 302 | }); |
| 303 | |
| 304 | describe('Befund 59 - OePNV ueber 70 km/h im Haltefall', () => { |
| 305 | it('nennt im Haltefall den Anfahransatz und die Ueberfahrzeit 0 s', () => { |
| 306 | const ergebnis = computeIntergreen(bahnRaeumend({ haltVorKnoten: true }), kfzEinfahrend(10)); |
| 307 | expect(ergebnis.crossingTime).toBe(0); |
| 308 | const notiz = ergebnis.notes.find((n) => n.code === 'oepnv-vmax-ueber-70'); |
| 309 | expect(notiz?.message).not.toContain('Staffelstufe'); |
| 310 | expect(notiz?.message).toContain('Anfahransatz'); |
| 311 | expect(notiz?.message).toContain('0 s'); |
| 312 | }); |
| 313 | |
| 314 | it('nennt ohne Halt weiterhin die oberste Staffelstufe', () => { |
| 315 | const ergebnis = computeIntergreen(bahnRaeumend(), kfzEinfahrend(10)); |
| 316 | expect(ergebnis.crossingTime).toBe(7); |
| 317 | const notiz = ergebnis.notes.find((n) => n.code === 'oepnv-vmax-ueber-70'); |
| 318 | expect(notiz?.message).toContain('obersten Staffelstufe'); |
| 319 | }); |
| 320 | }); |