lsa-planer

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

/ tests domain oepnvZwischenzeit.test.ts

13,1 KB Rohdatei
tests/domain/oepnvZwischenzeit.test.ts — 325 Zeilen
1 import { describe, expect, it } from 'vitest';
2 import { computeIntergreen } from '@/domain/rilsa/intergreen';
3 import {
4 OEPNV_ANFAHRBESCHLEUNIGUNG,
5 RILSA_DEFAULTS,
6 SAFETY_FLOORS,
7 fahrzeugartBeschriftung,
8 type RilsaDefaults,
9 } from '@/domain/rilsa/constants';
10 import { resolveRilsaSettings, type RilsaOverrides } from '@/domain/rilsa/settings';
11 import type { ClearingStream, EnteringStream } from '@/domain/rilsa/types';
12 import { parseProject } from '@/domain/model/schema';
13 import {
14 createConflict,
15 createSignalGroup,
16 createStandardIntersectionProject,
17 } from '@/domain/model/factory';
18 import { CURRENT_SCHEMA_VERSION, type Project } from '@/domain/model/project';
19
20 /**
21 * Befund A2: OePNV-Zwischenzeitfall nach RiLSA 2015.
22 *
23 * Primaer belegt durch das FGSV-Aenderungsblatt vom 29.07.2015, das die
24 * Originalseiten 23-25 der RiLSA 2015 wiedergibt:
25 *
26 * Fall 3 (ohne Halt): "Die Ueberfahrzeit betraegt bei Strassenbahnen:
27 * tue = 3 s bei Vmax <= 30 km/h, tue = 5 s bei 30 km/h < Vmax <= 50 km/h,
28 * tue = 7 s bei 50 km/h < Vmax <= 70 km/h." - "vr = Vmax/3,6" -
29 * "lFZ = 15 m bei Strassenbahnen, lFZ = 6 m bei Linienbussen".
30 *
31 * Fall 4 (bei Halt vor dem Knotenpunkt): "Ueberfahrzeit: tue = 0 s";
32 * Anfahrbeschleunigung Strassenbahn a = 1,0 m/s2, Linienbus a = 1,2 m/s2;
33 * tue + tr = Wurzel aus (2*(s0+lFZ)/a), bei Erreichen von Vmax Restweg mit
34 * Vmax/3,6.
35 *
36 * Der Altbestand rechnete fest mit Ueberfahrzeit 0 s und vr = 10 m/s - die
37 * Zwischenzeit einer Bahn ohne Halt (Vmax 50, Raeumweg + Laenge 40 m) war
38 * damit rund 4 s zu kurz.
39 */
40
41 const FIXED_DATE = new Date('2026-01-01T00:00:00Z');
42
43 function bahnRaeumend(extra: Partial<ClearingStream> = {}): ClearingStream {
44 return {
45 mode: 'oepnv',
46 movement: 'geradeaus',
47 // 25 m Grundraeumweg + 15 m Fahrzeuglaenge = 40 m Raeumweg.
48 clearingDistance: 25,
49 vehicleClass: 'strassenbahn',
50 vZul: 50,
51 ...extra,
52 };
53 }
54
55 function kfzEinfahrend(extra: Partial<EnteringStream> = {}): EnteringStream {
56 return { mode: 'kfz', movement: 'geradeaus', enteringDistance: 8, ...extra };
57 }
58
59 describe('OePNV-Ueberfahrzeit: Vmax-Staffel (RiLSA Fall 3)', () => {
60 it('staffelt 3/5/7 s an den Grenzen 30, 50 und 70 km/h', () => {
61 const tue = (vZul: number): number =>
62 computeIntergreen(bahnRaeumend({ vZul }), kfzEinfahrend()).crossingTime;
63
64 expect(tue(20)).toBe(3);
65 expect(tue(30)).toBe(3); // "tue = 3 s bei Vmax <= 30 km/h" - einschliesslich
66 expect(tue(31)).toBe(5); // "30 km/h < Vmax"
67 expect(tue(50)).toBe(5);
68 expect(tue(51)).toBe(7);
69 expect(tue(70)).toBe(7);
70 });
71
72 it('rechnet das Zahlenbeispiel zu Befund A2: Bahn ohne Halt, Vmax 50, 40 m Raeumweg', () => {
73 // Befund A2: vor der Fassung 5.4.0 rechnete das Programm 0 + 40/10 = 4,0 s,
74 // die RiLSA 5 + 40/13,9 = 7,9 s (vor Einfahrzeitabzug, deshalb Einfahrweg 0).
75 const result = computeIntergreen(bahnRaeumend(), kfzEinfahrend({ enteringDistance: 0 }));
76
77 expect(result.vehicleLength).toBe(15);
78 expect(result.clearingPath).toBe(40);
79 expect(result.crossingTime).toBe(5);
80 expect(result.clearingSpeed).toBeCloseTo(50 / 3.6, 6);
81 expect(result.clearingTime).toBeCloseTo(2.88, 3);
82 expect(result.raw).toBeCloseTo(7.88, 3);
83 expect(result.intergreen).toBe(8);
84 expect(result.raeumansatz).toBe('geschwindigkeit');
85 expect(result.anfahrbeschleunigung).toBeNull();
86 });
87 });
88
89 describe('OePNV-Raeumgeschwindigkeit: Vmax/3,6 mit Schranken', () => {
90 it('raeumt mit Vmax/3,6 - auch oberhalb der alten Kfz-Schranke von 10 m/s', () => {
91 const result = computeIntergreen(bahnRaeumend({ vZul: 70 }), kfzEinfahrend());
92 expect(result.clearingSpeed).toBeCloseTo(70 / 3.6, 6);
93 expect(result.notes.some((n) => n.severity === 'fehler')).toBe(false);
94 });
95
96 it('faellt ohne Vmax konservativ auf den Rueckfallwert zurueck und warnt', () => {
97 const result = computeIntergreen(bahnRaeumend({ vZul: 0 }), kfzEinfahrend());
98
99 expect(result.clearingSpeed).toBe(RILSA_DEFAULTS.clearingSpeed.oepnv);
100 expect(result.crossingTime).toBe(7); // oberste Staffelstufe, sichere Seite
101 expect(
102 result.notes.some((n) => n.code === 'oepnv-vmax-fehlt' && n.severity === 'warnung'),
103 ).toBe(true);
104 });
105
106 it('nimmt ohne Vmax den KLEINEREN von Rueckfallwert und bisherigem Verhalten', () => {
107 // Ein niedrigerer konfigurierter Rueckfallwert bleibt stehen ...
108 const niedrig: RilsaDefaults = {
109 ...RILSA_DEFAULTS,
110 clearingSpeed: { ...RILSA_DEFAULTS.clearingSpeed, oepnv: 8 },
111 };
112 expect(
113 computeIntergreen(bahnRaeumend({ vZul: 0 }), kfzEinfahrend(), niedrig).clearingSpeed,
114 ).toBe(8);
115
116 // ... ein hoeherer wird auf die bisherigen 10 m/s gekappt (mit Fehler,
117 // siehe raeumgeschwindigkeitSchranken.test.ts).
118 const hoch: RilsaDefaults = {
119 ...RILSA_DEFAULTS,
120 clearingSpeed: { ...RILSA_DEFAULTS.clearingSpeed, oepnv: 15 },
121 };
122 expect(computeIntergreen(bahnRaeumend({ vZul: 0 }), kfzEinfahrend(), hoch).clearingSpeed).toBe(
123 SAFETY_FLOORS.maxClearingSpeedKfz,
124 );
125 });
126
127 it('begrenzt bei Vmax ueber 70 km/h auf die OePNV-Obergrenze und warnt', () => {
128 // VwV-StVO zu Par. 37 Rn. 10: oberhalb 70 km/h sollen LSA nicht betrieben
129 // werden - die Staffel der RiLSA endet dort aus demselben Grund.
130 const result = computeIntergreen(bahnRaeumend({ vZul: 100 }), kfzEinfahrend());
131
132 expect(result.clearingSpeed).toBeCloseTo(SAFETY_FLOORS.maxClearingSpeedOepnv, 6);
133 expect(result.crossingTime).toBe(7);
134 expect(
135 result.notes.some((n) => n.code === 'oepnv-vmax-ueber-70' && n.severity === 'warnung'),
136 ).toBe(true);
137 });
138
139 it('deckelt eine Ueberschreibung gegen die Vmax des Stroms - nicht schneller raeumen als erlaubt faehrt', () => {
140 // 15 m/s = 54 km/h. Bei Vmax 50 unzulaessig ...
141 const bei50 = computeIntergreen(
142 bahnRaeumend({ vZul: 50, clearingSpeedOverride: 15 }),
143 kfzEinfahrend(),
144 );
145 expect(bei50.clearingSpeed).toBeCloseTo(50 / 3.6, 6);
146 expect(
147 bei50.notes.some((n) => n.code === 'raeumgeschwindigkeit-zu-hoch' && n.severity === 'fehler'),
148 ).toBe(true);
149
150 // ... bei Vmax 70 zulaessig (unterhalb 70/3,6 = 19,4 m/s).
151 const bei70 = computeIntergreen(
152 bahnRaeumend({ vZul: 70, clearingSpeedOverride: 15 }),
153 kfzEinfahrend(),
154 );
155 expect(bei70.clearingSpeed).toBe(15);
156 expect(bei70.notes.some((n) => n.severity === 'fehler')).toBe(false);
157
158 // Die absolute Obergrenze gilt auch bei Vmax 70.
159 const zuHoch = computeIntergreen(
160 bahnRaeumend({ vZul: 70, clearingSpeedOverride: 25 }),
161 kfzEinfahrend(),
162 );
163 expect(zuHoch.clearingSpeed).toBeCloseTo(SAFETY_FLOORS.maxClearingSpeedOepnv, 6);
164 expect(
165 zuHoch.notes.some(
166 (n) => n.code === 'raeumgeschwindigkeit-zu-hoch' && n.severity === 'fehler',
167 ),
168 ).toBe(true);
169 });
170 });
171
172 describe('OePNV-Haltefall: Anfahransatz (RiLSA Fall 4)', () => {
173 it('rechnet die Strassenbahn mit tue = 0 s und tr = Wurzel(2*sr/a), a = 1,0 m/s2', () => {
174 const result = computeIntergreen(bahnRaeumend({ haltVorKnoten: true }), kfzEinfahrend());
175
176 expect(result.crossingTime).toBe(0);
177 expect(result.raeumansatz).toBe('anfahren');
178 expect(result.anfahrbeschleunigung).toBe(OEPNV_ANFAHRBESCHLEUNIGUNG.strassenbahn);
179 // sr = 40 m, a = 1,0: Wurzel(2*40/1,0) = 8,944 s. Vmax 50 km/h wird auf
180 // den 40 m nicht erreicht (Beschleunigungsweg 13,89^2/2 = 96,5 m).
181 expect(result.clearingTime).toBeCloseTo(Math.sqrt(80), 3);
182 // Zwischenzeit bleibt aus denselben Groessen nachrechenbar:
183 // 0 + 8,944 - 8/11,1 = 8,224 -> 9 s.
184 expect(result.raw).toBeCloseTo(8.944 - 8 / 11.1, 2);
185 expect(result.intergreen).toBe(9);
186 });
187
188 it('rechnet den Linienbus mit a = 1,2 m/s2', () => {
189 const result = computeIntergreen(
190 bahnRaeumend({ haltVorKnoten: true, vehicleClass: 'bus' }),
191 kfzEinfahrend(),
192 );
193
194 // Laengenzuschlag Linienbus 6 m (RiLSA-Rechenlaenge, Fall 3 woertlich;
195 // seit Befund B5 auch der Vorgabewert). sr = 25 + 6 = 31 m;
196 // Wurzel(2*31/1,2) = 7,188 s.
197 const srBus = 25 + RILSA_DEFAULTS.vehicleLength.bus;
198 expect(RILSA_DEFAULTS.vehicleLength.bus).toBe(6);
199 expect(result.anfahrbeschleunigung).toBe(OEPNV_ANFAHRBESCHLEUNIGUNG.bus);
200 expect(result.clearingPath).toBe(srBus);
201 expect(result.clearingTime).toBeCloseTo(Math.sqrt((2 * srBus) / 1.2), 3);
202 });
203
204 it('faehrt nach Erreichen von Vmax den Restweg mit Vmax/3,6', () => {
205 // Vmax 30 km/h = 8,333 m/s; Beschleunigungsweg 8,333^2/(2*1,0) = 34,72 m.
206 // sr = 85 + 15 = 100 m > 34,72 m -> zweiter Zweig der Formel:
207 // tr = 8,333/1,0 + (100 - 34,72)/8,333 = 16,167 s.
208 const result = computeIntergreen(
209 bahnRaeumend({ haltVorKnoten: true, vZul: 30, clearingDistance: 85 }),
210 kfzEinfahrend(),
211 );
212
213 const v = 30 / 3.6;
214 const erwartet = v / 1.0 + (100 - (v * v) / 2) / v;
215 expect(result.clearingTime).toBeCloseTo(erwartet, 3);
216 // Der Deckel wirkt: Die reine Wurzelformel ergaebe nur 14,14 s.
217 expect(result.clearingTime).toBeGreaterThan(Math.sqrt(200));
218 });
219
220 it('laesst den Halt bei anderen Verkehrsarten wirkungslos', () => {
221 const mitHalt = computeIntergreen(
222 {
223 mode: 'kfz',
224 movement: 'geradeaus',
225 clearingDistance: 22,
226 vehicleClass: 'pkw',
227 vZul: 50,
228 haltVorKnoten: true,
229 },
230 kfzEinfahrend(),
231 );
232 expect(mitHalt.raeumansatz).toBe('geschwindigkeit');
233 expect(mitHalt.crossingTime).toBe(3);
234 });
235 });
236
237 describe('Fahrzeugart Strassenbahn', () => {
238 it('setzt die RiLSA-Rechenlaenge 15 m an', () => {
239 expect(RILSA_DEFAULTS.vehicleLength.strassenbahn).toBe(15);
240 const result = computeIntergreen(bahnRaeumend(), kfzEinfahrend());
241 expect(result.vehicleLength).toBe(15);
242 expect(result.clearingPath).toBe(40);
243 });
244
245 it('beschriftet mit echtem Umlaut und dem tatsaechlichen Zuschlag', () => {
246 expect(fahrzeugartBeschriftung('strassenbahn')).toBe('Straßenbahn (15 m)');
247 });
248
249 it('laesst den Laengenzuschlag nicht unter die Rechenlaenge sinken', () => {
250 const { defaults, notes } = resolveRilsaSettings({ vehicleLengthStrassenbahn: 10 });
251 expect(defaults.vehicleLength.strassenbahn).toBe(SAFETY_FLOORS.minVehicleLengthStrassenbahn);
252 expect(
253 notes.some(
254 (n) =>
255 n.code === 'vorgabe-unterschritten:vehicleLengthStrassenbahn' && n.severity === 'fehler',
256 ),
257 ).toBe(true);
258 });
259
260 it('nimmt eine begruendete oertliche Verlaengerung an', () => {
261 const { defaults, deviations } = resolveRilsaSettings({ vehicleLengthStrassenbahn: 30 });
262 expect(defaults.vehicleLength.strassenbahn).toBe(30);
263 expect(deviations.some((d) => d.key === 'vehicleLengthStrassenbahn')).toBe(true);
264 });
265 });
266
267 describe('Veraltete Vorgabe "Ueberfahrzeit OePNV"', () => {
268 it('wird gemeldet statt stillschweigend verworfen', () => {
269 // Dateien bis Fassung 5.3.5 koennen den Schluessel tragen; er hat keine
270 // Wirkung mehr, weil die Ueberfahrzeit der Vmax-Staffel folgt.
271 const alt = { crossingTimeOepnv: 2 } as unknown as RilsaOverrides;
272 const { notes } = resolveRilsaSettings(alt);
273 expect(
274 notes.some(
275 (n) => n.code === 'vorgabe-veraltet:crossingTimeOepnv' && n.severity === 'hinweis',
276 ),
277 ).toBe(true);
278 });
279 });
280
281 describe('Projektdatei: strassenbahn und haltVorKnoten ueberleben den Hin- und Rueckweg', () => {
282 function projektMitBahn(): Project {
283 const base = createStandardIntersectionProject('ÖPNV-Rundreise', FIXED_DATE);
284 const bahn = { ...createSignalGroup({ name: 'B1', mode: 'oepnv', index: 6 }), id: 'sg-bahn' };
285 const ersteGruppe = base.signalGroups[0]!;
286 return {
287 ...base,
288 signalGroups: [...base.signalGroups, { ...bahn, vehicleClass: 'strassenbahn', vZul: 50 }],
289 conflicts: [
290 ...base.conflicts,
291 {
292 ...createConflict('sg-bahn', ersteGruppe.id),
293 id: 'cf-bahn',
294 clearingDistance: 25,
295 enteringDistance: 8,
296 haltVorKnoten: true,
297 },
298 ],
299 };
300 }
301
302 it('traegt beide Angaben unveraendert durch Datei und Einlesen', () => {
303 const original = projektMitBahn();
304 const { project, issues } = parseProject(JSON.parse(JSON.stringify(original)), FIXED_DATE);
305
306 // Seit Befund C1 (blindenzusatz) steht das Schema auf 11; hier zaehlt nur,
307 // dass die Datei den aktuellen Stand traegt.
308 expect(project.schemaVersion).toBe(CURRENT_SCHEMA_VERSION);
309 expect(project.signalGroups.find((g) => g.id === 'sg-bahn')?.vehicleClass).toBe('strassenbahn');
310 expect(project.conflicts.find((c) => c.id === 'cf-bahn')?.haltVorKnoten).toBe(true);
311 // Keine Reparaturmeldung: 'strassenbahn' ist eine bekannte Fahrzeugart und
312 // darf beim Einlesen nicht auf 'pkw' zurueckfallen.
313 expect(issues.filter((i) => i.severity !== 'hinweis')).toEqual([]);
314 });
315
316 it('erfindet fuer Bestandsbeziehungen keinen Haltvermerk', () => {
317 // Eine Datei ohne das Feld sagt "ohne Halt"; beim Einlesen darf daraus
318 // keine Angabe werden, die niemand gemacht hat.
319 const original = projektMitBahn();
320 const { project } = parseProject(JSON.parse(JSON.stringify(original)), FIXED_DATE);
321 const alteBeziehung = project.conflicts.find((c) => c.id !== 'cf-bahn');
322 expect(alteBeziehung).toBeDefined();
323 expect('haltVorKnoten' in (alteBeziehung as object)).toBe(false);
324 });
325 });