lsa-planer

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

/ tests domain vertraeglichkeitAufheben.test.ts

8,8 KB Rohdatei
tests/domain/vertraeglichkeitAufheben.test.ts — 281 Zeilen
1 import { describe, expect, it } from 'vitest';
2 import { hebeVertraeglicheAuf, leiteAb, verbindeMitGruppe } from '@/domain/geometrie/ableitung';
3 import type { Haltlinie, Lageplan, Planlinie } from '@/domain/geometrie/lageplan';
4 import { REGELBREITE } from '@/domain/geometrie/vermessung';
5 import { createEmptyProject } from '@/domain/model/factory';
6 import {
7 WEGNACHWEIS_UNBESTIMMT,
8 type Conflict,
9 type Project,
10 type SignalGroup,
11 } from '@/domain/model/project';
12
13 /**
14 * Aufheben eines Konflikts, wenn eine Signalgruppe MEHRERE Stroeme fuehrt.
15 *
16 * Der Vorschlag steht je Strompaar, der Konflikt steht je Signalgruppenpaar.
17 * Ein Signalgeber fuer geradeaus UND rechts ist der Regelfall - dann gehoeren
18 * zu einem Gruppenpaar mehrere Strompaare, und es gilt das unguenstigste.
19 * Wird die Beziehung schon aufgehoben, weil EIN Strompaar vertraeglich ist,
20 * verliert das feindliche Strompaar derselben Gruppen seine Zwischenzeit.
21 */
22
23 const FIXED_DATE = new Date('2026-01-01T00:00:00Z');
24
25 /** 1 Bildpunkt = 1 m, damit sich die Masse unmittelbar ablesen lassen. */
26 const KALIBRIERUNG = {
27 von: { x: 0, y: 0 },
28 bis: { x: 100, y: 0 },
29 laengeMeter: 100,
30 herkunft: 'gemessen',
31 } as const;
32
33 const HL_ZUFAHRT = 'hl-nord';
34 const HL_FURT = 'hl-ost';
35
36 const HALTLINIEN: readonly Haltlinie[] = [
37 {
38 id: HL_ZUFAHRT,
39 name: 'Zufahrt Nord',
40 art: 'zufahrt',
41 armId: 'arm-nord',
42 von: { x: 0, y: 0 },
43 bis: { x: 70, y: 0 },
44 },
45 {
46 id: HL_FURT,
47 name: 'Furt Ost',
48 art: 'querung',
49 armId: 'arm-ost',
50 von: { x: 60, y: 5 },
51 bis: { x: 60, y: 30 },
52 },
53 ];
54
55 const linie = (
56 id: string,
57 punkte: readonly { x: number; y: number }[],
58 patch: Partial<Planlinie> = {},
59 ): Planlinie => ({
60 id,
61 name: id,
62 mode: 'kfz',
63 movement: 'geradeaus',
64 breiteMeter: REGELBREITE.kfz,
65 punkte,
66 haltlinieId: HL_ZUFAHRT,
67 startT: 0.5,
68 signalGroupId: null,
69 ...patch,
70 });
71
72 /** Geradeaus aus der Nordzufahrt - kreuzt die Furt im Osten nicht. */
73 const GERADEAUS = linie(
74 'L1 geradeaus',
75 [
76 { x: 35, y: 0 },
77 { x: 35, y: 40 },
78 ],
79 { signalGroupId: 'sg-a' },
80 );
81
82 /** Rechtsabbieger aus derselben Zufahrt - kreuzt die Furt im Osten. */
83 const RECHTS = linie(
84 'L2 rechts',
85 [
86 { x: 35, y: 0 },
87 { x: 38, y: 12 },
88 { x: 70, y: 16 },
89 ],
90 { movement: 'rechts', signalGroupId: 'sg-a' },
91 );
92
93 /** Fussgaengerfurt ueber den Ausfahrtarm Ost. */
94 const FURT = linie(
95 'L3 Furt',
96 [
97 { x: 60, y: 5 },
98 { x: 60, y: 30 },
99 ],
100 {
101 mode: 'fuss',
102 movement: 'querung',
103 breiteMeter: REGELBREITE.fuss,
104 haltlinieId: HL_FURT,
105 signalGroupId: 'sg-b',
106 },
107 );
108
109 function plan(linien: readonly Planlinie[]): Lageplan {
110 return {
111 bild: null,
112 kalibrierung: KALIBRIERUNG,
113 haltlinien: HALTLINIEN,
114 linien,
115 arbeitsbereiche: [],
116 signalgeber: [],
117 };
118 }
119
120 const gruppe = (id: string, patch: Partial<SignalGroup> = {}): SignalGroup => ({
121 id,
122 name: id,
123 mode: 'kfz',
124 movement: 'geradeaus',
125 armId: null,
126 vZul: 50,
127 lanes: 1,
128 vehicleClass: 'pkw',
129 minGreenOverride: null,
130 maxGreenOverride: null,
131 reducedMobility: false,
132 color: '#2f6fd0',
133 ...patch,
134 });
135
136 const konflikt = (id: string, fromId: string, toId: string): Conflict => ({
137 id,
138 fromId,
139 toId,
140 clearingDistance: 25,
141 enteringDistance: 5,
142 clearingSpeedOverride: null,
143 enteringSpeedOverride: null,
144 crossingTimeOverride: null,
145 manualIntergreen: null,
146 wegnachweis: WEGNACHWEIS_UNBESTIMMT,
147 note: '',
148 });
149
150 function projekt(lageplan: Lageplan, conflicts: readonly Conflict[]): Project {
151 const leer = createEmptyProject('Aufheben', FIXED_DATE);
152 return {
153 ...leer,
154 signalGroups: [
155 gruppe('sg-a'),
156 gruppe('sg-b', { mode: 'fuss', movement: 'querung', vehicleClass: 'keine' }),
157 ],
158 conflicts: [...conflicts],
159 lageplan,
160 };
161 }
162
163 /** Sucht den Vorschlag zu einem Linienpaar, unabhaengig von der Reihenfolge. */
164 function paar(
165 vorschlaege: ReturnType<typeof leiteAb>['vertraeglichkeit'],
166 a: string,
167 b: string,
168 ): (typeof vorschlaege)[number] {
169 const treffer = vorschlaege.find(
170 (v) => (v.aName === a && v.bName === b) || (v.aName === b && v.bName === a),
171 );
172 if (treffer === undefined) throw new Error(`Kein Vorschlag fuer "${a}" und "${b}"`);
173 return treffer;
174 }
175
176 describe('Konflikte aufheben bei einer Signalgruppe mit mehreren Stroemen', () => {
177 it('laesst die Zuordnung von geradeaus und rechts auf einen Signalgeber zu', () => {
178 // Grundlage des Falls: Die Buendelung ist zulaessig, sonst waere er nicht
179 // erreichbar.
180 const lageplan = plan([{ ...GERADEAUS, signalGroupId: null }, RECHTS, FURT]);
181 const p = projekt(lageplan, []);
182
183 const ergebnis = verbindeMitGruppe(p, lageplan, GERADEAUS.id, 'sg-a');
184
185 expect(ergebnis.hinweis).toBeNull();
186 expect(ergebnis.lageplan.linien.find((l) => l.id === GERADEAUS.id)?.signalGroupId).toBe('sg-a');
187 });
188
189 it('stellt genau ein vertraegliches und ein feindliches Strompaar derselben Gruppen fest', () => {
190 const lageplan = plan([GERADEAUS, RECHTS, FURT]);
191 const p = projekt(lageplan, [konflikt('k-hin', 'sg-a', 'sg-b')]);
192
193 const ableitung = leiteAb(p, lageplan);
194
195 expect(paar(ableitung.vertraeglichkeit, GERADEAUS.name, FURT.name).feindlich).toBe(false);
196 expect(paar(ableitung.vertraeglichkeit, GERADEAUS.name, FURT.name).imProjekt).toBe(true);
197 expect(paar(ableitung.vertraeglichkeit, RECHTS.name, FURT.name).feindlich).toBe(true);
198 });
199
200 it('haelt den Konflikt, solange ein Strompaar derselben Gruppen feindlich bleibt', () => {
201 // Ohne die Verdichtung auf Gruppenpaare reisst das vertraegliche Paar
202 // "geradeaus und Furt" die Beziehung SG-A/SG-B mit. Der Rechtsabbieger und
203 // die Furt bekaemen danach gleichzeitig Freigabe, ohne Zwischenzeit.
204 const lageplan = plan([GERADEAUS, RECHTS, FURT]);
205 const p = projekt(lageplan, [
206 konflikt('k-hin', 'sg-a', 'sg-b'),
207 konflikt('k-rueck', 'sg-b', 'sg-a'),
208 ]);
209 const ableitung = leiteAb(p, lageplan);
210
211 const ergebnis = hebeVertraeglicheAuf(p, lageplan, ableitung);
212
213 expect(ergebnis.aufgehoben).toBe(0);
214 expect(ergebnis.project.conflicts.map((c) => c.id).sort()).toEqual(['k-hin', 'k-rueck']);
215 });
216
217 it('haelt den Konflikt, solange ein Strom der Gruppen nicht fertig gezeichnet ist', () => {
218 // Der Rechtsabbieger hat nur einen Punkt. leiteAb nimmt ihn von der
219 // Auswertung aus ("linie-unfertig"), also steht kein Strompaar
220 // "rechts/Furt" in den Vorschlaegen. Wuerde allein danach entschieden,
221 // saehe das Gruppenpaar vertraeglich aus, und der Anwender verloere die
222 // Zwischenzeit zwischen Rechtsabbieger und Furt fuer einen Strom, den
223 // niemand geprueft hat.
224 const lageplan = plan([GERADEAUS, { ...RECHTS, punkte: [{ x: 35, y: 0 }] }, FURT]);
225 const p = projekt(lageplan, [
226 konflikt('k-hin', 'sg-a', 'sg-b'),
227 konflikt('k-rueck', 'sg-b', 'sg-a'),
228 ]);
229 const ableitung = leiteAb(p, lageplan);
230
231 // Grundlage: Das feindliche Strompaar fehlt in den Vorschlaegen wirklich.
232 expect(
233 ableitung.vertraeglichkeit.some(
234 (v) =>
235 (v.aName === RECHTS.name && v.bName === FURT.name) ||
236 (v.aName === FURT.name && v.bName === RECHTS.name),
237 ),
238 ).toBe(false);
239 expect(ableitung.hinweise.some((h) => h.code === 'linie-unfertig')).toBe(true);
240
241 const ergebnis = hebeVertraeglicheAuf(p, lageplan, ableitung);
242
243 expect(ergebnis.aufgehoben).toBe(0);
244 expect(ergebnis.project.conflicts.map((c) => c.id).sort()).toEqual(['k-hin', 'k-rueck']);
245 });
246
247 it('hebt einen Konflikt auf, den eine unfertige Linie anderer Gruppen nicht beruehrt', () => {
248 // Gegenprobe gegen die Ueberkorrektur: Die unfertige Linie gehoert zu
249 // SG-C. Das Gruppenpaar SG-A/SG-B ist davon unberuehrt und muss weiter
250 // aufhebbar bleiben.
251 const unfertig = linie('L4 unfertig', [{ x: 10, y: 0 }], { signalGroupId: 'sg-c' });
252 const lageplan = plan([GERADEAUS, FURT, unfertig]);
253 const leer = projekt(lageplan, [
254 konflikt('k-hin', 'sg-a', 'sg-b'),
255 konflikt('k-rueck', 'sg-b', 'sg-a'),
256 ]);
257 const p: Project = { ...leer, signalGroups: [...leer.signalGroups, gruppe('sg-c')] };
258 const ableitung = leiteAb(p, lageplan);
259
260 const ergebnis = hebeVertraeglicheAuf(p, lageplan, ableitung);
261
262 expect(ergebnis.aufgehoben).toBe(1);
263 expect(ergebnis.project.conflicts).toHaveLength(0);
264 });
265
266 it('hebt den Konflikt auf, sobald ALLE Strompaare der Gruppen vertraeglich sind', () => {
267 // Gegenprobe: Ohne den Rechtsabbieger bleibt nur das vertraegliche
268 // Strompaar - dann ist das Aufheben richtig und muss weiter geschehen.
269 const lageplan = plan([GERADEAUS, FURT]);
270 const p = projekt(lageplan, [
271 konflikt('k-hin', 'sg-a', 'sg-b'),
272 konflikt('k-rueck', 'sg-b', 'sg-a'),
273 ]);
274 const ableitung = leiteAb(p, lageplan);
275
276 const ergebnis = hebeVertraeglicheAuf(p, lageplan, ableitung);
277
278 expect(ergebnis.aufgehoben).toBe(1);
279 expect(ergebnis.project.conflicts).toHaveLength(0);
280 });
281 });