lsa-planer

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

/ tests domain schluesselAusKennungen.test.ts

12,5 KB Rohdatei
tests/domain/schluesselAusKennungen.test.ts — 365 Zeilen
1 import { describe, expect, it } from 'vitest';
2 import {
3 leiteAb,
4 pruefeAufhebbarkeit,
5 uebernimmWege,
6 wegSchluessel,
7 type Ableitung,
8 type VertraeglichkeitVorschlag,
9 } from '@/domain/geometrie/ableitung';
10 import type { Haltlinie, Lageplan, Planlinie } from '@/domain/geometrie/lageplan';
11 import { REGELBREITE } from '@/domain/geometrie/vermessung';
12 import { createEmptyProject } from '@/domain/model/factory';
13 import type { Project, SignalGroup } from '@/domain/model/project';
14
15 /*
16 * Folge aus Befund 61 (Fassung 5.10.0): zusammengesetzte Schluessel aus
17 * Signalgruppenkennungen in `ableitung.ts`.
18 *
19 * Eine Signalgruppenkennung kam ungeprueft aus der Projektdatei - das Einlesen
20 * beanstandete nur die leere und die doppelt vergebene Kennung, kein
21 * Zeichenrepertoire. Enthaelt eine Kennung ein "|", geht der zusammengeklebte
22 * Schluessel `${fromId}|${toId}` auf zwei Weisen kaputt:
23 *
24 * 1. Er ist MEHRDEUTIG. Die Paare ("a|b", "c") und ("a", "b|c") ergeben
25 * beide "a|b|c"; zwei sachlich verschiedene Beziehungen fallen auf
26 * denselben Eintrag, und eine von beiden verschwindet still.
27 * 2. Er ist NICHT UMKEHRBAR. `paar.split('|')` liefert fuer "sg|a|sg-b" die
28 * drei Stuecke "sg", "a", "sg-b"; uebernommen wurde die Beziehung
29 * "sg" -> "a" - zwei Kennungen, die es im Projekt gar nicht gibt.
30 *
31 * Beides trifft die gefaehrliche Richtung: Die gemeinte Beziehung bleibt ohne
32 * die vermessenen Wege stehen, ihre Zwischenzeit haengt weiter am alten (oder
33 * am Vorgabe-)Raeumweg, und der Anwender sieht im Lageplan eine Uebernahme,
34 * die woanders gelandet ist.
35 *
36 * WELCHE STELLEN ES HEUTE SIND (nachgezaehlt mit der Fassung 5.12.0; zuvor
37 * stand hier "die beiden verbliebenen"). `ableitung.ts` bildet an DREI Stellen
38 * einen Schluessel aus Kennungen:
39 *
40 * 1. die Verdichtung auf Signalgruppenpaare in `uebernimmWege` - seit
41 * Befund 61 verschachtelt gefuehrt, wie `parseConflicts`;
42 * 2. der ungerichtete Schluessel der Gruppenpaare in `pruefeAufhebbarkeit` -
43 * ebenso verschachtelt;
44 * 3. `wegSchluessel`, das Auswahlmerkmal eines Strompaares aus zwei
45 * PLANLINIENKENNUNGEN - seit Fassung 5.11.0 ueber
46 * `intergreenKey` maskiert statt mit "->" zusammengeklebt.
47 *
48 * Die dritte Stelle traegt jeder Fall dieser Datei mit: `wegeAufDieFurt` waehlt
49 * die zu uebernehmenden Wege ueber `wegSchluessel` aus. Eine eigene Wache dazu
50 * steht in `wegeschluessel.test.ts`.
51 *
52 * DIE QUELLE IST SEIT 5.11.0 GESCHLOSSEN: Das Einlesen prueft die Kennungen
53 * jetzt gegen ein Zeichenrepertoire (`ZULAESSIGE_KENNUNG` in
54 * src/domain/model/schema.ts) und ersetzt, was nicht hineinpasst. Eine Kennung
55 * mit "|" kann also nicht mehr aus einer Datei kommen. Die Faelle hier bleiben
56 * trotzdem: Sie bewachen die Schluesselbildung selbst - die zweite Reihe, und
57 * die einzige, die noch traegt, wenn das Repertoire eines Tages weiter gefasst
58 * wird. Sie bauen ihre Projekte deshalb unmittelbar auf, nicht ueber das
59 * Einlesen.
60 *
61 * GEGEN DEN ALTSTAND schlagen die drei Faelle mit "|" in der Kennung fehl; die
62 * beiden Gegenproben mit gewoehnlichen Kennungen bestehen dort ebenso.
63 */
64
65 const DATUM = new Date('2026-01-01T00:00:00Z');
66
67 /** 1 Bildpunkt = 1 m, damit sich die Masse unmittelbar ablesen lassen. */
68 const KALIBRIERUNG = {
69 von: { x: 0, y: 0 },
70 bis: { x: 100, y: 0 },
71 laengeMeter: 100,
72 herkunft: 'gemessen',
73 } as const;
74
75 /** Zufahrt und Furt eines Kreuzungspaares, um `versatz` Meter nach Osten geschoben. */
76 function haltlinienPaar(nummer: number, versatz: number): Haltlinie[] {
77 return [
78 {
79 id: `hl-zufahrt-${nummer}`,
80 name: `Zufahrt ${nummer}`,
81 art: 'zufahrt',
82 armId: `arm-${nummer}`,
83 von: { x: versatz, y: 0 },
84 bis: { x: versatz + 20, y: 0 },
85 },
86 {
87 id: `hl-furt-${nummer}`,
88 name: `Furt ${nummer}`,
89 art: 'querung',
90 armId: `arm-${nummer}`,
91 von: { x: versatz, y: 30 },
92 bis: { x: versatz + 30, y: 30 },
93 },
94 ];
95 }
96
97 /** Geradeausstrom und kreuzende Furt; sie schneiden sich bei (versatz+10 | 30). */
98 function linienPaar(
99 nummer: number,
100 versatz: number,
101 gruppeKfz: string,
102 gruppeFuss: string,
103 ): Planlinie[] {
104 return [
105 {
106 id: `l-gerade-${nummer}`,
107 name: `L${nummer} geradeaus`,
108 mode: 'kfz',
109 movement: 'geradeaus',
110 breiteMeter: REGELBREITE.kfz,
111 punkte: [
112 { x: versatz + 10, y: 0 },
113 { x: versatz + 10, y: 60 },
114 ],
115 haltlinieId: `hl-zufahrt-${nummer}`,
116 startT: 0.5,
117 signalGroupId: gruppeKfz,
118 },
119 {
120 id: `l-furt-${nummer}`,
121 name: `L${nummer} Furt`,
122 mode: 'fuss',
123 movement: 'querung',
124 breiteMeter: REGELBREITE.fuss,
125 punkte: [
126 { x: versatz, y: 30 },
127 { x: versatz + 30, y: 30 },
128 ],
129 haltlinieId: `hl-furt-${nummer}`,
130 startT: 0,
131 signalGroupId: gruppeFuss,
132 },
133 ];
134 }
135
136 const gruppe = (id: string, patch: Partial<SignalGroup> = {}): SignalGroup => ({
137 id,
138 name: id,
139 mode: 'kfz',
140 movement: 'geradeaus',
141 armId: null,
142 vZul: 50,
143 lanes: 1,
144 vehicleClass: 'pkw',
145 minGreenOverride: null,
146 maxGreenOverride: null,
147 reducedMobility: false,
148 color: '#2f6fd0',
149 ...patch,
150 });
151
152 const furtgruppe = (id: string): SignalGroup =>
153 gruppe(id, { mode: 'fuss', movement: 'querung', vehicleClass: 'keine' });
154
155 /**
156 * Projekt und Lageplan mit `paare` Kreuzungen; je Kreuzung eine Kfz- und eine
157 * Fussgaengergruppe mit den uebergebenen Kennungen.
158 */
159 function aufbau(paare: readonly (readonly [string, string])[]): {
160 project: Project;
161 lageplan: Lageplan;
162 } {
163 const haltlinien = paare.flatMap((_, i) => haltlinienPaar(i + 1, i * 100));
164 const linien = paare.flatMap(([kfz, fuss], i) => linienPaar(i + 1, i * 100, kfz, fuss));
165 const lageplan: Lageplan = {
166 bild: null,
167 kalibrierung: KALIBRIERUNG,
168 haltlinien,
169 linien,
170 arbeitsbereiche: [],
171 signalgeber: [],
172 };
173 const leer = createEmptyProject('Kennungen mit Strich', DATUM);
174 return {
175 project: {
176 ...leer,
177 signalGroups: paare.flatMap(([kfz, fuss]) => [gruppe(kfz), furtgruppe(fuss)]),
178 lageplan,
179 },
180 lageplan,
181 };
182 }
183
184 /** Die Wege der Geradeausstroeme auf ihre jeweilige Furt - je Kreuzung einer. */
185 function wegeAufDieFurt(project: Project, lageplan: Lageplan, ableitung: Ableitung): string[] {
186 const gewaehlt = ableitung.wege
187 .filter((w) => w.vonLinieId.startsWith('l-gerade-') && w.nachLinieId.startsWith('l-furt-'))
188 .map((w) => wegSchluessel(w));
189 // Vorbedingung jedes Falls: Ohne Wege prueft nichts darunter etwas.
190 expect(gewaehlt.length, 'die Ströme kreuzen sich im Plan nicht').toBeGreaterThan(0);
191 expect(project.signalGroups.length).toBeGreaterThan(0);
192 expect(lageplan.linien.length).toBe(project.signalGroups.length);
193 return gewaehlt;
194 }
195
196 describe('R1.2 - Wegeuebernahme aus dem Lageplan bei "|" in der Signalgruppenkennung', () => {
197 it('schreibt die Beziehung der wirklich gemeinten Signalgruppen', () => {
198 // Eine einzige Kreuzung genuegt: Der Schluessel "sg|a|sg-b" laesst sich
199 // nicht mehr zerlegen, und uebernommen wurde "sg" -> "a".
200 const { project, lageplan } = aufbau([['sg|a', 'sg-b']]);
201 const ableitung = leiteAb(project, lageplan);
202 const ergebnis = uebernimmWege(
203 project,
204 lageplan,
205 ableitung,
206 wegeAufDieFurt(project, lageplan, ableitung),
207 );
208
209 expect(ergebnis.uebernommen).toBe(1);
210 expect(ergebnis.project.conflicts).toHaveLength(1);
211 const geschrieben = ergebnis.project.conflicts[0]!;
212 expect(geschrieben.fromId).toBe('sg|a');
213 expect(geschrieben.toId).toBe('sg-b');
214 expect(geschrieben.clearingDistance).toBeGreaterThan(0);
215 // Und keine Beziehung zwischen Kennungen, die es im Projekt nicht gibt.
216 const kennungen = new Set(project.signalGroups.map((g) => g.id));
217 for (const c of ergebnis.project.conflicts) {
218 expect(kennungen.has(c.fromId), `fromId "${c.fromId}"`).toBe(true);
219 expect(kennungen.has(c.toId), `toId "${c.toId}"`).toBe(true);
220 }
221 });
222
223 it('haelt zwei Gruppenpaare auseinander, die denselben zusammengeklebten Schluessel ergaeben', () => {
224 // ("a|b", "c") und ("a", "b|c") ergeben beide "a|b|c".
225 const { project, lageplan } = aufbau([
226 ['a|b', 'c'],
227 ['a', 'b|c'],
228 ]);
229 const ableitung = leiteAb(project, lageplan);
230 const ergebnis = uebernimmWege(
231 project,
232 lageplan,
233 ableitung,
234 wegeAufDieFurt(project, lageplan, ableitung),
235 );
236
237 expect(ergebnis.uebernommen).toBe(2);
238 const paare = ergebnis.project.conflicts.map((c) => `${c.fromId} -> ${c.toId}`).sort();
239 expect(paare).toEqual(['a -> b|c', 'a|b -> c']);
240 });
241
242 it('uebernimmt bei gewoehnlichen Kennungen unveraendert', () => {
243 // Gegenprobe: Kennungen aus createId (a-z, 0-9, "-") sind von der
244 // Umstellung nicht betroffen.
245 const { project, lageplan } = aufbau([
246 ['sg-k1', 'sg-f1'],
247 ['sg-k2', 'sg-f2'],
248 ]);
249 const ableitung = leiteAb(project, lageplan);
250 const ergebnis = uebernimmWege(
251 project,
252 lageplan,
253 ableitung,
254 wegeAufDieFurt(project, lageplan, ableitung),
255 );
256
257 expect(ergebnis.uebernommen).toBe(2);
258 const paare = ergebnis.project.conflicts.map((c) => `${c.fromId} -> ${c.toId}`).sort();
259 expect(paare).toEqual(['sg-k1 -> sg-f1', 'sg-k2 -> sg-f2']);
260 });
261 });
262
263 /**
264 * Ein vertraegliches, im Projekt erfasstes Strompaar - der Fall, in dem
265 * `pruefeAufhebbarkeit` etwas zu melden hat.
266 */
267 function vertraeglich(aLinieId: string, bLinieId: string): VertraeglichkeitVorschlag {
268 return {
269 aLinieId,
270 bLinieId,
271 aName: aLinieId,
272 bName: bLinieId,
273 feindlich: false,
274 zuPruefen: false,
275 grund: 'kein-schnitt',
276 begruendung: 'Die Fahrlinien schneiden sich nicht.',
277 imProjekt: true,
278 abweichung: true,
279 };
280 }
281
282 /** Nur `vertraeglichkeit` wird von pruefeAufhebbarkeit gelesen. */
283 function nurVertraeglichkeit(paare: readonly VertraeglichkeitVorschlag[]): Ableitung {
284 return {
285 auswertbar: true,
286 signalgruppen: [],
287 vertraeglichkeit: paare,
288 wege: [],
289 unterdrueckt: [],
290 hinweise: [],
291 };
292 }
293
294 describe('R1.2 - Aufhebbarkeit bei "|" in der Signalgruppenkennung', () => {
295 it('beurteilt zwei Gruppenpaare mit demselben zusammengeklebten Schluessel getrennt', () => {
296 const { lageplan } = aufbau([
297 ['a|b', 'c'],
298 ['a', 'b|c'],
299 ]);
300 const ergebnis = pruefeAufhebbarkeit(
301 lageplan,
302 nurVertraeglichkeit([
303 vertraeglich('l-gerade-1', 'l-furt-1'),
304 vertraeglich('l-gerade-2', 'l-furt-2'),
305 ]),
306 );
307
308 expect(ergebnis.gesperrt).toHaveLength(0);
309 const paare = ergebnis.aufhebbar.map((p) => `${p.ersteId} + ${p.zweiteId}`).sort();
310 expect(paare).toEqual(['a + b|c', 'a|b + c']);
311 for (const eintrag of ergebnis.aufhebbar) {
312 expect(eintrag.paare, `${eintrag.ersteId} + ${eintrag.zweiteId}`).toHaveLength(1);
313 }
314 });
315
316 it('laesst ein feindliches Strompaar nur das eigene Gruppenpaar sperren', () => {
317 // Der zweite Vorschlag ist feindlich. Fielen beide auf denselben
318 // Schluessel, riss er das erste, vertraegliche Gruppenpaar mit in die
319 // Sperre - mit einer Begruendung, die zu ganz anderen Signalgruppen gehoert.
320 const { lageplan } = aufbau([
321 ['a|b', 'c'],
322 ['a', 'b|c'],
323 ]);
324 const feindlichesPaar: VertraeglichkeitVorschlag = {
325 ...vertraeglich('l-gerade-2', 'l-furt-2'),
326 feindlich: true,
327 grund: 'kreuzung',
328 begruendung: 'Die Fahrlinien kreuzen sich.',
329 abweichung: false,
330 };
331 const ergebnis = pruefeAufhebbarkeit(
332 lageplan,
333 nurVertraeglichkeit([vertraeglich('l-gerade-1', 'l-furt-1'), feindlichesPaar]),
334 );
335
336 expect(ergebnis.gesperrt).toHaveLength(0);
337 expect(ergebnis.aufhebbar).toHaveLength(1);
338 expect(ergebnis.aufhebbar[0]!.ersteId).toBe('a|b');
339 expect(ergebnis.aufhebbar[0]!.zweiteId).toBe('c');
340 });
341
342 it('fasst bei gewoehnlichen Kennungen weiter je Gruppenpaar zusammen', () => {
343 // Gegenprobe: Die Verdichtung auf Signalgruppenpaare bleibt - beide
344 // Strompaare derselben zwei Gruppen ergeben einen Eintrag.
345 const { lageplan } = aufbau([
346 ['sg-k1', 'sg-f1'],
347 ['sg-k2', 'sg-f2'],
348 ]);
349 const ergebnis = pruefeAufhebbarkeit(
350 lageplan,
351 nurVertraeglichkeit([
352 vertraeglich('l-gerade-1', 'l-furt-1'),
353 vertraeglich('l-furt-1', 'l-gerade-1'),
354 vertraeglich('l-gerade-2', 'l-furt-2'),
355 ]),
356 );
357
358 expect(ergebnis.aufhebbar).toHaveLength(2);
359 const nachPaar = new Map(
360 ergebnis.aufhebbar.map((p) => [`${p.ersteId} + ${p.zweiteId}`, p.paare.length]),
361 );
362 expect(nachPaar.get('sg-f1 + sg-k1')).toBe(2);
363 expect(nachPaar.get('sg-f2 + sg-k2')).toBe(1);
364 });
365 });