lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests domain zwischenzeitSchranken.test.ts
| 1 | import { describe, expect, it } from 'vitest'; |
| 2 | import { validateProject } from '@/domain/validation/engine'; |
| 3 | import { buildSignalPlan, intergreenKey } from '@/domain/plan/signalPlan'; |
| 4 | import { createStandardIntersectionProject } from '@/domain/model/factory'; |
| 5 | import type { Conflict, Project } from '@/domain/model/project'; |
| 6 | |
| 7 | const STICHTAG = new Date('2026-01-01T00:00:00Z'); |
| 8 | |
| 9 | /** Regelknotenpunkt, in dem genau eine Kfz-Beziehung abgewandelt ist. */ |
| 10 | function mitKonflikt(aenderung: Partial<Conflict>): { projekt: Project; konflikt: Conflict } { |
| 11 | const roh = createStandardIntersectionProject('Schranken', STICHTAG); |
| 12 | const [k1, , k3] = roh.signalGroups; |
| 13 | const ziel = roh.conflicts.find((c) => c.fromId === k1!.id && c.toId === k3!.id)!; |
| 14 | const geaendert = { ...ziel, ...aenderung }; |
| 15 | return { |
| 16 | konflikt: geaendert, |
| 17 | projekt: { |
| 18 | ...roh, |
| 19 | conflicts: roh.conflicts.map((c) => (c.id === ziel.id ? geaendert : c)), |
| 20 | meta: { |
| 21 | ...roh.meta, |
| 22 | projectNumber: 'VP-2026-001', |
| 23 | client: 'Stadt Musterhausen', |
| 24 | planner: 'O. Willerding', |
| 25 | location: 'Schranken', |
| 26 | }, |
| 27 | }, |
| 28 | }; |
| 29 | } |
| 30 | |
| 31 | describe('Schranken der Zwischenzeit lassen sich nicht ueber ein Nachbarfeld umgehen', () => { |
| 32 | /* |
| 33 | * Die vorgegebene Zwischenzeit ersetzt die Rechnung vollstaendig - |
| 34 | * computeIntergreen wurde gar nicht erst gerufen. Damit liefen alle Schranken |
| 35 | * ins Leere, die fuer die Einzelwerte gelten. In der Zwischenzeitenansicht |
| 36 | * stehen beide Felder nebeneinander: Wer kuerzen wollte, nahm das Feld ohne |
| 37 | * Schranke. |
| 38 | */ |
| 39 | it('meldet eine vorgegebene Zwischenzeit unter dem Rechenwert', () => { |
| 40 | const { projekt: ohne, konflikt: ungeaendert } = mitKonflikt({}); |
| 41 | const gerechnet = |
| 42 | buildSignalPlan(ohne).intergreens.get(intergreenKey(ungeaendert.fromId, ungeaendert.toId)) |
| 43 | ?.value ?? 0; |
| 44 | expect(gerechnet).toBeGreaterThan(0); |
| 45 | |
| 46 | const { projekt, konflikt } = mitKonflikt({ manualIntergreen: gerechnet - 3 }); |
| 47 | const plan = buildSignalPlan(projekt); |
| 48 | |
| 49 | expect(plan.intergreens.get(intergreenKey(konflikt.fromId, konflikt.toId))?.value).toBe( |
| 50 | gerechnet - 3, |
| 51 | ); |
| 52 | const meldung = plan.notes.find((n) => n.code === 'zwischenzeit-vorgabe-unter-rechenwert'); |
| 53 | expect(meldung, 'Die Unterschreitung darf nicht stillschweigend bleiben').toBeDefined(); |
| 54 | expect(meldung!.severity).toBe('warnung'); |
| 55 | // Beide Zahlen muessen im Text stehen, sonst ist die Abweichung nicht zu |
| 56 | // beziffern. |
| 57 | expect(meldung!.message).toContain(`${gerechnet - 3} s`); |
| 58 | expect(meldung!.message).toContain(`${gerechnet} s`); |
| 59 | }); |
| 60 | |
| 61 | it('schweigt, wenn die Vorgabe den Rechenwert erreicht oder ueberschreitet', () => { |
| 62 | const { projekt: ohne, konflikt: ungeaendert } = mitKonflikt({}); |
| 63 | const gerechnet = |
| 64 | buildSignalPlan(ohne).intergreens.get(intergreenKey(ungeaendert.fromId, ungeaendert.toId)) |
| 65 | ?.value ?? 0; |
| 66 | |
| 67 | for (const vorgabe of [gerechnet, gerechnet + 2]) { |
| 68 | const { projekt } = mitKonflikt({ manualIntergreen: vorgabe }); |
| 69 | expect( |
| 70 | buildSignalPlan(projekt).notes.some( |
| 71 | (n) => n.code === 'zwischenzeit-vorgabe-unter-rechenwert', |
| 72 | ), |
| 73 | `Vorgabe ${vorgabe}`, |
| 74 | ).toBe(false); |
| 75 | } |
| 76 | }); |
| 77 | |
| 78 | it('vergleicht nicht, solange kein Raeumweg erfasst ist - meldet den Fall aber', () => { |
| 79 | /* |
| 80 | * BERICHTIGUNG (Befund 19, Fassung 5.10.0): Hier stand |
| 81 | * "Die fehlende Vermassung meldet bereits konflikte.raeumweg-fehlt". Das |
| 82 | * war nachweislich falsch - jene Regel greift nur bei |
| 83 | * `manualIntergreen === null` (engine.ts), also gerade nicht in diesem |
| 84 | * Fall. Der Testfall hielt damit fest, dass zu "Handvorgabe gesetzt und |
| 85 | * Raeumweg 0 m" ueberhaupt nichts gemeldet wird, und begruendete es mit |
| 86 | * einer Meldung, die es dort nie gab: Der Eintrag im Nachbarfeld hob die |
| 87 | * Ausgabesperre auf - unter der Ueberschrift dieses describe, die das |
| 88 | * Gegenteil zusagt. |
| 89 | * |
| 90 | * Ohne Vergleichsgrundlage NICHT ZU VERGLEICHEN bleibt richtig; gerechnet |
| 91 | * wird ohne Raeumweg nichts, was sich gegenhalten liesse. Gemeldet werden |
| 92 | * muss der Zustand trotzdem, und zwar als Fehler - das leistet jetzt |
| 93 | * 'konflikte.vorgabe-ohne-raeumweg'. |
| 94 | */ |
| 95 | const { projekt, konflikt } = mitKonflikt({ manualIntergreen: 1, clearingDistance: 0 }); |
| 96 | expect( |
| 97 | buildSignalPlan(projekt).notes.some( |
| 98 | (n) => n.code === 'zwischenzeit-vorgabe-unter-rechenwert', |
| 99 | ), |
| 100 | ).toBe(false); |
| 101 | |
| 102 | const bericht = validateProject(projekt, buildSignalPlan(projekt), STICHTAG); |
| 103 | const befund = bericht.findings.find((f) => f.rule === 'konflikte.vorgabe-ohne-raeumweg'); |
| 104 | expect(befund, 'Die Vorgabe ohne Vergleichsgrundlage gehoert in den Bericht').toBeDefined(); |
| 105 | expect(befund!.severity).toBe('fehler'); |
| 106 | expect(befund!.target?.id).toBe(konflikt.id); |
| 107 | expect(bericht.exportBlocked).toBe(true); |
| 108 | }); |
| 109 | |
| 110 | it('fuehrt Meldungen der Zwischenzeitrechnung auf ihre Konfliktbeziehung zurueck', () => { |
| 111 | // Zuvor zeigten alle Meldungen aus dem Planaufbau pauschal auf das |
| 112 | // Programm; der Sprungknopf im Pruefbericht fuehrte damit in die |
| 113 | // Phasenansicht, wo sich an einem Raeumweg nichts aendern laesst. |
| 114 | const { projekt, konflikt } = mitKonflikt({ crossingTimeOverride: 0 }); |
| 115 | const bericht = validateProject(projekt, buildSignalPlan(projekt), STICHTAG); |
| 116 | |
| 117 | const befund = bericht.findings.find( |
| 118 | (f) => f.rule === 'signalplan.ueberfahrzeit-unter-gelbzeitschranke', |
| 119 | ); |
| 120 | expect(befund, 'Die gekappte Ueberfahrzeit muss im Bericht stehen').toBeDefined(); |
| 121 | expect(befund!.severity).toBe('fehler'); |
| 122 | expect(befund!.category).toBe('zwischenzeiten'); |
| 123 | expect(befund!.target?.kind).toBe('konflikt'); |
| 124 | expect(befund!.target?.id).toBe(konflikt.id); |
| 125 | expect(befund!.target?.label).toBe('K1 nach K3'); |
| 126 | // Eigene Ueberschrift statt des Sammeltitels "Hinweis aus dem Planaufbau". |
| 127 | expect(befund!.title).not.toContain('Hinweis aus dem Planaufbau'); |
| 128 | }); |
| 129 | }); |