lsa-planer

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

/ tests domain intergreen.test.ts

11,3 KB Rohdatei
tests/domain/intergreen.test.ts — 280 Zeilen
1 import { describe, expect, it } from 'vitest';
2 import { computeIntergreen, clearingSpeedFor } from '@/domain/rilsa/intergreen';
3 import { RILSA_DEFAULTS, SAFETY_FLOORS } from '@/domain/rilsa/constants';
4 import type { ClearingStream, EnteringStream } from '@/domain/rilsa/types';
5
6 const kfzClearing = (overrides: Partial<ClearingStream> = {}): ClearingStream => ({
7 mode: 'kfz',
8 movement: 'geradeaus',
9 clearingDistance: 22,
10 vehicleClass: 'pkw',
11 vZul: 50,
12 ...overrides,
13 });
14
15 const kfzEntering = (overrides: Partial<EnteringStream> = {}): EnteringStream => ({
16 mode: 'kfz',
17 movement: 'geradeaus',
18 enteringDistance: 8,
19 ...overrides,
20 });
21
22 describe('Zwischenzeitberechnung nach RiLSA', () => {
23 it('rechnet tz = tue + tr - te und rundet nur das Endergebnis auf', () => {
24 const result = computeIntergreen(kfzClearing(), kfzEntering());
25
26 // sr = 22 m + 6 m Fahrzeuglaenge = 28 m, vr = 10 m/s -> tr = 2,8 s
27 expect(result.clearingPath).toBe(28);
28 expect(result.clearingSpeed).toBe(10);
29 expect(result.clearingTime).toBeCloseTo(2.8, 6);
30
31 // se = 8 m, ve = 11,1 m/s -> te = 0,7207 s
32 expect(result.enteringSpeed).toBe(11.1);
33 expect(result.enteringTime).toBeCloseTo(0.721, 3);
34
35 // tue = Gelbzeit bei 50 km/h = 3 s
36 expect(result.crossingTime).toBe(3);
37
38 // 3 + 2,8 - 0,7207 = 5,0793 -> 6 s
39 expect(result.raw).toBeCloseTo(5.079, 3);
40 expect(result.intergreen).toBe(6);
41 });
42
43 it('rundet die Teilzeiten NICHT einzeln auf', () => {
44 // Der Altbestand rundete tr und te einzeln auf: ceil(2,8) - ceil(0,72) = 3 - 1 = 2,
45 // zusammen mit tue = 3 also 5 s statt der richtigen 6 s.
46 const result = computeIntergreen(kfzClearing(), kfzEntering());
47 expect(result.clearingTime).not.toBe(Math.ceil(result.clearingTime));
48 expect(result.enteringTime).not.toBe(Math.ceil(result.enteringTime));
49 expect(result.intergreen).toBe(6);
50 });
51
52 it('setzt fuer abbiegende Kraftfahrzeuge die geringere Raeumgeschwindigkeit an', () => {
53 const straight = computeIntergreen(kfzClearing({ movement: 'geradeaus' }), kfzEntering());
54 const turning = computeIntergreen(kfzClearing({ movement: 'links' }), kfzEntering());
55
56 expect(straight.clearingSpeed).toBe(RILSA_DEFAULTS.clearingSpeed.kfzStraight);
57 expect(turning.clearingSpeed).toBe(RILSA_DEFAULTS.clearingSpeed.kfzTurning);
58 // Langsameres Raeumen ergibt eine laengere - also sicherere - Zwischenzeit.
59 expect(turning.intergreen).toBeGreaterThan(straight.intergreen);
60 });
61
62 it('rechnet Fussgaenger in m/s und ohne Ueberfahrzeit', () => {
63 const result = computeIntergreen(
64 {
65 mode: 'fuss',
66 movement: 'querung',
67 clearingDistance: 12,
68 vehicleClass: 'keine',
69 vZul: 0,
70 },
71 kfzEntering(),
72 );
73
74 // Der Altbestand teilte die in m/s hinterlegten 1,2 nochmals durch 3,6.
75 expect(result.clearingSpeed).toBe(1.2);
76 expect(result.clearingTime).toBeCloseTo(10, 6);
77 expect(result.crossingTime).toBe(0);
78 expect(result.intergreen).toBe(10);
79 });
80
81 it('beruecksichtigt erhoehten Zeitbedarf bei Fussgaengerfurten', () => {
82 const normal = computeIntergreen(
83 { mode: 'fuss', movement: 'querung', clearingDistance: 12, vehicleClass: 'keine', vZul: 0 },
84 kfzEntering(),
85 );
86 const reduced = computeIntergreen(
87 {
88 mode: 'fuss',
89 movement: 'querung',
90 clearingDistance: 12,
91 vehicleClass: 'keine',
92 vZul: 0,
93 reducedMobility: true,
94 },
95 kfzEntering(),
96 );
97
98 expect(reduced.clearingSpeed).toBe(1.0);
99 expect(reduced.intergreen).toBeGreaterThan(normal.intergreen);
100 });
101
102 it('staffelt die Ueberfahrzeit von Kraftfahrzeugen nach der Hoechstgeschwindigkeit', () => {
103 expect(computeIntergreen(kfzClearing({ vZul: 50 }), kfzEntering()).crossingTime).toBe(3);
104 expect(computeIntergreen(kfzClearing({ vZul: 60 }), kfzEntering()).crossingTime).toBe(4);
105 expect(computeIntergreen(kfzClearing({ vZul: 70 }), kfzEntering()).crossingTime).toBe(5);
106 });
107
108 it('beruecksichtigt den Laengenzuschlag der Fahrzeugart', () => {
109 // RiLSA-Rechenlaengen (Befund B5): 6 m fuer alle Kraftfahrzeuge
110 // einschliesslich Lastzug - hier standen 18,75 m (Par. 32 StVZO). Eine
111 // Strassenbahn raeumt mit 15 m.
112 const pkw = computeIntergreen(kfzClearing({ vehicleClass: 'pkw' }), kfzEntering());
113 const lastzug = computeIntergreen(kfzClearing({ vehicleClass: 'lastzug' }), kfzEntering());
114 const bahn = computeIntergreen(kfzClearing({ vehicleClass: 'strassenbahn' }), kfzEntering());
115
116 expect(pkw.vehicleLength).toBe(6);
117 expect(lastzug.vehicleLength).toBe(6);
118 expect(lastzug.clearingPath).toBe(22 + 6);
119 expect(lastzug.intergreen).toBe(pkw.intergreen);
120 expect(bahn.vehicleLength).toBe(15);
121 expect(bahn.clearingPath).toBe(22 + 15);
122 expect(bahn.clearingTime).toBeGreaterThan(pkw.clearingTime);
123 // Wer laenger ansetzen will, tut das ueber die Vorgaben - als Verschaerfung.
124 const verschaerft = computeIntergreen(kfzClearing({ vehicleClass: 'lastzug' }), kfzEntering(), {
125 ...RILSA_DEFAULTS,
126 vehicleLength: { ...RILSA_DEFAULTS.vehicleLength, lastzug: 18.75 },
127 });
128 expect(verschaerft.vehicleLength).toBe(18.75);
129 expect(verschaerft.intergreen).toBeGreaterThan(pkw.intergreen);
130 });
131
132 it('setzt eine rechnerisch negative Zwischenzeit auf 0 s und meldet das', () => {
133 const result = computeIntergreen(
134 { mode: 'fuss', movement: 'querung', clearingDistance: 0, vehicleClass: 'keine', vZul: 0 },
135 kfzEntering({ enteringDistance: 30 }),
136 );
137
138 expect(result.raw).toBeLessThan(0);
139 expect(result.intergreen).toBe(0);
140 expect(result.clampedToZero).toBe(true);
141 expect(result.notes.some((n) => n.code === 'zwischenzeit-negativ')).toBe(true);
142 });
143
144 it('begrenzt eine zu hoch vorgegebene Raeumgeschwindigkeit', () => {
145 const result = computeIntergreen(kfzClearing({ clearingSpeedOverride: 25 }), kfzEntering());
146
147 expect(result.clearingSpeed).toBe(SAFETY_FLOORS.maxClearingSpeedKfz);
148 expect(
149 result.notes.some(
150 (n) => n.code === 'raeumgeschwindigkeit-zu-hoch' && n.severity === 'fehler',
151 ),
152 ).toBe(true);
153 });
154
155 it('laesst eine niedrigere Raeumgeschwindigkeit zu, weil sie auf der sicheren Seite liegt', () => {
156 const result = computeIntergreen(kfzClearing({ clearingSpeedOverride: 6 }), kfzEntering());
157 expect(result.clearingSpeed).toBe(6);
158 expect(result.notes.some((n) => n.severity === 'fehler')).toBe(false);
159 });
160
161 it('warnt, wenn die Einfahrgeschwindigkeit unter dem Regelwert liegt', () => {
162 const result = computeIntergreen(kfzClearing(), kfzEntering({ enteringSpeedOverride: 5 }));
163 expect(result.enteringSpeed).toBe(5);
164 expect(result.notes.some((n) => n.code === 'einfahrgeschwindigkeit-unter-regelwert')).toBe(
165 true,
166 );
167 });
168
169 /*
170 * Fuer Kraftfahrzeuge IST die Ueberfahrzeit die Gelbzeit. Sie war das einzige
171 * Vorgabefeld der Zwischenzeitenansicht ohne jede Schranke: Eine eingetragene
172 * 0 senkte die Zwischenzeit von 6 s auf 3 s und hebelte damit
173 * SAFETY_FLOORS.yellowKfz ueber ein Eingabefeld aus - ohne eine einzige
174 * Meldung.
175 */
176 it('kappt eine Ueberfahrzeit unter der Gelbzeitschranke und meldet das', () => {
177 const ohne = computeIntergreen(kfzClearing(), kfzEntering());
178 const mitNull = computeIntergreen(kfzClearing({ crossingTimeOverride: 0 }), kfzEntering());
179
180 expect(ohne.crossingTime).toBe(3);
181 expect(ohne.intergreen).toBe(6);
182
183 // Angesetzt wird die Schranke, nicht die 0.
184 expect(mitNull.crossingTime).toBe(SAFETY_FLOORS.yellowKfz);
185 expect(mitNull.intergreen).toBe(ohne.intergreen);
186 expect(
187 mitNull.notes.some(
188 (n) => n.code === 'ueberfahrzeit-unter-gelbzeitschranke' && n.severity === 'fehler',
189 ),
190 ).toBe(true);
191 });
192
193 /*
194 * Massgebend ist die Gelbzeit DIESER Signalgruppe, nicht die kuerzeste
195 * ueberhaupt zulaessige. Waere nur gegen SAFETY_FLOORS.yellowKfz geprueft,
196 * bliebe die Zwischenzeit bei 70 km/h um 2 s zu kurz - und der Anwender
197 * koennte den Fehler durch Eintragen ebendieser 3 s in eine blosse Warnung
198 * verwandeln, ohne dass sich am Ergebnis etwas aendert.
199 */
200 it('kappt fuer Kfz auf die Gelbzeit der Signalgruppe, nicht auf die unterste Schranke', () => {
201 const regel = computeIntergreen(kfzClearing({ vZul: 70 }), kfzEntering());
202 expect(regel.crossingTime).toBe(5);
203
204 for (const vorgabe of [0, SAFETY_FLOORS.yellowKfz, 4]) {
205 const gekuerzt = computeIntergreen(
206 kfzClearing({ vZul: 70, crossingTimeOverride: vorgabe }),
207 kfzEntering(),
208 );
209 expect(gekuerzt.crossingTime, `Vorgabe ${vorgabe}`).toBe(5);
210 expect(gekuerzt.intergreen, `Vorgabe ${vorgabe}`).toBe(regel.intergreen);
211 expect(
212 gekuerzt.notes.some(
213 (n) => n.code === 'ueberfahrzeit-unter-gelbzeitschranke' && n.severity === 'fehler',
214 ),
215 `Vorgabe ${vorgabe}`,
216 ).toBe(true);
217 }
218 });
219
220 it('nennt in der Meldung die Gelbzeit, die tatsaechlich zu zeigen ist', () => {
221 const gekuerzt = computeIntergreen(
222 kfzClearing({ vZul: 70, crossingTimeOverride: 0 }),
223 kfzEntering(),
224 );
225 const meldung = gekuerzt.notes.find((n) => n.code === 'ueberfahrzeit-unter-gelbzeitschranke');
226 // Ohne die richtige Zahl im Text traegt der Anwender die falsche ein.
227 expect(meldung?.message).toContain('5 s');
228 expect(meldung?.message).toContain('70 km/h');
229 });
230
231 it('warnt beim Radverkehr nur, weil es dort keine Gelbzeit gibt', () => {
232 const radRaeumend: ClearingStream = {
233 mode: 'rad',
234 movement: 'geradeaus',
235 clearingDistance: 20,
236 vehicleClass: 'rad',
237 vZul: 25,
238 };
239 const regel = computeIntergreen(radRaeumend, kfzEntering());
240 const gekuerzt = computeIntergreen({ ...radRaeumend, crossingTimeOverride: 0 }, kfzEntering());
241
242 expect(gekuerzt.crossingTime).toBe(0);
243 expect(gekuerzt.crossingTime).toBeLessThan(regel.crossingTime);
244 expect(
245 gekuerzt.notes.some(
246 (n) => n.code === 'ueberfahrzeit-unter-regelwert' && n.severity === 'warnung',
247 ),
248 ).toBe(true);
249 expect(gekuerzt.notes.some((n) => n.severity === 'fehler')).toBe(false);
250 });
251
252 it('laesst eine laengere Ueberfahrzeit ohne Meldung zu, weil sie sicher liegt', () => {
253 const result = computeIntergreen(kfzClearing({ crossingTimeOverride: 5 }), kfzEntering());
254 expect(result.crossingTime).toBe(5);
255 expect(result.notes.some((n) => n.code?.startsWith('ueberfahrzeit'))).toBe(false);
256 });
257
258 it('faengt fehlende und negative Wege ab, statt mit NaN weiterzurechnen', () => {
259 const missing = computeIntergreen(
260 kfzClearing({ clearingDistance: Number.NaN }),
261 kfzEntering({ enteringDistance: Number.NaN }),
262 );
263 expect(Number.isFinite(missing.intergreen)).toBe(true);
264 expect(missing.notes.some((n) => n.code === 'raeumweg-fehlt')).toBe(true);
265
266 const negative = computeIntergreen(kfzClearing({ clearingDistance: -22 }), kfzEntering());
267 expect(negative.clearingPath).toBe(28);
268 expect(negative.notes.some((n) => n.code === 'raeumweg-negativ')).toBe(true);
269 });
270
271 it('liefert fuer jede Verkehrsart eine definierte Raeumgeschwindigkeit', () => {
272 for (const mode of ['kfz', 'rad', 'fuss', 'oepnv'] as const) {
273 for (const movement of ['geradeaus', 'rechts', 'links', 'querung'] as const) {
274 const speed = clearingSpeedFor({ mode, movement });
275 expect(Number.isFinite(speed)).toBe(true);
276 expect(speed).toBeGreaterThan(0);
277 }
278 }
279 });
280 });