lsa-planer

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

/ tests ui linksabbiegerPhase.test.ts

8,4 KB Rohdatei
tests/ui/linksabbiegerPhase.test.ts — 183 Zeilen
1 import { describe, expect, it } from 'vitest';
2 import { isGreenAt } from '@/domain/plan/aspects';
3 import { buildSignalPlan } from '@/domain/plan/signalPlan';
4 import { validateProject } from '@/domain/validation';
5 import { ASSISTENT_VORGABE, assistentZusammenfassung, projektAusAngaben } from '@/ui/assistent';
6 import type { AssistentAngaben } from '@/ui/assistent';
7 import type { Project, SignalGroup } from '@/domain/model/project';
8
9 /**
10 * Befund 2: das Kaestchen "Eigene Freigabe fuer Linksabbieger" des gefuehrten
11 * Einstiegs.
12 *
13 * Der Assistent legte die neue Linksabbiegergruppe in DIESELBE Phase wie den
14 * Geradeausstrom ihrer eigenen Zufahrt. Die Konfliktschleife ueberspringt jedes
15 * Paar derselben Phase, und damit stand der Linksabbieger aus Nord gegenueber
16 * dem geradeausfahrenden Gegenverkehr aus Sued als vertraeglich im Plan: keine
17 * Konfliktbeziehung, keine Zwischenzeit, gleichzeitiges Gruen. Dieselbe Phase
18 * fuehrte ausserdem die Furten der Querachse mit, die der Linksabbieger quert.
19 *
20 * Die Zusage des Kaestchens ist die gesicherte Fuehrung ("Noetig, wenn
21 * Linksabbieger nicht in Luecken des Gegenverkehrs abbiegen koennen"), und das
22 * Programm nennt sie an zwei weiteren Stellen so: die Hilfe zur Phase ("wird
23 * daraus eine dritte Phase") und der Pruefbericht ("fuehren Sie ... mit eigener
24 * Freigabezeit"). Geprueft wird deshalb das Erzeugnis, nicht die Oberflaeche.
25 */
26
27 const FESTES_DATUM = new Date('2026-01-01T00:00:00Z');
28
29 function baue(teil: Partial<AssistentAngaben>): Project {
30 return projektAusAngaben({ ...ASSISTENT_VORGABE, ...teil }, 'Pruefung', FESTES_DATUM);
31 }
32
33 /** Kfz-Signalgruppe einer Zufahrt in der gesuchten Fahrtrichtung. */
34 function kfz(projekt: Project, zufahrt: string, movement: string): SignalGroup {
35 const arm = projekt.intersection.arms.find((a) => a.name === zufahrt);
36 expect(arm, `Zufahrt "${zufahrt}" fehlt`).toBeDefined();
37 const treffer = projekt.signalGroups.find(
38 (g) => g.armId === arm!.id && g.mode === 'kfz' && g.movement === movement,
39 );
40 expect(treffer, `Kfz-Gruppe ${zufahrt}/${movement} fehlt`).toBeDefined();
41 return treffer!;
42 }
43
44 /** Die Phase, in der die Gruppe Freigabe bekommt. */
45 function phaseVon(projekt: Project, gruppe: SignalGroup): { name: string; ids: readonly string[] } {
46 const phase = projekt.phases.find((p) => p.signalGroupIds.includes(gruppe.id));
47 expect(phase, `${gruppe.name} steht in keiner Phase`).toBeDefined();
48 return { name: phase!.name, ids: phase!.signalGroupIds };
49 }
50
51 /** Gibt es zwischen beiden Gruppen eine erfasste Beziehung - in einer Richtung? */
52 function beziehung(projekt: Project, a: SignalGroup, b: SignalGroup): boolean {
53 return projekt.conflicts.some(
54 (c) => (c.fromId === a.id && c.toId === b.id) || (c.fromId === b.id && c.toId === a.id),
55 );
56 }
57
58 /** Zeigen beide Gruppen zu irgendeinem Zeitpunkt gemeinsam Freigabe? */
59 function gemeinsamesGruen(projekt: Project, a: SignalGroup, b: SignalGroup): number {
60 const plan = buildSignalPlan(projekt);
61 expect(plan.feasible, 'Der Plan ist nicht schaltbar').toBe(true);
62 const links = plan.groups.find((g) => g.groupId === a.id)!;
63 const gegen = plan.groups.find((g) => g.groupId === b.id)!;
64 let sekunden = 0;
65 for (let t = 0; t < plan.cycleTime; t += 0.5) {
66 if (isGreenAt(links, t, plan.cycleTime) && isGreenAt(gegen, t, plan.cycleTime)) sekunden += 0.5;
67 }
68 return sekunden;
69 }
70
71 describe('Eigene Freigabe fuer Linksabbieger: eigene Phase statt gemeinsamer', () => {
72 it('legt den Linksabbieger nicht in die Phase seines Gegenverkehrs', () => {
73 const projekt = baue({ form: 'kreuzung', linksabbieger: true });
74 const nordLinks = kfz(projekt, 'Nord', 'links');
75 const suedGerade = kfz(projekt, 'Süd', 'geradeaus');
76
77 expect(phaseVon(projekt, nordLinks).name).not.toBe(phaseVon(projekt, suedGerade).name);
78 });
79
80 it('fuehrt Linksabbieger und Gegenverkehr als Konfliktbeziehung', () => {
81 const projekt = baue({ form: 'kreuzung', linksabbieger: true });
82 const nordLinks = kfz(projekt, 'Nord', 'links');
83
84 // Der Gegenverkehr der eigenen Achse - geradeaus wie abbiegend ist der
85 // Linksabbieger aus Sued unkritisch (beide biegen links ab und kreuzen sich
86 // nicht), der GERADEAUSSTROM aus Sued kreuzt ihn dagegen.
87 expect(beziehung(projekt, nordLinks, kfz(projekt, 'Süd', 'geradeaus'))).toBe(true);
88 expect(
89 beziehung(projekt, kfz(projekt, 'Süd', 'links'), kfz(projekt, 'Nord', 'geradeaus')),
90 ).toBe(true);
91 });
92
93 it('gibt Linksabbieger und Gegenverkehr nie gleichzeitig frei', () => {
94 const projekt = baue({ form: 'kreuzung', linksabbieger: true });
95 expect(
96 gemeinsamesGruen(projekt, kfz(projekt, 'Nord', 'links'), kfz(projekt, 'Süd', 'geradeaus')),
97 ).toBe(0);
98 });
99
100 it('fuehrt in der Linksabbiegerphase keine Furt und keinen Geradeausstrom mit', () => {
101 const projekt = baue({ form: 'kreuzung', linksabbieger: true, fussgaenger: true, rad: true });
102 const phase = phaseVon(projekt, kfz(projekt, 'Nord', 'links'));
103 const mitlaeufer = phase.ids.map((id) => projekt.signalGroups.find((g) => g.id === id)!);
104
105 for (const g of mitlaeufer) {
106 expect(`${g.name}: ${g.mode}/${g.movement}`).toBe(`${g.name}: kfz/links`);
107 }
108 });
109
110 it('gibt die gegenueberliegenden Linksabbieger gemeinsam frei', () => {
111 const projekt = baue({ form: 'kreuzung', linksabbieger: true });
112 // Sie kreuzen einander nicht - eine eigene Phase je Zufahrt waere ein
113 // Umlauf, den kein Knotenpunkt braucht.
114 expect(phaseVon(projekt, kfz(projekt, 'Nord', 'links')).name).toBe(
115 phaseVon(projekt, kfz(projekt, 'Süd', 'links')).name,
116 );
117 expect(beziehung(projekt, kfz(projekt, 'Nord', 'links'), kfz(projekt, 'Süd', 'links'))).toBe(
118 false,
119 );
120 });
121
122 it('legt ohne das Kaestchen keine zusaetzliche Phase an', () => {
123 // Die Gegenprobe: Wer nicht ankreuzt, bekommt den Umlauf von vorher.
124 expect(baue({ form: 'kreuzung' }).phases.map((p) => p.name)).toEqual([
125 'Phase 1 – Nord/Süd',
126 'Phase 2 – Ost/West',
127 ]);
128 expect(baue({ form: 'fuenfarmig' }).phases).toHaveLength(3);
129 });
130
131 it('nummeriert die Phasen des Umlaufs lueckenlos durch', () => {
132 for (const teil of [
133 { form: 'kreuzung' as const, linksabbieger: true },
134 { form: 'einmuendung' as const, linksabbieger: true },
135 { form: 'fuenfarmig' as const, linksabbieger: true },
136 ]) {
137 const projekt = baue(teil);
138 projekt.phases.forEach((p, i) => {
139 expect(p.name.startsWith(`Phase ${i + 1} – `), `${teil.form}: ${p.name}`).toBe(true);
140 expect(p.signalGroupIds.length, `${teil.form}: ${p.name} ist leer`).toBeGreaterThan(0);
141 });
142 expect(projekt.program.phaseOrder).toHaveLength(projekt.phases.length);
143 }
144 });
145
146 it('gibt dem Ostarm der Einmuendung keinen Linksabbieger und keine leere Phase', () => {
147 const projekt = baue({ form: 'einmuendung', linksabbieger: true });
148 const arm = projekt.intersection.arms.find((a) => a.name === 'Ost')!;
149 expect(
150 projekt.signalGroups.filter((g) => g.armId === arm.id && g.movement === 'links'),
151 ).toHaveLength(0);
152 // Nord und Sued haben Gegenverkehr, Ost nicht: drei Phasen, nicht vier.
153 expect(projekt.phases).toHaveLength(3);
154 });
155
156 it('bleibt mit eigener Linksabbiegerphase schaltbar und ohne Fachfehler', () => {
157 for (const teil of [
158 { form: 'kreuzung' as const, linksabbieger: true, rad: true },
159 { form: 'einmuendung' as const, linksabbieger: true, rad: true },
160 { form: 'fuenfarmig' as const, linksabbieger: true, rad: true },
161 ]) {
162 const projekt = baue(teil);
163 const plan = buildSignalPlan(projekt);
164 expect(plan.feasible, teil.form).toBe(true);
165 const fehler = validateProject(projekt, plan, FESTES_DATUM)
166 .findings.filter((f) => f.severity === 'fehler' && f.category !== 'projektdaten')
167 .map((f) => `${f.rule}: ${f.message}`);
168 expect(fehler, `${teil.form}: ${fehler.join('\n')}`).toHaveLength(0);
169 }
170 });
171
172 it('nennt die eigene Phase in der Zusammenfassung der Annahmen', () => {
173 // Was der Assistent an Fuehrungsform festlegt, steht im letzten Schritt -
174 // dieselbe Regel wie fuer die Furtwege (Befund C12).
175 const mit = assistentZusammenfassung({ ...ASSISTENT_VORGABE, linksabbieger: true }).join(' ');
176 expect(mit).toContain('Linksabbieger');
177 expect(mit).toContain('eigene Phase');
178 expect(mit).toContain('Gegenverkehr');
179
180 const ohne = assistentZusammenfassung({ ...ASSISTENT_VORGABE, linksabbieger: false }).join(' ');
181 expect(ohne).not.toContain('Linksabbieger');
182 });
183 });