lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests domain wirkungsloseVorgaben.test.ts
| 1 | import { describe, expect, it } from 'vitest'; |
| 2 | import { buildSignalPlan } from '@/domain/plan/signalPlan'; |
| 3 | import { createStandardIntersectionProject } from '@/domain/model/factory'; |
| 4 | import { engstellenFolgen, umlaufBeiFreigabe } from '@/domain/rilsa/engstelle'; |
| 5 | import { RILSA_DEFAULTS } from '@/domain/rilsa/constants'; |
| 6 | import { QUELLEN, type QuellenSchluessel } from '@/domain/rilsa/quellen'; |
| 7 | import { resolveRilsaSettings, settingFields, wirkungslosigkeit } from '@/domain/rilsa/settings'; |
| 8 | import { wirkungsvermerk } from '@/services/export/bewertung'; |
| 9 | import { validateProject } from '@/domain/validation'; |
| 10 | import type { Project } from '@/domain/model/project'; |
| 11 | |
| 12 | /** |
| 13 | * Kennwerte, die einstellbar sind und in keine Zahl eingehen (Befund |
| 14 | * C20) - und die Kennwerte der Engstellen-Vorschau, die in |
| 15 | * eine eingehen sollen (Befund D1). |
| 16 | * |
| 17 | * C20: Die drei Progressionsgeschwindigkeiten der Koordinierung standen in der |
| 18 | * Vorgabenverwaltung unter dem Kopfsatz "Diese Kennwerte gehen unmittelbar in |
| 19 | * jede Berechnung ein", und der Pruefbericht verlangte fuer eine Abweichung |
| 20 | * eine fachliche Begruendung. coordination.ts wird von keiner Ansicht und |
| 21 | * keiner Rechnung aufgerufen; quellen.ts sagt das seit jeher. Eine Begruendung |
| 22 | * fuer eine Zahl zu verlangen, die in kein Ergebnis eingeht, ist eine leere |
| 23 | * Forderung - und sie lenkt von den Abweichungen ab, die wirklich zu |
| 24 | * begruenden sind. |
| 25 | * |
| 26 | * D1: engstellenFolgen rechnete fest mit RILSA_DEFAULTS. Wer die |
| 27 | * Raeumgeschwindigkeit angepasst hatte, bekam in Assistent und Ausdruck eine |
| 28 | * Zwischenzeit, die im Plan daneben nicht vorkommt - und im Ausdruck ist genau |
| 29 | * diese Zahl die Begruendung der Umlaufzeitobergrenze. |
| 30 | * |
| 31 | * Gegen den Altstand schlagen diese Faelle fehl: `wirkungslosigkeit` gab es |
| 32 | * nicht, der Pruefbericht kannte als wirkungslosen Kennwert allein den |
| 33 | * Instationaritaetsfaktor, und engstellenFolgen nahm keinen dritten Parameter |
| 34 | * entgegen (die Vorschau blieb bei jeder Vorgabe dieselbe). |
| 35 | */ |
| 36 | |
| 37 | const FESTES_DATUM = new Date('2026-01-01T00:00:00Z'); |
| 38 | |
| 39 | /** Alle Herkuenfte, die an einem Vorgabenfeld haengen. */ |
| 40 | const HERKUENFTE = [...new Set(settingFields().map((f) => f.quellenSchluessel))]; |
| 41 | |
| 42 | /* |
| 43 | * EIN Grundprojekt fuer alle Faelle: createStandardIntersectionProject vergibt |
| 44 | * bei jedem Aufruf neue Kennungen. Zwei getrennt erzeugte Projekte |
| 45 | * unterscheiden sich in jeder Signalgruppen- und Phasenkennung - der Vergleich |
| 46 | * zweier Plaene weiter unten faende dann einen Unterschied, der mit der |
| 47 | * Vorgabe nichts zu tun hat. |
| 48 | */ |
| 49 | const GRUNDPROJEKT = createStandardIntersectionProject('Wirkung', FESTES_DATUM); |
| 50 | |
| 51 | function projektMitVorgabe( |
| 52 | vorgaben: Record<string, number>, |
| 53 | verfahren: 'HBS' | 'HCM' = 'HBS', |
| 54 | ): Project { |
| 55 | return { |
| 56 | ...GRUNDPROJEKT, |
| 57 | settings: { ...GRUNDPROJEKT.settings, serviceLevelScale: verfahren, rilsa: vorgaben }, |
| 58 | }; |
| 59 | } |
| 60 | |
| 61 | function befundZu(project: Project, key: string) { |
| 62 | const bericht = validateProject(project, buildSignalPlan(project), FESTES_DATUM); |
| 63 | return bericht.findings.find((f) => f.rule === `vorgaben.abweichung.${key}`); |
| 64 | } |
| 65 | |
| 66 | describe('C20 - Auskunft des Fachkerns: welcher Kennwert wirkt nicht', () => { |
| 67 | it('nennt die Koordinierung wirkungslos, solange kein Straßenzug erfasst ist', () => { |
| 68 | /* |
| 69 | * SEIT 5.35.0 HAENGT DER VERMERK AM PROJEKT, NICHT AN DER FASSUNG. Bis |
| 70 | * dahin lautete er "ohne Wirkung in dieser Fassung", und das war richtig: |
| 71 | * Die Koordinierung wurde ueberhaupt nicht gerechnet. Seit es die Ansicht |
| 72 | * "Koordinierung" gibt, waere derselbe Satz eine falsche Auskunft ueber |
| 73 | * eine wirkende Zahl - genau der Fehler, den Befund C20 in der |
| 74 | * Gegenrichtung geruegt hat. |
| 75 | */ |
| 76 | for (const verfahren of ['HBS', 'HCM'] as const) { |
| 77 | const auskunft = wirkungslosigkeit('rilsa-koordinierung', verfahren, { |
| 78 | koordinierungErfasst: false, |
| 79 | }); |
| 80 | expect(auskunft, `Koordinierung bei ${verfahren}`).not.toBeNull(); |
| 81 | expect(auskunft!.vermerk).toBe('ohne Wirkung: keine Koordinierung erfasst'); |
| 82 | expect(auskunft!.begruendung).toContain('kein Straßenzug erfasst'); |
| 83 | // Der Satz, der an die Stelle der Begruendungspflicht tritt. |
| 84 | expect(auskunft!.zutun).toContain('Setzen Sie den Wert zurück'); |
| 85 | expect(auskunft!.zutun).not.toContain('zu begründen'); |
| 86 | } |
| 87 | }); |
| 88 | |
| 89 | it('erklärt die Koordinierung für wirksam, sobald ein Straßenzug erfasst ist', () => { |
| 90 | // Die Gegenprobe zum Fall darueber, und der eigentliche Gewinn der Koordinierungskette: |
| 91 | // Mit erfasstem Strassenzug gehen die Kennwerte in Reisezeit, Versatz und |
| 92 | // Bandbreite ein - der Vermerk muss dann verschwinden. |
| 93 | for (const verfahren of ['HBS', 'HCM'] as const) { |
| 94 | expect( |
| 95 | wirkungslosigkeit('rilsa-koordinierung', verfahren, { koordinierungErfasst: true }), |
| 96 | `Koordinierung bei ${verfahren}`, |
| 97 | ).toBeNull(); |
| 98 | } |
| 99 | }); |
| 100 | |
| 101 | it('fällt ohne Angabe auf "der Wert wirkt" zurück', () => { |
| 102 | // Die Grundregel des Kontexts: Wer die Lage nicht kennt, bekommt eine |
| 103 | // Begruendungspflicht zu viel statt einer Wirkungsbehauptung zu wenig. |
| 104 | expect(wirkungslosigkeit('rilsa-koordinierung', 'HBS')).toBeNull(); |
| 105 | }); |
| 106 | |
| 107 | it('bleibt beim Instationaritaetsfaktor bei der Auskunft aus P3: nur ohne HBS', () => { |
| 108 | expect(wirkungslosigkeit('hbs-wartezeit', 'HBS')).toBeNull(); |
| 109 | const auskunft = wirkungslosigkeit('hbs-wartezeit', 'HCM'); |
| 110 | expect(auskunft).not.toBeNull(); |
| 111 | expect(auskunft!.vermerk).toBe('ohne Wirkung: Bewertungsverfahren HCM'); |
| 112 | }); |
| 113 | |
| 114 | it('erklaert jeden anderen Kennwert fuer wirksam - auch die, die kein Regelwerkswert sind', () => { |
| 115 | // Die Koordinierung steht NICHT mehr in dieser Liste: Ohne Kontext gilt |
| 116 | // sie als wirksam, wie jeder andere Kennwert auch. |
| 117 | const wirkungslos: QuellenSchluessel[] = ['hbs-wartezeit']; |
| 118 | for (const schluessel of HERKUENFTE) { |
| 119 | if (wirkungslos.includes(schluessel)) continue; |
| 120 | for (const verfahren of ['HBS', 'HCM'] as const) { |
| 121 | expect( |
| 122 | wirkungslosigkeit(schluessel, verfahren), |
| 123 | `${schluessel} (${verfahren}) darf nicht als wirkungslos gelten`, |
| 124 | ).toBeNull(); |
| 125 | } |
| 126 | } |
| 127 | }); |
| 128 | |
| 129 | it('haengt die Koordinierung wirklich an der Quelle, die ihre Wirkungslosigkeit schon sagt', () => { |
| 130 | const felder = settingFields().filter((f) => f.quellenSchluessel === 'rilsa-koordinierung'); |
| 131 | expect(felder.map((f) => f.key)).toEqual([ |
| 132 | 'coordinationMinSpeed', |
| 133 | 'coordinationMaxSpeed', |
| 134 | 'coordinationDefaultSpeed', |
| 135 | ]); |
| 136 | // Vorbedingung des Befundes: Das Fundstellenverzeichnis sagt es laengst, |
| 137 | // die Vorgabenansicht und der Pruefbericht sagten das Gegenteil. |
| 138 | // Das Fundstellenverzeichnis sagt seit 5.35.0 BEIDES: wo die Kennwerte |
| 139 | // wirken und wo nicht. Eine Verwendung zu verschweigen, die es gibt, laesst |
| 140 | // den Pruefer eine wirkende Zahl fuer folgenlos halten. |
| 141 | expect(QUELLEN['rilsa-koordinierung'].verwendung).toContain( |
| 142 | 'Sie wirken in der Ansicht „Koordinierung"', |
| 143 | ); |
| 144 | expect(QUELLEN['rilsa-koordinierung'].verwendung).toContain( |
| 145 | 'in den Signalzeitenplan dieses Knotenpunkts gehen sie nicht ein', |
| 146 | ); |
| 147 | }); |
| 148 | }); |
| 149 | |
| 150 | describe('C20 - Beleg: eine geaenderte Progressionsgeschwindigkeit aendert kein Ergebnis', () => { |
| 151 | /** |
| 152 | * Der Nachweis, dass der Vermerk nicht selbst eine Behauptung ist: Zwei |
| 153 | * Plaene, die sich allein in der Progressionsgeschwindigkeit unterscheiden, |
| 154 | * ergeben dieselben Zahlen. Verglichen wird alles AUSSER `defaults` - der |
| 155 | * Vorgabensatz ist die Eingabe, nicht das Ergebnis, und in ihm steht der |
| 156 | * geaenderte Wert selbstverstaendlich drin. |
| 157 | */ |
| 158 | it('liefert denselben Signalzeitenplan wie ohne Vorgabe', () => { |
| 159 | const ohne = buildSignalPlan(projektMitVorgabe({})); |
| 160 | const mit = buildSignalPlan(projektMitVorgabe({ coordinationDefaultSpeed: 70 })); |
| 161 | |
| 162 | const ergebnis = (plan: ReturnType<typeof buildSignalPlan>): string => |
| 163 | JSON.stringify({ ...plan, defaults: null }); |
| 164 | expect(ergebnis(mit)).toBe(ergebnis(ohne)); |
| 165 | |
| 166 | // Und der Wert ist wirklich angekommen - sonst waere die Gleichheit oben |
| 167 | // nur der Beweis, dass die Vorgabe verworfen wurde. |
| 168 | expect(mit.defaults.coordination.defaultSpeed).toBe(70); |
| 169 | expect(ohne.defaults.coordination.defaultSpeed).toBe(RILSA_DEFAULTS.coordination.defaultSpeed); |
| 170 | }); |
| 171 | }); |
| 172 | |
| 173 | describe('C20 - Pruefbericht verlangt keine Begruendung fuer eine wirkungslose Abweichung', () => { |
| 174 | it('meldet die Abweichung, nennt aber den Grund der Wirkungslosigkeit statt der Begruendungspflicht', () => { |
| 175 | const befund = befundZu( |
| 176 | projektMitVorgabe({ coordinationDefaultSpeed: 70 }), |
| 177 | 'coordinationDefaultSpeed', |
| 178 | ); |
| 179 | expect(befund, 'Die Abweichung wird nicht verschwiegen').toBeDefined(); |
| 180 | expect(befund!.title).toContain('Progressionsgeschwindigkeit'); |
| 181 | expect(befund!.message).toContain('Angesetzt: 70 km/h'); |
| 182 | expect(befund!.message).toContain('Koordinierung'); |
| 183 | expect(befund!.message).toContain('kein Straßenzug erfasst'); |
| 184 | // Der Kern des Befundes: kein "zu begruenden" mehr. |
| 185 | expect(befund!.suggestion).not.toContain('zu begründen'); |
| 186 | expect(befund!.suggestion).toContain('Setzen Sie den Wert zurück'); |
| 187 | // Die Fundstelle bleibt die Quelle des Kennwerts. |
| 188 | expect(befund!.reference).toContain('Koordinierung benachbarter Lichtsignalanlagen'); |
| 189 | }); |
| 190 | |
| 191 | it('verlangt fuer eine WIRKSAME Abweichung weiterhin die fachliche Begruendung', () => { |
| 192 | const befund = befundZu( |
| 193 | projektMitVorgabe({ clearingSpeedKfzStraight: 8 }), |
| 194 | 'clearingSpeedKfzStraight', |
| 195 | ); |
| 196 | expect(befund).toBeDefined(); |
| 197 | expect(befund!.suggestion).toBe('Die Abweichung ist in den Planunterlagen zu begründen.'); |
| 198 | expect(befund!.message).not.toContain('ohne Wirkung'); |
| 199 | }); |
| 200 | |
| 201 | it('behandelt den Instationaritaetsfaktor unveraendert wie seit P3', () => { |
| 202 | const hcm = befundZu( |
| 203 | projektMitVorgabe({ instationaritaetsfaktor: 1.3 }, 'HCM'), |
| 204 | 'instationaritaetsfaktor', |
| 205 | ); |
| 206 | expect(hcm!.suggestion).toContain('Bewertungsverfahren'); |
| 207 | expect(hcm!.suggestion).not.toContain('zu begründen'); |
| 208 | const hbs = befundZu( |
| 209 | projektMitVorgabe({ instationaritaetsfaktor: 1.3 }, 'HBS'), |
| 210 | 'instationaritaetsfaktor', |
| 211 | ); |
| 212 | expect(hbs!.suggestion).toBe('Die Abweichung ist in den Planunterlagen zu begründen.'); |
| 213 | }); |
| 214 | |
| 215 | it('nimmt die Abweichung NICHT aus der Liste heraus - sie bleibt sichtbar', () => { |
| 216 | const { deviations } = resolveRilsaSettings( |
| 217 | { coordinationDefaultSpeed: 70 }, |
| 218 | undefined, |
| 219 | 'knotenpunkt', |
| 220 | ); |
| 221 | expect(deviations.map((d) => d.key)).toContain('coordinationDefaultSpeed'); |
| 222 | }); |
| 223 | }); |
| 224 | |
| 225 | describe('C20 - Vermerk im Fundstellenverzeichnis', () => { |
| 226 | it('haengt den Vermerk an die Koordinierung, solange kein Strassenzug erfasst ist', () => { |
| 227 | for (const verfahren of ['HBS', 'HCM'] as const) { |
| 228 | expect( |
| 229 | wirkungsvermerk('rilsa-koordinierung', verfahren, { koordinierungErfasst: false }), |
| 230 | ).toBe(' (ohne Wirkung: keine Koordinierung erfasst)'); |
| 231 | expect( |
| 232 | wirkungsvermerk('rilsa-koordinierung', verfahren, { koordinierungErfasst: true }), |
| 233 | ).toBe(''); |
| 234 | } |
| 235 | }); |
| 236 | |
| 237 | it('laesst den Vermerk fuer wirksame Herkuenfte leer', () => { |
| 238 | expect(wirkungsvermerk('rilsa-zwischenzeiten', 'HBS')).toBe(''); |
| 239 | expect(wirkungsvermerk('hbs-wartezeit', 'HBS')).toBe(''); |
| 240 | expect(wirkungsvermerk('hbs-wartezeit', 'HCM')).toBe( |
| 241 | ' (ohne Wirkung: Bewertungsverfahren HCM)', |
| 242 | ); |
| 243 | }); |
| 244 | }); |
| 245 | |
| 246 | describe('D1 - Engstellen-Vorschau rechnet mit den uebergebenen Kennwerten', () => { |
| 247 | /** Vorgabensatz mit einer geaenderten Raeumgeschwindigkeit Kfz geradeaus. */ |
| 248 | const mitRaeumgeschwindigkeit = (wert: number) => |
| 249 | resolveRilsaSettings({ clearingSpeedKfzStraight: wert }, undefined, 'einstreifig').defaults; |
| 250 | |
| 251 | it('verlaengert die Zwischenzeit, wenn langsamer geraeumt wird', () => { |
| 252 | /* |
| 253 | * NACHGEZOGEN MIT 5.27.0: Die Engstelle rechnet nach RiLSA 2015, |
| 254 | * Abschnitt 5.2.2, und dort steht die Raeumgeschwindigkeit im Regelwerk - |
| 255 | * 40 km/h bei zulaessig 50 km/h. Der Vorgabenwert der Knotenpunktrechnung |
| 256 | * (10,0 m/s) wirkt dort nicht mehr; eine ABWEICHENDE Vorgabe wirkt weiter, |
| 257 | * aber nur nach unten. Das ist die Aussage, die D1 verlangt: Was der |
| 258 | * Bearbeiter selbst gesetzt hat, muss sich in der Vorschau wiederfinden. |
| 259 | */ |
| 260 | const laenge = 400; |
| 261 | // Regelwert: 4 + 400/40 * 3,6 = 40 s (bis 5.26.0: 3 + 406/10 = 44 s). |
| 262 | expect(engstellenFolgen(laenge, 50).zwischenzeit).toBe(40); |
| 263 | // Abweichende Vorgabe 8 m/s - langsamer als die 11,11 m/s der Staffel: |
| 264 | // 4 + 400/8 = 54 s. |
| 265 | const langsam = mitRaeumgeschwindigkeit(8); |
| 266 | expect(langsam.clearingSpeed.kfzStraight, 'Vorbedingung: die Vorgabe gilt').toBe(8); |
| 267 | expect(engstellenFolgen(laenge, 50, langsam).zwischenzeit).toBe(54); |
| 268 | }); |
| 269 | |
| 270 | it('laesst eine schnellere Vorgabe an der Engstelle nicht durch', () => { |
| 271 | // 12 m/s liegen ueber den 11,11 m/s der Staffel; sie wuerden die |
| 272 | // Zwischenzeit gegenueber dem Regelwerk verkuerzen und werden verworfen. |
| 273 | const schnell = mitRaeumgeschwindigkeit(12); |
| 274 | expect(engstellenFolgen(400, 50, schnell).zwischenzeit).toBe(40); |
| 275 | }); |
| 276 | |
| 277 | it('schlaegt bis in Umlauf und Wartezeit durch', () => { |
| 278 | const langsam = mitRaeumgeschwindigkeit(8); |
| 279 | const regel = engstellenFolgen(400, 50); |
| 280 | const vorgabe = engstellenFolgen(400, 50, langsam); |
| 281 | // Umlauf = 2 x Zwischenzeit + 2 x Mindestfreigabe (5 s). |
| 282 | expect(regel.umlaufzeit).toBe(2 * 40 + 2 * RILSA_DEFAULTS.minGreen.kfz); |
| 283 | expect(vorgabe.umlaufzeit).toBe(2 * 54 + 2 * RILSA_DEFAULTS.minGreen.kfz); |
| 284 | // Wartezeit = Umlauf abzueglich der eigenen Freigabezeit. |
| 285 | expect(vorgabe.wartezeit).toBe(2 * 54 + RILSA_DEFAULTS.minGreen.kfz); |
| 286 | expect(vorgabe.wartezeit).toBeGreaterThan(regel.wartezeit); |
| 287 | }); |
| 288 | |
| 289 | it('rechnet auch den Umlauf bei brauchbarer Freigabezeit mit den Vorgaben', () => { |
| 290 | const langsam = mitRaeumgeschwindigkeit(8); |
| 291 | expect(umlaufBeiFreigabe(400, 50, 20)).toBe(2 * 40 + 2 * 20); |
| 292 | expect(umlaufBeiFreigabe(400, 50, 20, langsam)).toBe(2 * 54 + 2 * 20); |
| 293 | }); |
| 294 | |
| 295 | it('bleibt ohne Angabe bei den Regelwerten - alte Aufrufe rechnen unveraendert', () => { |
| 296 | expect(engstellenFolgen(400, 50, RILSA_DEFAULTS).zwischenzeit).toBe( |
| 297 | engstellenFolgen(400, 50).zwischenzeit, |
| 298 | ); |
| 299 | expect(umlaufBeiFreigabe(400, 50, 20, RILSA_DEFAULTS)).toBe(umlaufBeiFreigabe(400, 50, 20)); |
| 300 | }); |
| 301 | |
| 302 | it('hebt die Stufe an, wenn die Vorgabe den Umlauf ueber die Schranke der Anlagenart treibt', () => { |
| 303 | // 1000 m mit dem Regelwert: 94 s Zwischenzeit, 193 s Wartezeit - ueber der |
| 304 | // Warnschwelle von 180 s, unter der Beanstandungsschwelle von 300 s. Bis |
| 305 | // 5.26.0 genuegten dafuer 900 m; das Verfahren des Abschnitts 5.2 raeumt |
| 306 | // bei zulaessig 50 km/h schneller. |
| 307 | expect(engstellenFolgen(1000, 50).zwischenzeit).toBe(94); |
| 308 | expect(engstellenFolgen(1000, 50).stufe).toBe('warnung'); |
| 309 | // Mit 3 m/s: 304 s Zwischenzeit, 618 s Umlauf - ueber den 300 s, die bei |
| 310 | // einstreifiger Fuehrung zulaessig sind. |
| 311 | expect(engstellenFolgen(900, 50, mitRaeumgeschwindigkeit(3)).stufe).toBe('fehler'); |
| 312 | }); |
| 313 | |
| 314 | it('nimmt die Schwellen aus der Anlagenart, nicht aus dem Vorgabensatz', () => { |
| 315 | /* |
| 316 | * Wartezeitwarnung, Wartezeitfehler und die 300 s Umlauf sind Schwellen |
| 317 | * dieses Programms (ANLAGENART_GRENZEN, Befund B8) und keine anpassbaren |
| 318 | * Vorgaben. Eine kleinere "Groesste Umlaufzeit" darf die Vorschau deshalb |
| 319 | * nicht umstufen - sonst liesse sich die Warnung, die vom Verzicht auf die |
| 320 | * Ampel abraet, ueber die Vorgabenverwaltung verstellen. |
| 321 | */ |
| 322 | const kleinererUmlauf = resolveRilsaSettings( |
| 323 | { cycleTimeMax: 150 }, |
| 324 | undefined, |
| 325 | 'einstreifig', |
| 326 | ).defaults; |
| 327 | expect(kleinererUmlauf.cycleTime.max, 'Vorbedingung: die Vorgabe gilt').toBe(150); |
| 328 | const folgen = engstellenFolgen(1000, 50, kleinererUmlauf); |
| 329 | expect(folgen.umlaufzeit).toBe(198); |
| 330 | expect(folgen.stufe).toBe('warnung'); |
| 331 | }); |
| 332 | }); |