lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests domain ansatzOhneWirkung.test.ts
| 1 | import { describe, expect, it } from 'vitest'; |
| 2 | import { |
| 3 | createConflict, |
| 4 | createEmptyProject, |
| 5 | createPhase, |
| 6 | createSignalGroup, |
| 7 | createStandardIntersectionProject, |
| 8 | } from '@/domain/model/factory'; |
| 9 | import { buildSignalPlan } from '@/domain/plan/signalPlan'; |
| 10 | import { |
| 11 | ueberfahrzeitAnsatzOhneWirkung, |
| 12 | verfahrenDerKfzRaeumstroeme, |
| 13 | } from '@/domain/rilsa/ansaetze'; |
| 14 | import { validateProject } from '@/domain/validation/engine'; |
| 15 | import type { Project } from '@/domain/model/project'; |
| 16 | |
| 17 | /** |
| 18 | * Fassung 5.43.0: Der Kfz-Ueberfahrzeit-Ansatz wird nur dort genannt, wo er |
| 19 | * gerechnet hat - und dieselbe Antwort tragen alle Ausgaben. |
| 20 | * |
| 21 | * DER BEFUND. Der Absatz unter der Zwischenzeitenmatrix war eingegrenzt, drei |
| 22 | * Stellen derselben Aussage blieben unbedingt: die Deckblattzeile |
| 23 | * "Ueberfahrzeit Kfz (Rechenansatz)", die Tafel "Abweichungen von den |
| 24 | * Regelwerten" und die Meldung 'zwischenzeiten.ueberfahrzeit-ansatz-fest' des |
| 25 | * Pruefberichts. An einer einstreifigen Verkehrsfuehrung rechnet |
| 26 | * `buildSignalPlan` jeden raeumenden Kfz-Strom nach RiLSA 2015, Abschnitt 5.2.2 |
| 27 | * mit einer festen Ueberfahrzeit von 4 s; der Bericht meldete gleichwohl, die |
| 28 | * Ueberfahrzeit werde "mit 3 s geradeaus und 2 s abbiegend angesetzt", und |
| 29 | * verlangte dafuer eine Begruendung - waehrend die Planunterlage desselben Plans |
| 30 | * seit dieser Fassung das Verfahren nennt, das gerechnet hat. |
| 31 | * |
| 32 | * WAS HIER GEPRUEFT WIRD: die Antwort des Fachkerns |
| 33 | * (`verfahrenDerKfzRaeumstroeme`, `ueberfahrzeitAnsatzOhneWirkung`) und der |
| 34 | * Pruefbericht, der sie liest. Dass die Planunterlage sie an drei Stellen |
| 35 | * dieselbe liest, prueft tests/export/matrixVerfahrenssatz.test.ts. |
| 36 | * |
| 37 | * DIE DRITTE LAGE HAT KEINE ANLAGENART: Ist jede Zwischenzeit eines raeumenden |
| 38 | * Kraftfahrzeugs von Hand vorgegeben, hat der Ansatz auch am Knotenpunkt nicht |
| 39 | * gerechnet (`calculation: null`). Deshalb liest die Antwort den PLAN und nicht |
| 40 | * `project.anlagenart`. |
| 41 | */ |
| 42 | |
| 43 | const DATUM = new Date('2026-01-01T12:00:00Z'); |
| 44 | |
| 45 | /** Zwei Richtungen einer Baustellenampel, 200 m Engstelle, 50 km/h. */ |
| 46 | function baustelle(): Project { |
| 47 | const basis = createEmptyProject('Baustelle', DATUM, 'einstreifig'); |
| 48 | const a = createSignalGroup({ name: 'K1', mode: 'kfz', vZul: 50, index: 0 }); |
| 49 | const b = createSignalGroup({ name: 'K2', mode: 'kfz', vZul: 50, index: 1 }); |
| 50 | const p1 = createPhase('Seite A', [a.id]); |
| 51 | const p2 = createPhase('Seite B', [b.id]); |
| 52 | const beziehung = (von: string, nach: string) => ({ |
| 53 | ...createConflict(von, nach), |
| 54 | clearingDistance: 200, |
| 55 | enteringDistance: 0, |
| 56 | }); |
| 57 | return { |
| 58 | ...basis, |
| 59 | signalGroups: [a, b], |
| 60 | conflicts: [beziehung(a.id, b.id), beziehung(b.id, a.id)], |
| 61 | phases: [p1, p2], |
| 62 | program: { ...basis.program, phaseOrder: [p1.id, p2.id] }, |
| 63 | }; |
| 64 | } |
| 65 | |
| 66 | /** Eine Fussgaengerschutzanlage: geraeumt wird nur von der Furt. */ |
| 67 | function nurFussRaeumt(): Project { |
| 68 | const basis = createEmptyProject('Furt', DATUM, 'fussgaengerschutzanlage'); |
| 69 | const f = createSignalGroup({ name: 'F1', mode: 'fuss', vZul: 50, index: 0 }); |
| 70 | const k = createSignalGroup({ name: 'K1', mode: 'kfz', vZul: 50, index: 1 }); |
| 71 | const p1 = createPhase('Furt', [f.id]); |
| 72 | const p2 = createPhase('Fahrbahn', [k.id]); |
| 73 | return { |
| 74 | ...basis, |
| 75 | signalGroups: [f, k], |
| 76 | conflicts: [{ ...createConflict(f.id, k.id), clearingDistance: 10, enteringDistance: 0 }], |
| 77 | phases: [p1, p2], |
| 78 | program: { ...basis.program, phaseOrder: [p1.id, p2.id] }, |
| 79 | }; |
| 80 | } |
| 81 | |
| 82 | /** Jede Beziehung mit raeumendem Kraftfahrzeug von Hand vorgegeben. */ |
| 83 | function mitHandvorgaben(project: Project, wert = 8): Project { |
| 84 | return { |
| 85 | ...project, |
| 86 | conflicts: project.conflicts.map((c) => |
| 87 | project.signalGroups.find((g) => g.id === c.fromId)?.mode === 'kfz' |
| 88 | ? { ...c, manualIntergreen: wert } |
| 89 | : c, |
| 90 | ), |
| 91 | }; |
| 92 | } |
| 93 | |
| 94 | function mitFestemAnsatz(project: Project): Project { |
| 95 | return { ...project, settings: { ...project.settings, ueberfahrzeitAnsatz: 'fest' } }; |
| 96 | } |
| 97 | |
| 98 | describe('Der Fachkern sagt, welches Verfahren die Kfz-Zwischenzeiten gerechnet hat', () => { |
| 99 | it('nennt am Knotenpunkt die Knotenpunktformel und keine Wirkungslosigkeit', () => { |
| 100 | const projekt = createStandardIntersectionProject('Knotenpunkt', DATUM); |
| 101 | const plan = buildSignalPlan(projekt); |
| 102 | expect([...verfahrenDerKfzRaeumstroeme(projekt, plan)]).toEqual(['knotenpunkt']); |
| 103 | expect(ueberfahrzeitAnsatzOhneWirkung(projekt, plan)).toBeNull(); |
| 104 | }); |
| 105 | |
| 106 | it('nennt an der einstreifigen Verkehrsfuehrung das Verfahren des Abschnitts 5.2.2', () => { |
| 107 | const projekt = baustelle(); |
| 108 | const plan = buildSignalPlan(projekt); |
| 109 | expect([...verfahrenDerKfzRaeumstroeme(projekt, plan)]).toEqual(['engstelle']); |
| 110 | |
| 111 | const ohneWirkung = ueberfahrzeitAnsatzOhneWirkung(projekt, plan); |
| 112 | expect(ohneWirkung, 'der Ansatz rechnet dort nicht').not.toBeNull(); |
| 113 | expect(ohneWirkung!.vermerk).toContain('ohne Wirkung an dieser Anlage'); |
| 114 | expect(ohneWirkung!.begruendung).toContain('RiLSA 2015, Abschnitt 5.2.2'); |
| 115 | expect(ohneWirkung!.begruendung, 'die feste Ueberfahrzeit des Abschnitts').toContain( |
| 116 | 'tü = 4 s', |
| 117 | ); |
| 118 | expect(ohneWirkung!.zutun, 'zu begruenden ist nichts').toContain('nichts zu begründen'); |
| 119 | }); |
| 120 | |
| 121 | it('erkennt die Handvorgabe auch am Knotenpunkt, wo keine Anlagenart sie verraet', () => { |
| 122 | // Die Lage, die kein Blick auf project.anlagenart findet: Die Formel des |
| 123 | // Abschnitts 2 gilt, aber sie ist an keiner Beziehung gelaufen. |
| 124 | const projekt = mitHandvorgaben(createStandardIntersectionProject('Vorgaben', DATUM)); |
| 125 | const plan = buildSignalPlan(projekt); |
| 126 | expect([...verfahrenDerKfzRaeumstroeme(projekt, plan)]).toEqual(['vorgegeben']); |
| 127 | |
| 128 | const ohneWirkung = ueberfahrzeitAnsatzOhneWirkung(projekt, plan); |
| 129 | expect(ohneWirkung, 'auch hier hat der Ansatz nicht gerechnet').not.toBeNull(); |
| 130 | expect(ohneWirkung!.vermerk).toContain('von Hand vorgegeben'); |
| 131 | expect(ohneWirkung!.begruendung, 'die Gegenprobe ist die Ausnahme').toContain('Gegenprobe'); |
| 132 | }); |
| 133 | |
| 134 | it('nennt die Wirkungslosigkeit auch, wo gar kein Kraftfahrzeug raeumt', () => { |
| 135 | const projekt = nurFussRaeumt(); |
| 136 | const plan = buildSignalPlan(projekt); |
| 137 | expect([...verfahrenDerKfzRaeumstroeme(projekt, plan)], 'keine Kfz-Raeumbeziehung').toEqual([]); |
| 138 | expect(ueberfahrzeitAnsatzOhneWirkung(projekt, plan)?.vermerk).toContain( |
| 139 | 'kein räumendes Kraftfahrzeug erfasst', |
| 140 | ); |
| 141 | }); |
| 142 | |
| 143 | it('bleibt bei "es rechnet", wo eine einzige Beziehung nach Abschnitt 2 gerechnet ist', () => { |
| 144 | // Die Richtung der Antwort: Eine zu viel verlangte Begruendung ist harmlos, |
| 145 | // die Behauptung "ohne Wirkung" ueber einen rechnenden Ansatz nicht. |
| 146 | const alle = createStandardIntersectionProject('Gemischt', DATUM); |
| 147 | const eineFrei = { |
| 148 | ...mitHandvorgaben(alle), |
| 149 | conflicts: mitHandvorgaben(alle).conflicts.map((c, i) => |
| 150 | i === 0 ? { ...c, manualIntergreen: null } : c, |
| 151 | ), |
| 152 | }; |
| 153 | const plan = buildSignalPlan(eineFrei); |
| 154 | expect(verfahrenDerKfzRaeumstroeme(eineFrei, plan).has('knotenpunkt')).toBe(true); |
| 155 | expect(ueberfahrzeitAnsatzOhneWirkung(eineFrei, plan)).toBeNull(); |
| 156 | }); |
| 157 | }); |
| 158 | |
| 159 | describe('Der Pruefbericht verlangt keine Begruendung fuer eine Wahl ohne Wirkung', () => { |
| 160 | function befund(project: Project) { |
| 161 | const plan = buildSignalPlan(project); |
| 162 | return validateProject(project, plan, DATUM).findings.find( |
| 163 | (f) => f.rule === 'zwischenzeiten.ueberfahrzeit-ansatz-fest', |
| 164 | ); |
| 165 | } |
| 166 | |
| 167 | it('meldet am Knotenpunkt weiter die Warnung mit der Fehlerrichtung', () => { |
| 168 | const treffer = befund(mitFestemAnsatz(createStandardIntersectionProject('Knoten', DATUM))); |
| 169 | expect(treffer?.severity).toBe('warnung'); |
| 170 | expect(treffer?.message).toContain('VERKÜRZT'); |
| 171 | expect(treffer?.suggestion).toContain('Begründen Sie die Wahl'); |
| 172 | }); |
| 173 | |
| 174 | it('meldet an der Engstelle einen Hinweis und nennt das Verfahren, das gerechnet hat', () => { |
| 175 | const treffer = befund(mitFestemAnsatz(baustelle())); |
| 176 | expect(treffer, 'die Meldung bleibt - die Einstellung steht ja da').toBeDefined(); |
| 177 | expect(treffer?.severity, 'zu begruenden ist nichts').toBe('hinweis'); |
| 178 | expect(treffer?.message).toContain('RiLSA 2015, Abschnitt 5.2.2'); |
| 179 | expect(treffer?.message, 'die Rechnung, die dort nicht stattfand').not.toContain( |
| 180 | 'angesetzt, unabhängig von der zulässigen Höchstgeschwindigkeit', |
| 181 | ); |
| 182 | expect(treffer?.message, 'und keine Fehlerrichtung ohne Rechnung').not.toContain('VERKÜRZT'); |
| 183 | expect(treffer?.suggestion).toContain('nichts zu begründen'); |
| 184 | }); |
| 185 | |
| 186 | it('meldet den Hinweis auch, wo jede Kfz-Zwischenzeit von Hand vorgegeben ist', () => { |
| 187 | const treffer = befund( |
| 188 | mitFestemAnsatz(mitHandvorgaben(createStandardIntersectionProject('Vorgaben', DATUM))), |
| 189 | ); |
| 190 | expect(treffer?.severity).toBe('hinweis'); |
| 191 | expect(treffer?.message).toContain('von Hand'); |
| 192 | }); |
| 193 | |
| 194 | it('schweigt beim Regelansatz weiter ganz', () => { |
| 195 | const projekt = baustelle(); |
| 196 | expect(projekt.settings.ueberfahrzeitAnsatz, 'Vorbedingung: die Vorgabe gilt').toBe('gelbzeit'); |
| 197 | expect(befund(projekt)).toBeUndefined(); |
| 198 | }); |
| 199 | }); |