lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests domain massgebenderStromBericht.test.ts
| 1 | import { describe, expect, it } from 'vitest'; |
| 2 | import { createStandardIntersectionProject } from '@/domain/model/factory'; |
| 3 | import type { Conflict, Project } from '@/domain/model/project'; |
| 4 | import { |
| 5 | buildSignalPlan, |
| 6 | intergreenKey, |
| 7 | massgebendeRaeumbeziehung, |
| 8 | } from '@/domain/plan/signalPlan'; |
| 9 | import { raeumbeziehungSatz } from '@/domain/rilsa/ansaetze'; |
| 10 | import { RILSA_DEFAULTS } from '@/domain/rilsa/constants'; |
| 11 | import { validateProject } from '@/domain/validation/engine'; |
| 12 | |
| 13 | /* |
| 14 | * Fassung 5.11.0 - der massgebende Strom im PRUEFBERICHT. |
| 15 | * |
| 16 | * Eine fruehere Behebung hat die Entscheidung, mit welcher Fahrbeziehung eine Beziehung |
| 17 | * raeumt, an genau eine Stelle gelegt: `massgebendeRaeumbeziehung(from, |
| 18 | * conflict)` in signalPlan.ts. Der Planaufbau liest sie. Der Pruefbericht |
| 19 | * bildete den Ausdruck bis zu diesem Befund weiterhin selbst und nahm |
| 20 | * `from.movement` - die Fahrbeziehung der SIGNALGRUPPE. |
| 21 | * |
| 22 | * Was der Anwender davon merkt: An einer Sammelsignalgruppe "geradeaus UND |
| 23 | * rechts" - dem Regelfall des gemeinsamen Fahrstreifens - raeumt der Plan die |
| 24 | * Beziehung als Rechtsabbieger und setzt das Merkmal "enger Innenradius" an; |
| 25 | * der Bericht derselben Rechnung meldet daneben, das Merkmal gehe "in keine |
| 26 | * Zwischenzeit ein", weil der raeumende Strom kein abbiegender |
| 27 | * Kraftfahrzeugstrom sei. Zwei Aussagen zu EINER Beziehung, und die des |
| 28 | * Berichts raet dazu, ein Merkmal zu entfernen, das gerechnet hat. |
| 29 | */ |
| 30 | |
| 31 | const DATUM = new Date('2026-01-01T00:00:00Z'); |
| 32 | |
| 33 | /** |
| 34 | * Standardknotenpunkt, an dem "K1" geradeaus UND rechts fuehrt: Die |
| 35 | * Signalgruppe bleibt bei 'geradeaus', die Beziehung K1 -> K3 traegt die |
| 36 | * Fahrbeziehung ihres massgebenden Stroms ('rechts') - genau das, was |
| 37 | * `uebernimmWege` aus einem Lageplan schreibt. |
| 38 | */ |
| 39 | function sammelgruppe(zusatz: Partial<Conflict> = {}): Project { |
| 40 | const basis = createStandardIntersectionProject('Sammelgruppe', DATUM); |
| 41 | const k1 = basis.signalGroups.find((g) => g.name === 'K1')!; |
| 42 | const k3 = basis.signalGroups.find((g) => g.name === 'K3')!; |
| 43 | return { |
| 44 | ...basis, |
| 45 | conflicts: basis.conflicts.map((c) => |
| 46 | c.fromId === k1.id && c.toId === k3.id |
| 47 | ? { ...c, raeumbeziehung: 'rechts' as const, ...zusatz } |
| 48 | : c, |
| 49 | ), |
| 50 | }; |
| 51 | } |
| 52 | |
| 53 | function beziehung(project: Project): Conflict { |
| 54 | const k1 = project.signalGroups.find((g) => g.name === 'K1')!; |
| 55 | const k3 = project.signalGroups.find((g) => g.name === 'K3')!; |
| 56 | return project.conflicts.find((c) => c.fromId === k1.id && c.toId === k3.id)!; |
| 57 | } |
| 58 | |
| 59 | describe('N1/1: Der Bericht beurteilt den engen Innenradius am massgebenden Strom', () => { |
| 60 | it('meldet ihn als wirksam, wo der Plan mit ihm rechnet', () => { |
| 61 | const project = sammelgruppe({ engerRadius: true }); |
| 62 | const konflikt = beziehung(project); |
| 63 | const plan = buildSignalPlan(project); |
| 64 | const gerechnet = plan.intergreens.get(intergreenKey(konflikt.fromId, konflikt.toId)); |
| 65 | |
| 66 | // Der Plan raeumt als Rechtsabbieger mit engem Innenradius: 5,0 m/s. |
| 67 | expect(gerechnet?.calculation?.clearingSpeed).toBe( |
| 68 | RILSA_DEFAULTS.clearingSpeed.kfzTurningEngerRadius, |
| 69 | ); |
| 70 | |
| 71 | const bericht = validateProject(project, plan, DATUM); |
| 72 | const zurBeziehung = bericht.findings.filter( |
| 73 | (f) => f.target?.kind === 'konflikt' && f.target.id === konflikt.id, |
| 74 | ); |
| 75 | |
| 76 | expect(zurBeziehung.map((f) => f.rule)).toContain('zwischenzeiten.enger-radius'); |
| 77 | expect(zurBeziehung.map((f) => f.rule)).not.toContain( |
| 78 | 'zwischenzeiten.enger-radius-ohne-wirkung', |
| 79 | ); |
| 80 | }); |
| 81 | |
| 82 | it('nennt nicht "kein abbiegender Kraftfahrzeugstrom", wo der Plan abbiegend raeumt', () => { |
| 83 | const project = sammelgruppe({ engerRadius: true }); |
| 84 | const konflikt = beziehung(project); |
| 85 | const bericht = validateProject(project, buildSignalPlan(project), DATUM); |
| 86 | const texte = bericht.findings |
| 87 | .filter((f) => f.target?.kind === 'konflikt' && f.target.id === konflikt.id) |
| 88 | .map((f) => f.message) |
| 89 | .join(' '); |
| 90 | |
| 91 | expect(texte).not.toContain('kein abbiegender Kraftfahrzeugstrom'); |
| 92 | }); |
| 93 | |
| 94 | it('bleibt bei der Gruppenbeziehung, wo die Beziehung keine eigene fuehrt', () => { |
| 95 | // Gegenprobe: ohne `raeumbeziehung` gilt die Fahrbeziehung der Gruppe |
| 96 | // ('geradeaus'). Dort wirkt das Merkmal wirklich nicht - der Bericht muss |
| 97 | // weiter warnen, sonst prueft der Fall oben nichts. |
| 98 | const basis = sammelgruppe({ engerRadius: true }); |
| 99 | const project: Project = { |
| 100 | ...basis, |
| 101 | conflicts: basis.conflicts.map((c) => { |
| 102 | const { raeumbeziehung: _weg, ...ohne } = c; |
| 103 | return ohne; |
| 104 | }), |
| 105 | }; |
| 106 | const konflikt = beziehung(project); |
| 107 | const plan = buildSignalPlan(project); |
| 108 | |
| 109 | expect( |
| 110 | plan.intergreens.get(intergreenKey(konflikt.fromId, konflikt.toId))?.calculation |
| 111 | ?.clearingSpeed, |
| 112 | ).toBe(RILSA_DEFAULTS.clearingSpeed.kfzStraight); |
| 113 | expect( |
| 114 | validateProject(project, plan, DATUM) |
| 115 | .findings.filter((f) => f.target?.kind === 'konflikt' && f.target.id === konflikt.id) |
| 116 | .map((f) => f.rule), |
| 117 | ).toContain('zwischenzeiten.enger-radius-ohne-wirkung'); |
| 118 | }); |
| 119 | |
| 120 | it('vergleicht die Handeingabe mit der Raeumgeschwindigkeit des massgebenden Stroms', () => { |
| 121 | /* |
| 122 | * Der dritte Zweig der Regel (Handeingabe hat Vorrang) nennt die |
| 123 | * Raeumgeschwindigkeit, die das Merkmal ergaebe. Mit der Fahrbeziehung der |
| 124 | * Gruppe waren das 10,0 m/s - der Geradeauswert -, und der Satz "Geraeumt |
| 125 | * wird mit 9 m/s, nicht mit 10 m/s" behauptete eine Verkuerzung, wo die |
| 126 | * Eingabe die Zwischenzeit in Wahrheit VERLAENGERT (5,0 m/s waeren |
| 127 | * langsamer). |
| 128 | */ |
| 129 | const project = sammelgruppe({ engerRadius: true, clearingSpeedOverride: 9 }); |
| 130 | const konflikt = beziehung(project); |
| 131 | const befund = validateProject(project, buildSignalPlan(project), DATUM).findings.find( |
| 132 | (f) => |
| 133 | f.rule === 'zwischenzeiten.enger-radius-ohne-wirkung' && |
| 134 | f.target?.kind === 'konflikt' && |
| 135 | f.target.id === konflikt.id, |
| 136 | ); |
| 137 | |
| 138 | expect(befund?.message).toContain('nicht mit 5 m/s'); |
| 139 | expect(befund?.message).toContain('KÜRZER'); |
| 140 | }); |
| 141 | }); |
| 142 | |
| 143 | /* |
| 144 | * NACHTRAG (Fassung 5.11.0): Der Bericht urteilt am massgebenden Strom, sagte |
| 145 | * als einzige der vier Auskunftsstellen aber nicht, dass er es tut. Im |
| 146 | * Spiegelfall widersprach er damit der Signalgruppentabelle: "der raeumende |
| 147 | * Strom "K1" ist kein abbiegender Kraftfahrzeugstrom" ueber einer Gruppe, die |
| 148 | * als rechts abbiegend erfasst ist. |
| 149 | */ |
| 150 | describe('N1/7: Der Bericht sagt, dass er am massgebenden Strom urteilt', () => { |
| 151 | /** Meldungen zur Beziehung K1 -> K3. */ |
| 152 | function meldungen(project: Project): { rule: string; message: string }[] { |
| 153 | const konflikt = beziehung(project); |
| 154 | return validateProject(project, buildSignalPlan(project), DATUM) |
| 155 | .findings.filter((f) => f.target?.kind === 'konflikt' && f.target.id === konflikt.id) |
| 156 | .map((f) => ({ rule: f.rule, message: f.message })); |
| 157 | } |
| 158 | |
| 159 | it('nennt den Strom, wo das Merkmal an ihm wirkt', () => { |
| 160 | const project = sammelgruppe({ engerRadius: true }); |
| 161 | const befund = meldungen(project).find((f) => f.rule === 'zwischenzeiten.enger-radius'); |
| 162 | |
| 163 | expect(befund?.message).toContain( |
| 164 | 'Den Räumweg dieser Beziehung fährt ein Strom der Signalgruppe „K1“ mit der ' + |
| 165 | 'Fahrbeziehung „rechts abbiegend“; geräumt wird mit ihr.', |
| 166 | ); |
| 167 | }); |
| 168 | |
| 169 | it('nennt ihn im Spiegelfall, in dem die Gruppe abbiegt und der Strom geradeaus faehrt', () => { |
| 170 | /* |
| 171 | * K1 ist als rechts abbiegend erfasst, den Raeumweg dieser Beziehung faehrt |
| 172 | * aber der Geradeausstrom. Das Merkmal wirkt an ihm nicht - der Bericht |
| 173 | * warnt zu Recht und muss daneben sagen, warum er die Gruppe anders |
| 174 | * beurteilt, als die Signalgruppentabelle sie fuehrt. |
| 175 | */ |
| 176 | const basis = sammelgruppe({ engerRadius: true }); |
| 177 | const project: Project = { |
| 178 | ...basis, |
| 179 | signalGroups: basis.signalGroups.map((g) => |
| 180 | g.name === 'K1' ? { ...g, movement: 'rechts' as const } : g, |
| 181 | ), |
| 182 | conflicts: basis.conflicts.map((c) => |
| 183 | c.raeumbeziehung === undefined ? c : { ...c, raeumbeziehung: 'geradeaus' as const }, |
| 184 | ), |
| 185 | }; |
| 186 | const befund = meldungen(project).find( |
| 187 | (f) => f.rule === 'zwischenzeiten.enger-radius-ohne-wirkung', |
| 188 | ); |
| 189 | |
| 190 | expect(befund?.message).toContain('kein abbiegender Kraftfahrzeugstrom'); |
| 191 | expect(befund?.message).toContain( |
| 192 | 'Den Räumweg dieser Beziehung fährt ein Strom der Signalgruppe „K1“ mit der ' + |
| 193 | 'Fahrbeziehung „geradeaus“; geräumt wird mit ihr.', |
| 194 | ); |
| 195 | }); |
| 196 | |
| 197 | it('nennt ihn auch dort, wo eine Handeingabe dem Merkmal vorgeht', () => { |
| 198 | const project = sammelgruppe({ engerRadius: true, clearingSpeedOverride: 9 }); |
| 199 | const befund = meldungen(project).find( |
| 200 | (f) => f.rule === 'zwischenzeiten.enger-radius-ohne-wirkung', |
| 201 | ); |
| 202 | |
| 203 | expect(befund?.message).toContain('Den Räumweg dieser Beziehung fährt ein Strom'); |
| 204 | }); |
| 205 | |
| 206 | it('schweigt dazu, wo Strom und Signalgruppe dieselbe Fahrbeziehung haben', () => { |
| 207 | // Gegenprobe: ohne `raeumbeziehung` gibt es nichts zu erklaeren, und ein |
| 208 | // Satz darueber waere eine Behauptung. |
| 209 | const basis = sammelgruppe({ engerRadius: true }); |
| 210 | const project: Project = { |
| 211 | ...basis, |
| 212 | conflicts: basis.conflicts.map((c) => { |
| 213 | const { raeumbeziehung: _weg, ...ohne } = c; |
| 214 | return ohne; |
| 215 | }), |
| 216 | }; |
| 217 | |
| 218 | expect( |
| 219 | meldungen(project) |
| 220 | .map((f) => f.message) |
| 221 | .join(' '), |
| 222 | ).not.toContain('Den Räumweg dieser Beziehung fährt'); |
| 223 | }); |
| 224 | |
| 225 | it('formuliert den Satz einmal - Bericht und Konfliktfenster teilen ihn', () => { |
| 226 | // `raeumbeziehungSatz` ist die eine Quelle; das Konfliktfenster setzt |
| 227 | // denselben Satz in seinen Kopf (tests/ui/massgebenderStromKonfliktfenster.test.ts). |
| 228 | const project = sammelgruppe({ engerRadius: true }); |
| 229 | const konflikt = beziehung(project); |
| 230 | const from = project.signalGroups.find((g) => g.id === konflikt.fromId)!; |
| 231 | const satz = raeumbeziehungSatz(from, konflikt); |
| 232 | |
| 233 | expect(satz).not.toBe(''); |
| 234 | expect( |
| 235 | meldungen(project).find((f) => f.rule === 'zwischenzeiten.enger-radius')?.message, |
| 236 | ).toContain(satz); |
| 237 | }); |
| 238 | }); |
| 239 | |
| 240 | describe('N1/2: Plan und Bericht legen dieselbe Fahrbeziehung zugrunde', () => { |
| 241 | it('beurteilt jede Beziehung mit der Fahrbeziehung, die massgebendeRaeumbeziehung nennt', () => { |
| 242 | const project = sammelgruppe({ engerRadius: true }); |
| 243 | const plan = buildSignalPlan(project); |
| 244 | const bericht = validateProject(project, plan, DATUM); |
| 245 | |
| 246 | const abweichend = project.conflicts |
| 247 | .filter((c) => c.engerRadius === true) |
| 248 | .map((c) => { |
| 249 | const from = project.signalGroups.find((g) => g.id === c.fromId)!; |
| 250 | const massgebend = massgebendeRaeumbeziehung(from, c); |
| 251 | const abbiegend = massgebend === 'links' || massgebend === 'rechts'; |
| 252 | const gemeldet = bericht.findings.some( |
| 253 | (f) => |
| 254 | f.rule === 'zwischenzeiten.enger-radius' && |
| 255 | f.target?.kind === 'konflikt' && |
| 256 | f.target.id === c.id, |
| 257 | ); |
| 258 | return { beziehung: `${c.fromId} -> ${c.toId}`, massgebend, abbiegend, gemeldet }; |
| 259 | }) |
| 260 | .filter((z) => z.abbiegend !== z.gemeldet); |
| 261 | |
| 262 | expect(abweichend).toEqual([]); |
| 263 | }); |
| 264 | }); |