lsa-planer

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

/ tests ui assistentAnlagenformen.test.ts

8,3 KB Rohdatei
tests/ui/assistentAnlagenformen.test.ts — 191 Zeilen
1 import { describe, expect, it } from 'vitest';
2 import { isGreenAt } from '@/domain/plan/aspects';
3 import { buildSignalPlan } from '@/domain/plan/signalPlan';
4 import { ANLAGENART_GRENZEN } from '@/domain/rilsa/constants';
5 import { validateProject } from '@/domain/validation';
6 import { ASSISTENT_VORGABE, engstellenFolgen, projektAusAngaben } from '@/ui/assistent';
7 import type { AssistentAngaben } from '@/ui/assistent';
8 import type { Project } from '@/domain/model/project';
9
10 /**
11 * Die neuen Anlagenformen des gefuehrten Einstiegs.
12 *
13 * Geprueft wird nicht die Oberflaeche, sondern das Erzeugnis: Ein Assistent,
14 * der eine Baustellenampel anbietet und daraus einen nicht schaltbaren Plan
15 * macht, waere schlimmer als einer, der sie gar nicht anbietet - der Anwender
16 * haette dann eine Planung, der er ansieht, dass sie fertig ist.
17 */
18
19 const FESTES_DATUM = new Date('2026-01-01T00:00:00Z');
20
21 function angaben(teil: Partial<AssistentAngaben>): AssistentAngaben {
22 return { ...ASSISTENT_VORGABE, ...teil };
23 }
24
25 function baue(teil: Partial<AssistentAngaben>): Project {
26 return projektAusAngaben(angaben(teil), 'Pruefung', FESTES_DATUM);
27 }
28
29 /** Keine feindliche Signalgruppe darf gleichzeitig Freigabe zeigen. */
30 function pruefeVertraeglichkeit(projekt: Project): void {
31 const plan = buildSignalPlan(projekt);
32 expect(plan.feasible).toBe(true);
33 for (const konflikt of projekt.conflicts) {
34 const a = plan.groups.find((g) => g.groupId === konflikt.fromId)!;
35 const b = plan.groups.find((g) => g.groupId === konflikt.toId)!;
36 for (let t = 0; t < plan.cycleTime; t += 0.5) {
37 expect(
38 isGreenAt(a, t, plan.cycleTime) && isGreenAt(b, t, plan.cycleTime),
39 `${a.name} und ${b.name} bei t=${t}s`,
40 ).toBe(false);
41 }
42 }
43 }
44
45 /** Fachfehler ohne die Projektstammdaten, die der Assistent nicht kennt. */
46 function fachfehler(projekt: Project): readonly string[] {
47 const plan = buildSignalPlan(projekt);
48 return validateProject(projekt, plan, FESTES_DATUM)
49 .findings.filter((f) => f.severity === 'fehler' && f.category !== 'projektdaten')
50 .map((f) => `${f.rule}: ${f.message}`);
51 }
52
53 describe('Anlagenart des erzeugten Projekts', () => {
54 it('traegt die gewaehlte Art ein', () => {
55 expect(baue({ form: 'kreuzung' }).anlagenart).toBe('knotenpunkt');
56 expect(baue({ form: 'einmuendung' }).anlagenart).toBe('knotenpunkt');
57 expect(baue({ form: 'fuenfarmig' }).anlagenart).toBe('knotenpunkt');
58 expect(baue({ form: 'fussgaengerschutzanlage' }).anlagenart).toBe('fussgaengerschutzanlage');
59 expect(baue({ form: 'einstreifig' }).anlagenart).toBe('einstreifig');
60 });
61 });
62
63 describe('Fuenfarmiger Knotenpunkt', () => {
64 it('legt fuenf Zufahrten und drei Phasen an', () => {
65 const projekt = baue({ form: 'fuenfarmig' });
66 expect(projekt.intersection.arms).toHaveLength(5);
67 expect(projekt.phases).toHaveLength(3);
68 expect(projekt.program.phaseOrder).toHaveLength(3);
69 // Jede Phase fuehrt Signalgruppen - eine leere Phase waere ein Umlauf, in
70 // dem nichts faehrt.
71 for (const phase of projekt.phases) expect(phase.signalGroupIds.length).toBeGreaterThan(0);
72 });
73
74 it('gibt den schraegen Arm nie mit einer der Hauptachsen frei', () => {
75 pruefeVertraeglichkeit(baue({ form: 'fuenfarmig', rad: true, linksabbieger: true }));
76 });
77
78 it('bleibt ohne Fachfehler rechenbar', () => {
79 expect(fachfehler(baue({ form: 'fuenfarmig' }))).toHaveLength(0);
80 });
81 });
82
83 describe('Fussgaengerschutzanlage', () => {
84 it('legt zwei Signalgruppen, zwei Phasen und beide Konfliktrichtungen an', () => {
85 const projekt = baue({ form: 'fussgaengerschutzanlage' });
86 expect(projekt.signalGroups.map((g) => g.mode)).toEqual(['kfz', 'fuss']);
87 expect(projekt.phases).toHaveLength(2);
88 expect(projekt.conflicts).toHaveLength(2);
89 // Gerichtet und in beiden Richtungen erfasst.
90 const [hin, zurueck] = projekt.conflicts;
91 expect(hin!.fromId).toBe(zurueck!.toId);
92 expect(hin!.toId).toBe(zurueck!.fromId);
93 });
94
95 it('bildet den Raeumweg aus der Fahrbahnbreite', () => {
96 const projekt = baue({ form: 'fussgaengerschutzanlage', fahrbahnbreite: 9 });
97 const furt = projekt.signalGroups.find((g) => g.mode === 'fuss')!;
98 const raeumtFurt = projekt.conflicts.find((c) => c.fromId === furt.id)!;
99 // Der Fussgaenger raeumt genau die Fahrbahnbreite.
100 expect(raeumtFurt.clearingDistance).toBe(9);
101 // Das Fahrzeug raeumt darueber hinaus bis hinter die Furt und faehrt von
102 // der Haltlinie aus ein - der Einfahrweg des Fussgaengers ist dagegen 0.
103 const raeumtKfz = projekt.conflicts.find((c) => c.toId === furt.id)!;
104 expect(raeumtKfz.clearingDistance).toBeGreaterThan(9);
105 expect(raeumtKfz.enteringDistance).toBe(0);
106 expect(raeumtFurt.enteringDistance).toBeGreaterThan(0);
107 });
108
109 it('erbt den Wegnachweis "unbestimmt" - der Assistent vermisst nichts', () => {
110 const projekt = baue({ form: 'fussgaengerschutzanlage' });
111 for (const konflikt of projekt.conflicts) {
112 expect(konflikt.wegnachweis.herkunft).toBe('unbestimmt');
113 }
114 });
115
116 it('bleibt ohne Fachfehler schaltbar', () => {
117 const projekt = baue({ form: 'fussgaengerschutzanlage' });
118 pruefeVertraeglichkeit(projekt);
119 expect(fachfehler(projekt)).toHaveLength(0);
120 });
121 });
122
123 describe('Einstreifige Verkehrsfuehrung', () => {
124 it('macht die Laenge der Engstelle zum Raeumweg beider Richtungen', () => {
125 const projekt = baue({ form: 'einstreifig', engstellenlaenge: 400 });
126 expect(projekt.signalGroups).toHaveLength(2);
127 expect(projekt.conflicts).toHaveLength(2);
128 for (const konflikt of projekt.conflicts) {
129 expect(konflikt.clearingDistance).toBe(400);
130 // Das entgegenkommende Fahrzeug steht an der Haltlinie und ist mit dem
131 // ersten Meter im Konfliktbereich.
132 expect(konflikt.enteringDistance).toBe(0);
133 }
134 });
135
136 it('ist auch bei 400 m Engstelle schaltbar - dafuer gibt es die Anlagenart', () => {
137 const projekt = baue({ form: 'einstreifig', engstellenlaenge: 400 });
138 const plan = buildSignalPlan(projekt);
139 expect(plan.feasible).toBe(true);
140 // Genau der Umlauf, an dem die Knotenpunktschranke von 120 s zerbrach.
141 expect(plan.cycleTime).toBeGreaterThan(ANLAGENART_GRENZEN.knotenpunkt.cycleTime.max);
142 expect(plan.cycleTime).toBeLessThanOrEqual(ANLAGENART_GRENZEN.einstreifig.cycleTime.max);
143 pruefeVertraeglichkeit(projekt);
144 expect(fachfehler(projekt)).toHaveLength(0);
145 });
146
147 it('haelt bei kurzer Engstelle einen kurzen Umlauf', () => {
148 const projekt = baue({ form: 'einstreifig', engstellenlaenge: 120 });
149 expect(fachfehler(projekt)).toHaveLength(0);
150 pruefeVertraeglichkeit(projekt);
151 });
152 });
153
154 describe('Folgen der Engstellenlaenge', () => {
155 it('nennt dieselbe Zwischenzeit, die anschliessend im Plan steht', () => {
156 const folgen = engstellenFolgen(400, 50);
157 // Seit 5.27.0 nach RiLSA 2015, Abschnitt 5.2.2: 4 s Ueberfahrzeit + 400 m
158 // bei 40 km/h = 40 s. Bis 5.26.0: 400 m + 6 m Pkw bei 10 m/s
159 // Raeumgeschwindigkeit, dazu 3 s Gelbzeit - 44 s.
160 expect(folgen.zwischenzeit).toBe(40);
161
162 const plan = buildSignalPlan(baue({ form: 'einstreifig', engstellenlaenge: 400 }));
163 for (const uebergang of plan.transitions) {
164 expect(uebergang.duration).toBe(folgen.zwischenzeit);
165 }
166 });
167
168 it('waechst mit der Laenge und warnt vor unzumutbarer Wartezeit', () => {
169 const kurz = engstellenFolgen(120, 50);
170 const lang = engstellenFolgen(ANLAGENART_GRENZEN.einstreifig.maxClearingDistance, 50);
171
172 expect(lang.zwischenzeit).toBeGreaterThan(kurz.zwischenzeit);
173 expect(lang.wartezeit).toBeGreaterThan(kurz.wartezeit);
174 // Die Wartezeit ist der Umlauf abzueglich der eigenen Freigabezeit.
175 expect(kurz.wartezeit).toBeLessThan(kurz.umlaufzeit);
176
177 expect(kurz.stufe).toBe('hinweis');
178 expect(lang.stufe).toBe('warnung');
179 expect(lang.wartezeit).toBeGreaterThanOrEqual(ANLAGENART_GRENZEN.einstreifig.warteZeitWarnung);
180 });
181
182 it('haelt die groesste eintragbare Engstelle unter der Fehlerschwelle', () => {
183 // Die Obergrenze des Raeumwegs ist so gewaehlt, dass die Anlage bei
184 // kuerzest zulaessiger Freigabezeit noch schaltbar bleibt. Laeuft eine der
185 // beiden Zahlen im Regelwerk davon, faellt es hier auf.
186 const grenzen = ANLAGENART_GRENZEN.einstreifig;
187 const folgen = engstellenFolgen(grenzen.maxClearingDistance, 50);
188 expect(folgen.umlaufzeit).toBeLessThanOrEqual(grenzen.cycleTime.max);
189 expect(folgen.wartezeit).toBeLessThan(grenzen.warteZeitFehler);
190 });
191 });