lsa-planer

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

/ tests domain laengenzuschlag.test.ts

6,4 KB Rohdatei
tests/domain/laengenzuschlag.test.ts — 163 Zeilen
1 import { describe, expect, it } from 'vitest';
2 import { computeIntergreen } from '@/domain/rilsa/intergreen';
3 import { RILSA_DEFAULTS, SAFETY_FLOORS } from '@/domain/rilsa/constants';
4 import { buildSignalPlan, type PlanNote } from '@/domain/plan/signalPlan';
5 import { validateProject } from '@/domain/validation/engine';
6 import {
7 createConflict,
8 createEmptyProject,
9 createPhase,
10 createSignalGroup,
11 } from '@/domain/model/factory';
12 import type { Project, SignalGroup } from '@/domain/model/project';
13 import type {
14 ClearingStream,
15 EnteringStream,
16 TrafficMode,
17 VehicleClass,
18 } from '@/domain/rilsa/types';
19
20 /*
21 * Laengenzuschlag unter der harten Schranke (Fassung 5.9.0).
22 *
23 * Die Behebung von Befund 4 hat die Auswahlliste der Oberflaeche geschlossen: An
24 * einem Kfz- oder OePNV-Strom ist keine Fahrzeugart mehr waehlbar, deren
25 * Laengenzuschlag unter SAFETY_FLOORS.minVehicleLengthKfz liegt. Der Fachkern
26 * zog nicht nach - `resolveVehicleLength` nahm den hinterlegten Wert, auch
27 * wenn er 0 m betrug, und die Zwischenzeit fiel um rund eine Sekunde kuerzer
28 * aus: die gefaehrliche Richtung, ohne eine einzige Meldung. Die Notiz
29 * `fahrzeugart-unbekannt` greift nur bei einer UNBEKANNTEN Art; 0 m ist ein
30 * hinterlegter Wert.
31 */
32
33 const CODE = 'fahrzeugart-laengenzuschlag-zu-kurz';
34 const DATUM = new Date('2026-01-01T00:00:00Z');
35
36 const raeumend = (
37 mode: TrafficMode,
38 vehicleClass: VehicleClass,
39 overrides: Partial<ClearingStream> = {},
40 ): ClearingStream => ({
41 mode,
42 movement: 'geradeaus',
43 clearingDistance: 22,
44 vehicleClass,
45 vZul: 50,
46 ...overrides,
47 });
48
49 const einfahrend: EnteringStream = {
50 mode: 'kfz',
51 movement: 'geradeaus',
52 enteringDistance: 8,
53 };
54
55 /** Zwei Gruppen in zwei Phasen; die raeumende Gruppe laesst sich anpassen. */
56 function zweiGruppen(raeumendeGruppe: Partial<SignalGroup>): Project {
57 const basis = createEmptyProject('Längenzuschlag', DATUM);
58 const a: SignalGroup = {
59 ...createSignalGroup({ name: 'K1', mode: 'kfz', index: 0 }),
60 ...raeumendeGruppe,
61 };
62 const b = createSignalGroup({ name: 'K2', mode: 'kfz', index: 1 });
63 const phase1 = createPhase('Phase 1', [a.id]);
64 const phase2 = createPhase('Phase 2', [b.id]);
65 return {
66 ...basis,
67 signalGroups: [a, b],
68 conflicts: [
69 { ...createConflict(a.id, b.id), clearingDistance: 22, enteringDistance: 8 },
70 { ...createConflict(b.id, a.id), clearingDistance: 22, enteringDistance: 8 },
71 ],
72 phases: [phase1, phase2],
73 program: { ...basis.program, phaseOrder: [phase1.id, phase2.id] },
74 };
75 }
76
77 function notizen(project: Project, code: string): PlanNote[] {
78 return buildSignalPlan(project).notes.filter((n) => n.code === code);
79 }
80
81 describe('Zu kurzer Laengenzuschlag am Kfz-Strom wird gemeldet und ersetzt', () => {
82 it('setzt bei Fahrzeugart "keine" die RiLSA-Rechenlaenge an statt 0 m', () => {
83 const result = computeIntergreen(raeumend('kfz', 'keine'), einfahrend);
84 expect(RILSA_DEFAULTS.vehicleLength.keine).toBe(0);
85 expect(result.vehicleLength).toBe(SAFETY_FLOORS.minVehicleLengthKfz);
86 expect(result.clearingPath).toBe(28);
87 });
88
89 it('meldet die Ersetzung als Fehler mit eigenem Notizcode', () => {
90 const result = computeIntergreen(raeumend('kfz', 'keine'), einfahrend);
91 const notiz = result.notes.find((n) => n.code === CODE);
92 expect(notiz).toBeDefined();
93 expect(notiz!.severity).toBe('fehler');
94 expect(notiz!.message).toContain('6 m');
95 // Die Notiz nennt die Richtung: Der kuerzere Zuschlag verkuerzt.
96 expect(notiz!.message).toContain('verkürz');
97 });
98
99 it('rechnet damit dieselbe Zwischenzeit wie ein Pkw - nicht eine Sekunde kuerzer', () => {
100 // Altstand: 0 m -> sr = 22 m -> tr = 2,2 s -> 3 + 2,2 - 0,72 = 4,48 s -> 5 s.
101 // Jetzt: 6 m -> sr = 28 m -> tr = 2,8 s -> 5,08 s -> 6 s, wie beim Pkw.
102 const pkw = computeIntergreen(raeumend('kfz', 'pkw'), einfahrend);
103 const keine = computeIntergreen(raeumend('kfz', 'keine'), einfahrend);
104 expect(pkw.intergreen).toBe(6);
105 expect(keine.intergreen).toBe(pkw.intergreen);
106 expect(keine.clearingTime).toBeCloseTo(pkw.clearingTime, 6);
107 });
108
109 it('gilt ebenso am OePNV-Strom', () => {
110 const result = computeIntergreen(raeumend('oepnv', 'rad'), einfahrend);
111 expect(RILSA_DEFAULTS.vehicleLength.rad).toBeLessThan(SAFETY_FLOORS.minVehicleLengthKfz);
112 expect(result.vehicleLength).toBe(SAFETY_FLOORS.minVehicleLengthKfz);
113 expect(result.notes.some((n) => n.code === CODE)).toBe(true);
114 });
115
116 it('laesst Fussgaenger- und Radstroeme unberuehrt - dort ist ein kurzer Zuschlag zulaessig', () => {
117 for (const mode of ['fuss', 'rad'] as const) {
118 const result = computeIntergreen(
119 raeumend(mode, 'keine', { movement: mode === 'fuss' ? 'querung' : 'geradeaus' }),
120 einfahrend,
121 );
122 expect(result.vehicleLength, mode).toBe(0);
123 expect(
124 result.notes.some((n) => n.code === CODE),
125 mode,
126 ).toBe(false);
127 }
128 });
129
130 it('meldet die Arten mit ausreichendem Zuschlag nicht', () => {
131 for (const art of ['pkw', 'lkw', 'bus', 'lastzug', 'strassenbahn'] as const) {
132 const result = computeIntergreen(raeumend('kfz', art), einfahrend);
133 expect(
134 result.notes.some((n) => n.code === CODE),
135 art,
136 ).toBe(false);
137 expect(result.vehicleLength, art).toBe(RILSA_DEFAULTS.vehicleLength[art]);
138 }
139 });
140
141 it('haelt die Notiz von der unbekannten Fahrzeugart getrennt', () => {
142 const unbekannt = computeIntergreen(raeumend('kfz', 'traktor' as VehicleClass), einfahrend);
143 expect(unbekannt.notes.some((n) => n.code === 'fahrzeugart-unbekannt')).toBe(true);
144 expect(unbekannt.notes.some((n) => n.code === CODE)).toBe(false);
145 });
146
147 it('traegt die Meldung in den Plan und sperrt als Fehler den Export', () => {
148 const projekt = zweiGruppen({ vehicleClass: 'keine' });
149 const [notiz] = notizen(projekt, CODE);
150 expect(notiz).toBeDefined();
151 expect(notiz!.severity).toBe('fehler');
152 expect(notiz!.konfliktId).toBeDefined();
153
154 const bericht = validateProject(projekt, undefined, DATUM);
155 const befund = bericht.findings.find((f) => f.rule === `signalplan.${CODE}`);
156 expect(befund).toBeDefined();
157 expect(befund!.severity).toBe('fehler');
158 // Der Praefix `fahrzeugart` trifft in mapNoteCategory bereits das
159 // Sachgebiet; Ueberschrift und naechster Schritt traegt engine.ts nach.
160 expect(befund!.category).toBe('zwischenzeiten');
161 expect(bericht.exportBlocked).toBe(true);
162 });
163 });