lsa-planer

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

/ tests domain gruppenzuordnung.test.ts

7,7 KB Rohdatei
tests/domain/gruppenzuordnung.test.ts — 244 Zeilen
1 import { describe, expect, it } from 'vitest';
2 import { verbindeMitGruppe } from '@/domain/geometrie/ableitung';
3 import { lotAuf, type Haltlinie, type Lageplan, type Planlinie } from '@/domain/geometrie/lageplan';
4 import { REGELBREITE } from '@/domain/geometrie/vermessung';
5 import { createEmptyProject } from '@/domain/model/factory';
6 import type { Project, SignalGroup } from '@/domain/model/project';
7
8 /**
9 * Zuordnung eines Stroms zu einer bestehenden Signalgruppe.
10 *
11 * Der Fall, den das Programm bis dahin nicht ausdruecken konnte: EIN Signalgeber
12 * fuer geradeaus UND rechts. Eine Signalgruppe ist die Menge gleich schaltender
13 * Signalgeber, kein Verkehrsstrom - sie darf mehrere Stroeme bedienen.
14 *
15 * Die drei Sperren sind der eigentliche Gegenstand dieser Faelle. Jede von ihnen
16 * verhindert eine Zuordnung, die auf den ersten Blick vernuenftig aussieht und
17 * einen Konflikt ungesichert liesse.
18 */
19
20 const FIXED_DATE = new Date('2026-01-01T00:00:00Z');
21 const HALTLINIE = 'hl-nord';
22
23 const haltlinie = (id: string, versatz = 0): Haltlinie => ({
24 id,
25 name: id,
26 art: 'zufahrt',
27 armId: 'arm-nord',
28 von: { x: versatz, y: 0 },
29 bis: { x: versatz + 70, y: 0 },
30 });
31
32 const linie = (
33 id: string,
34 punkte: readonly { x: number; y: number }[],
35 patch: Partial<Planlinie> = {},
36 ): Planlinie => ({
37 id,
38 name: id,
39 mode: 'kfz',
40 movement: 'geradeaus',
41 breiteMeter: REGELBREITE.kfz,
42 punkte,
43 haltlinieId: HALTLINIE,
44 startT: 0.5,
45 signalGroupId: null,
46 ...patch,
47 });
48
49 const gruppe = (id: string, patch: Partial<SignalGroup> = {}): SignalGroup => ({
50 id,
51 name: id,
52 mode: 'kfz',
53 movement: 'geradeaus',
54 armId: null,
55 vZul: 50,
56 lanes: 1,
57 vehicleClass: 'pkw',
58 minGreenOverride: null,
59 maxGreenOverride: null,
60 reducedMobility: false,
61 color: '#2f6fd0',
62 ...patch,
63 });
64
65 /** 1 Bildpunkt = 1 m, damit sich die Masse unmittelbar ablesen lassen. */
66 function plan(linien: readonly Planlinie[], haltlinien: readonly Haltlinie[]): Lageplan {
67 return {
68 arbeitsbereiche: [],
69 signalgeber: [],
70 bild: null,
71 kalibrierung: {
72 von: { x: 0, y: 0 },
73 bis: { x: 100, y: 0 },
74 laengeMeter: 100,
75 herkunft: 'gemessen',
76 },
77 haltlinien,
78 linien,
79 };
80 }
81
82 function projekt(gruppen: readonly SignalGroup[], lageplan: Lageplan): Project {
83 const leer = createEmptyProject('Zuordnung', FIXED_DATE);
84 return { ...leer, signalGroups: gruppen, lageplan };
85 }
86
87 // Zwei Stroeme derselben Haltlinie, die auseinanderlaufen.
88 const GERADEAUS = linie('geradeaus', [
89 { x: 35, y: 0 },
90 { x: 35, y: 40 },
91 ]);
92 const RECHTS = linie(
93 'rechts',
94 [
95 { x: 35, y: 0 },
96 { x: 38, y: 12 },
97 { x: 70, y: 16 },
98 ],
99 { movement: 'rechts' },
100 );
101
102 describe('Strom einer bestehenden Signalgruppe zuordnen', () => {
103 it('legt geradeaus und rechts auf denselben Signalgeber', () => {
104 const lageplan = plan(
105 [{ ...GERADEAUS, signalGroupId: 'sg-1' }, RECHTS],
106 [haltlinie(HALTLINIE)],
107 );
108 const p = projekt([gruppe('sg-1')], lageplan);
109
110 const ergebnis = verbindeMitGruppe(p, lageplan, 'rechts', 'sg-1');
111
112 expect(ergebnis.hinweis).toBeNull();
113 expect(ergebnis.lageplan.linien.find((l) => l.id === 'rechts')?.signalGroupId).toBe('sg-1');
114 // Und die andere Linie bleibt unberuehrt.
115 expect(ergebnis.lageplan.linien.find((l) => l.id === 'geradeaus')?.signalGroupId).toBe('sg-1');
116 });
117
118 it('loest eine Zuordnung wieder, ohne zu pruefen', () => {
119 const lageplan = plan([{ ...GERADEAUS, signalGroupId: 'sg-1' }], [haltlinie(HALTLINIE)]);
120 const p = projekt([gruppe('sg-1')], lageplan);
121
122 const ergebnis = verbindeMitGruppe(p, lageplan, 'geradeaus', null);
123
124 expect(ergebnis.hinweis).toBeNull();
125 expect(ergebnis.lageplan.linien[0]?.signalGroupId).toBeNull();
126 });
127
128 it('verweigert eine fremde Verkehrsart', () => {
129 // Fussgaenger und Kraftfahrzeuge haben verschiedene Mindestfreigabe- und
130 // Raeumzeiten; eine gemeinsame Gruppe koennte beide nicht einhalten.
131 const furt = linie(
132 'furt',
133 [
134 { x: 20, y: 6 },
135 { x: 60, y: 6 },
136 ],
137 { mode: 'fuss', movement: 'querung', breiteMeter: REGELBREITE.fuss },
138 );
139 const lageplan = plan([{ ...GERADEAUS, signalGroupId: 'sg-1' }, furt], [haltlinie(HALTLINIE)]);
140 const p = projekt([gruppe('sg-1')], lageplan);
141
142 const ergebnis = verbindeMitGruppe(p, lageplan, 'furt', 'sg-1');
143
144 expect(ergebnis.hinweis?.code).toBe('gruppe-fremde-verkehrsart');
145 expect(ergebnis.lageplan).toBe(lageplan);
146 });
147
148 it('verweigert Stroeme verschiedener Haltlinien', () => {
149 // Gleich schaltende Signalgeber stehen an einem Querschnitt.
150 const andere = linie(
151 'von-sued',
152 [
153 { x: 35, y: 40 },
154 { x: 35, y: 5 },
155 ],
156 { haltlinieId: 'hl-sued' },
157 );
158 const lageplan = plan(
159 [{ ...GERADEAUS, signalGroupId: 'sg-1' }, andere],
160 [
161 haltlinie(HALTLINIE),
162 { ...haltlinie('hl-sued'), von: { x: 0, y: 40 }, bis: { x: 70, y: 40 } },
163 ],
164 );
165 const p = projekt([gruppe('sg-1')], lageplan);
166
167 const ergebnis = verbindeMitGruppe(p, lageplan, 'von-sued', 'sg-1');
168
169 expect(ergebnis.hinweis?.code).toBe('gruppe-fremde-haltlinie');
170 expect(ergebnis.lageplan).toBe(lageplan);
171 });
172
173 it('verweigert sich kreuzende Stroeme', () => {
174 // Die entscheidende Sperre: Zwischen Stroemen derselben Gruppe gibt es keine
175 // Zwischenzeit. Wuerden zwei kreuzende Stroeme gebuendelt, erhielten sie
176 // gleichzeitig Freigabe fuer einander feindliche Bewegungen - und niemand
177 // saehe es, weil kein Konflikt mehr gefuehrt wird.
178 const kreuzend = linie(
179 'kreuzend',
180 [
181 { x: 60, y: 0 },
182 { x: 10, y: 30 },
183 ],
184 { movement: 'links' },
185 );
186 const lageplan = plan(
187 [{ ...GERADEAUS, signalGroupId: 'sg-1' }, kreuzend],
188 [haltlinie(HALTLINIE)],
189 );
190 const p = projekt([gruppe('sg-1')], lageplan);
191
192 const ergebnis = verbindeMitGruppe(p, lageplan, 'kreuzend', 'sg-1');
193
194 expect(ergebnis.hinweis?.code).toBe('gruppe-kreuzende-stroeme');
195 expect(ergebnis.lageplan).toBe(lageplan);
196 });
197
198 it('begruendet jede Verweigerung im Klartext', () => {
199 const furt = linie(
200 'furt',
201 [
202 { x: 20, y: 6 },
203 { x: 60, y: 6 },
204 ],
205 { mode: 'fuss', movement: 'querung' },
206 );
207 const lageplan = plan([{ ...GERADEAUS, signalGroupId: 'sg-1' }, furt], [haltlinie(HALTLINIE)]);
208 const p = projekt([gruppe('sg-1', { name: 'K1' })], lageplan);
209
210 const hinweis = verbindeMitGruppe(p, lageplan, 'furt', 'sg-1').hinweis;
211
212 // Der Text muss beide Beteiligten nennen - sonst weiss der Anwender nicht,
213 // welche Zuordnung abgelehnt wurde.
214 expect(hinweis?.message).toContain('K1');
215 expect(hinweis?.message).toContain('furt');
216 expect(hinweis?.severity).toBe('warnung');
217 });
218 });
219
220 describe('Lotfusspunkt auf der Haltlinie', () => {
221 it('findet die naechstgelegene Stelle', () => {
222 // Beim nachtraeglichen Zuordnen wandert der Anfangspunkt auf die Haltlinie.
223 // Genommen wird die Stelle, die der Anwender gemeint hat.
224 const h = haltlinie(HALTLINIE); // von (0,0) bis (70,0)
225 expect(lotAuf(h, { x: 35, y: 12 })).toBeCloseTo(0.5, 6);
226 expect(lotAuf(h, { x: 0, y: 3 })).toBeCloseTo(0, 6);
227 expect(lotAuf(h, { x: 70, y: -3 })).toBeCloseTo(1, 6);
228 });
229
230 it('begrenzt auf die Strecke, statt sie zu verlaengern', () => {
231 const h = haltlinie(HALTLINIE);
232 expect(lotAuf(h, { x: -40, y: 0 })).toBe(0);
233 expect(lotAuf(h, { x: 200, y: 0 })).toBe(1);
234 });
235
236 it('bleibt bei einer entarteten Haltlinie in der Mitte', () => {
237 const entartet: Haltlinie = { ...haltlinie(HALTLINIE), bis: { x: 0, y: 0 } };
238 expect(lotAuf(entartet, { x: 10, y: 10 })).toBe(0.5);
239 });
240
241 it('faengt einen fehlenden Punkt ab', () => {
242 expect(lotAuf(haltlinie(HALTLINIE), undefined)).toBe(0.5);
243 });
244 });