lsa-planer

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

/ tests domain wirkungsloseVorgaben.test.ts

15,5 KB Rohdatei
tests/domain/wirkungsloseVorgaben.test.ts — 332 Zeilen
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 });