lsa-planer

LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.

/ tests domain ansatzOhneWirkung.test.ts

8,8 KB Rohdatei
tests/domain/ansatzOhneWirkung.test.ts — 199 Zeilen
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 });