lsa-planer

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

/ tests domain wegeschluessel.test.ts

8,3 KB Rohdatei
tests/domain/wegeschluessel.test.ts — 253 Zeilen
1 import { describe, expect, it } from 'vitest';
2 import { leiteAb, uebernimmWege, wegSchluessel } 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 { WEGNACHWEIS_HANDEINGABE, type Conflict, type Project } from '@/domain/model/project';
7 import type { SignalGroup } from '@/domain/model/project';
8
9 /*
10 * Befund 43, behoben in Fassung 5.11.0.
11 *
12 * `wegSchluessel` klebte den Schluessel eines Wegevorschlags aus den beiden
13 * PLANLINIENKENNUNGEN zusammen: `${vonLinieId}->${nachLinieId}`. Dieselbe
14 * Bauart hat Befund 61 (Fassung 5.10.0) an drei Stellen beseitigt; die
15 * Folge dazu (schluesselAusKennungen.test.ts) sah dabei nur die
16 * Signalgruppenkennungen an.
17 *
18 * Planlinienkennungen sind noch schwaecher geprueft als Signalgruppenkennungen:
19 * `parseLageplan` vergibt eine doppelte zwar neu, prueft aber kein
20 * Zeichenrepertoire - eine Kennung mit "->" ueberlebt das Einlesen unveraendert.
21 * Damit fielen ("a->b", "c") und ("a", "b->c") auf denselben Schluessel
22 * "a->b->c".
23 *
24 * WAS DARAN SCHADET: `uebernimmWege` filtert mit
25 * `if (!schluessel.has(wegSchluessel(weg))) continue;`. Der Anwender hakt EIN
26 * Paar an, uebernommen werden ZWEI. Fuer das nicht angehakte Paar werden Raeum-
27 * und Einfahrweg ueberschrieben - ein von Hand eingetragener, bewusst laengerer
28 * Raeumweg wird durch den gemessenen ersetzt, und die Zwischenzeit kann dadurch
29 * KUERZER werden. Dazu bekommt es den Herkunftsnachweis "aus dem Lageplan" mit
30 * dem heutigen Datum, obwohl der Kopfkommentar von `uebernimmWege` zusagt, das
31 * sei die einzige Stelle, die ihn vergibt, "und sie vergibt ihn nur fuer die
32 * Beziehungen, deren Wege sie im selben Zug schreibt".
33 *
34 * GEGEN DEN ALTSTAND schlagen die beiden Faelle mit "->" in der Kennung fehl;
35 * die Gegenprobe mit gewoehnlichen Kennungen besteht dort ebenso.
36 */
37
38 const DATUM = new Date('2026-01-01T00:00:00Z');
39
40 /** 1 Bildpunkt = 1 m, damit sich die Masse unmittelbar ablesen lassen. */
41 const KALIBRIERUNG = {
42 von: { x: 0, y: 0 },
43 bis: { x: 400, y: 0 },
44 laengeMeter: 400,
45 herkunft: 'gemessen',
46 } as const;
47
48 /** Zufahrt und Furt einer Kreuzung, um `versatz` Meter nach Osten geschoben. */
49 function haltlinienPaar(nummer: number, versatz: number): Haltlinie[] {
50 return [
51 {
52 id: `hl-zufahrt-${nummer}`,
53 name: `Zufahrt ${nummer}`,
54 art: 'zufahrt',
55 armId: null,
56 von: { x: versatz, y: 0 },
57 bis: { x: versatz + 20, y: 0 },
58 },
59 {
60 id: `hl-furt-${nummer}`,
61 name: `Furt ${nummer}`,
62 art: 'querung',
63 armId: null,
64 von: { x: versatz, y: 30 },
65 bis: { x: versatz + 30, y: 30 },
66 },
67 ];
68 }
69
70 /** Geradeausstrom und kreuzende Furt; sie schneiden sich bei (versatz+10 | 30). */
71 function linienPaar(
72 nummer: number,
73 versatz: number,
74 kennungGerade: string,
75 kennungFurt: string,
76 ): Planlinie[] {
77 return [
78 {
79 id: kennungGerade,
80 name: `L${nummer} geradeaus`,
81 mode: 'kfz',
82 movement: 'geradeaus',
83 breiteMeter: REGELBREITE.kfz,
84 punkte: [
85 { x: versatz + 10, y: 0 },
86 { x: versatz + 10, y: 60 },
87 ],
88 haltlinieId: `hl-zufahrt-${nummer}`,
89 startT: 0.5,
90 signalGroupId: `sg-k${nummer}`,
91 },
92 {
93 id: kennungFurt,
94 name: `L${nummer} Furt`,
95 mode: 'fuss',
96 movement: 'querung',
97 breiteMeter: REGELBREITE.fuss,
98 punkte: [
99 { x: versatz, y: 30 },
100 { x: versatz + 30, y: 30 },
101 ],
102 haltlinieId: `hl-furt-${nummer}`,
103 startT: 0,
104 signalGroupId: `sg-f${nummer}`,
105 },
106 ];
107 }
108
109 const gruppe = (id: string, patch: Partial<SignalGroup> = {}): SignalGroup => ({
110 id,
111 name: id,
112 mode: 'kfz',
113 movement: 'geradeaus',
114 armId: null,
115 vZul: 50,
116 lanes: 1,
117 vehicleClass: 'pkw',
118 minGreenOverride: null,
119 maxGreenOverride: null,
120 reducedMobility: false,
121 color: '#2f6fd0',
122 ...patch,
123 });
124
125 /** Die von Hand gepflegte Beziehung der ZWEITEN Kreuzung - 99 m Raeumweg. */
126 const VON_HAND: Conflict = {
127 id: 'cf-hand',
128 fromId: 'sg-k2',
129 toId: 'sg-f2',
130 clearingDistance: 99,
131 enteringDistance: 4,
132 clearingSpeedOverride: null,
133 enteringSpeedOverride: null,
134 crossingTimeOverride: null,
135 manualIntergreen: null,
136 wegnachweis: WEGNACHWEIS_HANDEINGABE,
137 note: 'Von Hand nachgemessen.',
138 };
139
140 /**
141 * Zwei getrennte Kreuzungen in EINEM Lageplan, mit frei waehlbaren
142 * Planlinienkennungen.
143 */
144 function aufbau(kennungen: readonly (readonly [string, string])[]): {
145 project: Project;
146 lageplan: Lageplan;
147 } {
148 const haltlinien = kennungen.flatMap((_, i) => haltlinienPaar(i + 1, i * 100));
149 const linien = kennungen.flatMap(([gerade, furt], i) => linienPaar(i + 1, i * 100, gerade, furt));
150 const lageplan: Lageplan = {
151 bild: null,
152 kalibrierung: KALIBRIERUNG,
153 haltlinien,
154 linien,
155 arbeitsbereiche: [],
156 signalgeber: [],
157 };
158 const leer = createEmptyProject('Planlinienkennungen mit Pfeil', DATUM);
159 return {
160 project: {
161 ...leer,
162 signalGroups: kennungen.flatMap((_, i) => [
163 gruppe(`sg-k${i + 1}`),
164 gruppe(`sg-f${i + 1}`, { mode: 'fuss', movement: 'querung', vehicleClass: 'keine' }),
165 ]),
166 conflicts: [VON_HAND],
167 lageplan,
168 },
169 lageplan,
170 };
171 }
172
173 /** Der Weg der ERSTEN Kreuzung, vom Geradeausstrom auf seine Furt. */
174 function wegDerErstenKreuzung(lageplan: Lageplan, ableitung: ReturnType<typeof leiteAb>): string[] {
175 const erste = lageplan.linien[0]!.id;
176 const zweite = lageplan.linien[1]!.id;
177 const weg = ableitung.wege.find((w) => w.vonLinieId === erste && w.nachLinieId === zweite);
178 // Vorbedingung jedes Falls: Ohne diesen Weg prueft nichts darunter etwas.
179 expect(weg, 'die Ströme der ersten Kreuzung schneiden sich im Plan nicht').toBeDefined();
180 return [wegSchluessel(weg!)];
181 }
182
183 describe('Befund 43 - Wegeuebernahme bei "->" in der Planlinienkennung', () => {
184 it('haelt zwei Wege auseinander, die denselben zusammengeklebten Schluessel ergaeben', () => {
185 const { project, lageplan } = aufbau([
186 ['a->b', 'c'],
187 ['a', 'b->c'],
188 ]);
189 const ableitung = leiteAb(project, lageplan);
190
191 const erster = ableitung.wege.find((w) => w.vonLinieId === 'a->b' && w.nachLinieId === 'c');
192 const zweiter = ableitung.wege.find((w) => w.vonLinieId === 'a' && w.nachLinieId === 'b->c');
193 expect(erster, 'Weg der ersten Kreuzung').toBeDefined();
194 expect(zweiter, 'Weg der zweiten Kreuzung').toBeDefined();
195
196 // Der Kern: zwei sachlich verschiedene Wege, zwei verschiedene Schluessel.
197 expect(wegSchluessel(erster!)).not.toBe(wegSchluessel(zweiter!));
198 });
199
200 it('uebernimmt nur die angehakte Beziehung und laesst die andere unangetastet', () => {
201 const { project, lageplan } = aufbau([
202 ['a->b', 'c'],
203 ['a', 'b->c'],
204 ]);
205 const ableitung = leiteAb(project, lageplan);
206 const ergebnis = uebernimmWege(
207 project,
208 lageplan,
209 ableitung,
210 wegDerErstenKreuzung(lageplan, ableitung),
211 DATUM,
212 );
213
214 expect(ergebnis.uebernommen).toBe(1);
215
216 // Die nicht angehakte Beziehung behaelt ihren von Hand gepflegten,
217 // laengeren Raeumweg - und ihren Herkunftsnachweis.
218 const zweite = ergebnis.project.conflicts.find(
219 (c) => c.fromId === 'sg-k2' && c.toId === 'sg-f2',
220 );
221 expect(zweite?.clearingDistance).toBe(99);
222 expect(zweite?.wegnachweis.herkunft).toBe('handeingabe');
223 expect(zweite?.note).toBe('Von Hand nachgemessen.');
224
225 // Und die angehakte ist wirklich geschrieben worden.
226 const erste = ergebnis.project.conflicts.find(
227 (c) => c.fromId === 'sg-k1' && c.toId === 'sg-f1',
228 );
229 expect(erste?.wegnachweis.herkunft).toBe('lageplan');
230 });
231
232 it('uebernimmt bei gewoehnlichen Kennungen ebenfalls nur die angehakte (Gegenprobe)', () => {
233 const { project, lageplan } = aufbau([
234 ['l-gerade-1', 'l-furt-1'],
235 ['l-gerade-2', 'l-furt-2'],
236 ]);
237 const ableitung = leiteAb(project, lageplan);
238 const ergebnis = uebernimmWege(
239 project,
240 lageplan,
241 ableitung,
242 wegDerErstenKreuzung(lageplan, ableitung),
243 DATUM,
244 );
245
246 expect(ergebnis.uebernommen).toBe(1);
247 const zweite = ergebnis.project.conflicts.find(
248 (c) => c.fromId === 'sg-k2' && c.toId === 'sg-f2',
249 );
250 expect(zweite?.clearingDistance).toBe(99);
251 expect(zweite?.wegnachweis.herkunft).toBe('handeingabe');
252 });
253 });