lsa-planer

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

/ tests domain verkehrsstaerken.test.ts

10,4 KB Rohdatei
tests/domain/verkehrsstaerken.test.ts — 249 Zeilen
1 import { describe, expect, it } from 'vitest';
2 import { computeCycleTime } from '@/domain/rilsa/cycle';
3 import { buildSignalPlan } from '@/domain/plan/signalPlan';
4 import { validateProject } from '@/domain/validation/engine';
5 import {
6 createConflict,
7 createEmptyProject,
8 createPhase,
9 createSignalGroup,
10 createStandardIntersectionProject,
11 } from '@/domain/model/factory';
12 import type { Project } from '@/domain/model/project';
13
14 /*
15 * Befund 32 (Fassung 5.9.0).
16 *
17 * Eine Phase ohne erfasste Verkehrsstaerke ging mit dem Saettigungsgrad 0 in
18 * die Summe Y ein, ohne dass irgendwo stand, dass die Zahl fehlt: Die
19 * Umlaufzeitermittlung meldete nur die LEERE Liste, und ihr einziger Aufrufer
20 * uebergab statt einer Luecke die Zahl 0. Die ermittelte Umlaufzeit faellt
21 * dadurch zu kurz aus - die Anlage erscheint leistungsfaehiger, als sie ist.
22 *
23 * Unterschieden werden muessen zwei Faelle, die beide das Gewicht 0 ergeben:
24 * - "nicht erfasst": Die Phase fuehrt Signalgruppen mit
25 * Saettigungsverkehrsstaerke (Kfz, OePNV), zu keiner davon ist eine
26 * Verkehrsstaerke eingetragen. Das ist die Luecke.
27 * - "kein Verkehr im Sinne der Rechnung": Eine reine Fussgaengerphase traegt
28 * ueberhaupt keine Saettigungsverkehrsstaerke. Ihr Gewicht ist im Regelfall
29 * 0, und daran ist nichts zu beanstanden.
30 */
31
32 const DATUM = new Date('2026-01-01T00:00:00Z');
33 const CODE = 'saettigungsgrade-unvollstaendig';
34
35 describe('Umlaufzeitermittlung meldet fehlende Verkehrsstaerken', () => {
36 it('meldet die Phase, deren Verkehrsstaerke nicht erfasst ist', () => {
37 const ergebnis = computeCycleTime('webster', {
38 lostTime: 20,
39 criticalFlowRatios: [0.35, undefined],
40 });
41
42 const notiz = ergebnis.notes.find((n) => n.code === CODE);
43 expect(notiz).toBeDefined();
44 expect(notiz?.severity).toBe('warnung');
45 // Der Anwender muss wissen, WELCHE Phase gemeint ist - sonst sucht er in
46 // allen.
47 expect(notiz?.message).toContain('Phase 2');
48 expect(notiz?.message).not.toContain('Phase 1');
49 });
50
51 it('nennt alle betroffenen Phasen in einer einzigen Meldung', () => {
52 const ergebnis = computeCycleTime('webster', {
53 lostTime: 20,
54 criticalFlowRatios: [undefined, 0.3, undefined],
55 });
56
57 const notizen = ergebnis.notes.filter((n) => n.code === CODE);
58 expect(notizen).toHaveLength(1);
59 expect(notizen[0]?.message).toContain('Phase 1');
60 expect(notizen[0]?.message).toContain('Phase 3');
61 expect(notizen[0]?.message).not.toContain('Phase 2');
62 });
63
64 it('sagt, in welche Richtung die Luecke wirkt', () => {
65 // Die gefaehrliche Richtung: Y faellt zu klein aus, die Umlaufzeit damit
66 // zu kurz. Ohne diesen Satz liest sich die Meldung wie eine Formsache.
67 const ergebnis = computeCycleTime('webster', {
68 lostTime: 20,
69 criticalFlowRatios: [0.35, undefined],
70 });
71
72 const notiz = ergebnis.notes.find((n) => n.code === CODE);
73 expect(notiz?.message).toContain('zu kurz');
74 });
75
76 it('schweigt bei einem erfassten Saettigungsgrad 0', () => {
77 // 0 heisst "gezaehlt und kein Verkehr" - das ist eine Angabe, keine Luecke.
78 const ergebnis = computeCycleTime('webster', {
79 lostTime: 20,
80 criticalFlowRatios: [0.35, 0],
81 });
82
83 expect(ergebnis.notes.some((n) => n.code === CODE)).toBe(false);
84 });
85
86 it('rechnet mit der Summe der erfassten Saettigungsgrade weiter', () => {
87 // Die Luecke wird gemeldet, nicht geschaetzt: Ein Ersatzwert waere eine
88 // erfundene Verkehrsstaerke im Ausdruck.
89 const mitLuecke = computeCycleTime('webster', {
90 lostTime: 20,
91 criticalFlowRatios: [0.35, undefined],
92 });
93 const ohnePhase = computeCycleTime('webster', { lostTime: 20, criticalFlowRatios: [0.35] });
94
95 expect(mitLuecke.totalFlowRatio).toBe(ohnePhase.totalFlowRatio);
96 expect(mitLuecke.cycleTime).toBe(ohnePhase.cycleTime);
97 });
98 });
99
100 /** Vierarmiger Knotenpunkt, dem die Verkehrsstaerken der Ost/West-Phase fehlen. */
101 function ohneVerkehrsstaerkenInPhase2(): Project {
102 const roh = createStandardIntersectionProject('Lücke', DATUM);
103 const k3 = roh.signalGroups.find((g) => g.name === 'K3')!;
104 const k4 = roh.signalGroups.find((g) => g.name === 'K4')!;
105 return {
106 ...roh,
107 demands: roh.demands.filter((d) => d.signalGroupId !== k3.id && d.signalGroupId !== k4.id),
108 };
109 }
110
111 /** Derselbe Knotenpunkt mit einer dritten, reinen Fussgaengerphase. */
112 function mitReinerFussgaengerphase(): Project {
113 const roh = createStandardIntersectionProject('Furt', DATUM);
114 const f3 = createSignalGroup({ name: 'F3', mode: 'fuss', index: 6 });
115 const phase3 = createPhase('Phase 3 - Furt', [f3.id]);
116 return {
117 ...roh,
118 signalGroups: [...roh.signalGroups, f3],
119 phases: [...roh.phases, phase3],
120 program: { ...roh.program, phaseOrder: [...roh.program.phaseOrder, phase3.id] },
121 };
122 }
123
124 describe('Der Planaufbau reicht die Luecke weiter', () => {
125 it('meldet die Phase ohne erfasste Verkehrsstaerke im Signalzeitenplan', () => {
126 const plan = buildSignalPlan(ohneVerkehrsstaerkenInPhase2());
127
128 const notiz = plan.notes.find((n) => n.code === CODE);
129 expect(notiz).toBeDefined();
130 expect(notiz?.message).toContain('Phase 2');
131 });
132
133 it('schweigt, wenn alle Phasen mit Kraftfahrzeugen erfasst sind', () => {
134 const plan = buildSignalPlan(createStandardIntersectionProject('Vollständig', DATUM));
135
136 expect(plan.notes.some((n) => n.code === CODE)).toBe(false);
137 });
138
139 it('beanstandet eine reine Fussgaengerphase nicht', () => {
140 // Der Regelfall. Wuerde er gemeldet, stuende an jedem zweiten Plan eine
141 // Warnung, die nichts bedeutet - und die echten Luecken gingen darin unter.
142 const plan = buildSignalPlan(mitReinerFussgaengerphase());
143
144 expect(plan.notes.some((n) => n.code === CODE)).toBe(false);
145 });
146 });
147
148 /** Einstreifige Verkehrsfuehrung ueber eine Engstelle, ohne Verkehrsstaerken. */
149 function engstelleOhneVerkehrsstaerken(): Project {
150 const basis = createEmptyProject('Arbeitsstelle', DATUM, 'einstreifig');
151 const nord = createSignalGroup({ name: 'A1', mode: 'kfz', index: 0 });
152 const sued = createSignalGroup({ name: 'A2', mode: 'kfz', index: 1 });
153 const phase1 = createPhase('Richtung Nord', [nord.id]);
154 const phase2 = createPhase('Richtung Süd', [sued.id]);
155 return {
156 ...basis,
157 signalGroups: [nord, sued],
158 conflicts: [
159 { ...createConflict(nord.id, sued.id), clearingDistance: 200, enteringDistance: 0 },
160 { ...createConflict(sued.id, nord.id), clearingDistance: 200, enteringDistance: 0 },
161 ],
162 phases: [phase1, phase2],
163 program: { ...basis.program, phaseOrder: [phase1.id, phase2.id] },
164 };
165 }
166
167 /**
168 * Nachtrag zu Befund 32: EINE Ursache, EINE Zeile im Bericht.
169 *
170 * Der Pruefbericht wird gedruckt und traegt eine Warnungsbilanz. Ein Projekt
171 * ganz ohne Verkehrsstaerken erhielt danach drei Warnungen fuer denselben
172 * Grund - 'signalplan.saettigungsgrade-unvollstaendig',
173 * 'signalplan.keine-gewichtung' und 'leistungsfaehigkeit.keine-verkehrsstaerken' -,
174 * vorher zwei. Dasselbe Muster haben die Befunde 14, 35, 36 und 37
175 * (Fassung 5.9.0).
176 */
177 describe('Der Bericht nennt die fehlenden Verkehrsstaerken nur einmal', () => {
178 /** Derselbe Knotenpunkt, einmal mit und einmal ganz ohne Verkehrsstaerken. */
179 const mitVerkehrsstaerken = createStandardIntersectionProject('Vollständig', DATUM);
180 const ohneVerkehrsstaerken: Project = { ...mitVerkehrsstaerken, demands: [] };
181
182 /** Regeln, die genau diese eine Ursache melden. */
183 const URSACHE = [
184 'signalplan.saettigungsgrade-unvollstaendig',
185 'signalplan.keine-gewichtung',
186 'leistungsfaehigkeit.keine-verkehrsstaerken',
187 ];
188
189 it('zaehlt fuer ein Projekt ganz ohne Verkehrsstaerken zwei Warnungen, nicht drei', () => {
190 const mit = validateProject(mitVerkehrsstaerken, undefined, DATUM);
191 const ohne = validateProject(ohneVerkehrsstaerken, undefined, DATUM);
192
193 // Festgehalten wird der Zuwachs, nicht die Gesamtzahl: Die uebrigen
194 // Warnungen des Beispielprojekts (Projektdaten, kurze Zwischenzeiten)
195 // stehen in beiden Berichten und gehen andere Regeln an.
196 expect(ohne.warningCount - mit.warningCount).toBe(2);
197 expect(ohne.findings.filter((f) => URSACHE.includes(f.rule)).map((f) => f.rule)).toEqual([
198 'signalplan.keine-gewichtung',
199 'leistungsfaehigkeit.keine-verkehrsstaerken',
200 ]);
201 });
202
203 it('behaelt die Richtungsaussage, obwohl die Plan-Notiz entfaellt', () => {
204 // Die gefaehrliche Richtung stand nur in der entfallenden Notiz. Ohne
205 // diesen Satz liest der Planer die verbleibende Warnung als Formsache.
206 const ohne = validateProject(ohneVerkehrsstaerken, undefined, DATUM);
207
208 const befund = ohne.findings.find(
209 (f) => f.rule === 'leistungsfaehigkeit.keine-verkehrsstaerken',
210 );
211 expect(befund?.message).toContain('zu kurz');
212 });
213
214 it('laesst die Plan-Notiz stehen, wenn nur einzelne Phasen fehlen', () => {
215 // Hier schweigt 'leistungsfaehigkeit.keine-verkehrsstaerken' - sie greift
216 // erst, wenn KEINE Signalgruppe eine Verkehrsstaerke traegt. Faellt die
217 // Plan-Notiz auch hier weg, verschweigt der Bericht die Luecke.
218 const bericht = validateProject(ohneVerkehrsstaerkenInPhase2(), undefined, DATUM);
219
220 expect(
221 bericht.findings.some((f) => f.rule === 'leistungsfaehigkeit.keine-verkehrsstaerken'),
222 ).toBe(false);
223 const befund = bericht.findings.find((f) => f.rule === `signalplan.${CODE}`);
224 expect(befund).toBeDefined();
225 expect(befund?.message).toContain('Phase 2');
226 });
227
228 it('laesst die Plan-Notiz an der einstreifigen Verkehrsfuehrung stehen', () => {
229 // Dort verlangt der Bericht bewusst keine Verkehrsstaerken
230 // (schweigtBeiAnlagenart, checkPerformance); die Plan-Notiz ist die einzige
231 // Stelle, an der die Luecke ueberhaupt vorkommt.
232 const bericht = validateProject(engstelleOhneVerkehrsstaerken(), undefined, DATUM);
233
234 expect(
235 bericht.findings.some((f) => f.rule === 'leistungsfaehigkeit.keine-verkehrsstaerken'),
236 ).toBe(false);
237 expect(bericht.findings.some((f) => f.rule === `signalplan.${CODE}`)).toBe(true);
238 });
239
240 it('nennt in der Ueberschrift nicht "einzelne" Phasen, wenn alle betroffen sind', () => {
241 // Die Meldung derselben Zeile nennt "Phase 1, Phase 2" - also alle.
242 const bericht = validateProject(engstelleOhneVerkehrsstaerken(), undefined, DATUM);
243
244 const befund = bericht.findings.find((f) => f.rule === `signalplan.${CODE}`);
245 expect(befund?.message).toContain('Phase 1');
246 expect(befund?.message).toContain('Phase 2');
247 expect(befund?.title).not.toMatch(/einzelne/i);
248 });
249 });