lsa-planer

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

/ tests domain zufahrtAbleitung.test.ts

7,7 KB Rohdatei
tests/domain/zufahrtAbleitung.test.ts — 209 Zeilen
1 import { describe, expect, it } from 'vitest';
2 import { leiteAb } from '@/domain/geometrie/ableitung';
3 import { REGELBREITE } from '@/domain/geometrie/vermessung';
4 import type { Haltlinie, Lageplan, Planlinie } from '@/domain/geometrie/lageplan';
5 import { createEmptyProject } from '@/domain/model/factory';
6 import type { Movement } from '@/domain/rilsa/types';
7
8 /**
9 * Vierarmiger Knotenpunkt mit je drei Fahrbeziehungen.
10 *
11 * Genau die Zeichnung, nach der der Anwender gefragt hat: aus jeder Zufahrt
12 * geradeaus, links und rechts. Zwoelf Stroeme, alle an vier Haltlinien.
13 *
14 * Der Fall ist der Massstab fuer die ganze Aenderung. Ohne Haltlinien meldet
15 * das Programm zwoelf Konflikte, die keine sind - je Zufahrt drei Paare, die
16 * sich nur ihren Anfangspunkt teilen. Mit Haltlinien duerfen es null sein, und
17 * kein einziger echter Konflikt darf dabei verlorengehen.
18 *
19 * Massstab: 1 Bildpunkt = 1 m, damit sich die Zahlen von Hand nachrechnen
20 * lassen. Der Knotenpunkt liegt im Ursprung, die Zufahrten enden 8 m davor.
21 */
22
23 const FIXED_DATE = new Date('2026-01-01T00:00:00Z');
24
25 interface Arm {
26 readonly id: string;
27 readonly name: string;
28 /** Einheitsvektor von der Haltlinie zum Knotenpunktmittelpunkt. */
29 readonly ein: { x: number; y: number };
30 }
31
32 const ARME: readonly Arm[] = [
33 { id: 'nord', name: 'Nord', ein: { x: 0, y: 1 } },
34 { id: 'ost', name: 'Ost', ein: { x: -1, y: 0 } },
35 { id: 'sued', name: 'Sued', ein: { x: 0, y: -1 } },
36 { id: 'west', name: 'West', ein: { x: 1, y: 0 } },
37 ];
38
39 /** Abstand der Haltlinie vom Mittelpunkt. */
40 const HALT = 8;
41 /** Wie weit die Fahrlinien ueber den Knotenpunkt hinauslaufen. */
42 const AUS = 30;
43
44 function drehe(v: { x: number; y: number }, links: boolean): { x: number; y: number } {
45 // Linksdrehung um 90 Grad im Bildkoordinatensystem.
46 return links ? { x: v.y, y: -v.x } : { x: -v.y, y: v.x };
47 }
48
49 /**
50 * Baut einen Knotenpunkt.
51 * `mitHaltlinien` steuert, ob die Stroeme ihrer Zufahrt zugeordnet sind.
52 */
53 function knotenpunkt(mitHaltlinien: boolean): Lageplan {
54 const haltlinien: Haltlinie[] = [];
55 const linien: Planlinie[] = [];
56
57 for (const arm of ARME) {
58 const start = { x: -arm.ein.x * HALT, y: -arm.ein.y * HALT };
59 // Die Haltlinie liegt quer zur Fahrtrichtung, 6 m breit.
60 const quer = drehe(arm.ein, true);
61 const haltlinie: Haltlinie = {
62 id: `hl-${arm.id}`,
63 name: arm.name,
64 art: 'zufahrt',
65 armId: arm.id,
66 von: { x: start.x - quer.x * 3, y: start.y - quer.y * 3 },
67 bis: { x: start.x + quer.x * 3, y: start.y + quer.y * 3 },
68 };
69 haltlinien.push(haltlinie);
70
71 const beziehungen: readonly { movement: Movement; links: boolean | null }[] = [
72 { movement: 'geradeaus', links: null },
73 { movement: 'links', links: true },
74 { movement: 'rechts', links: false },
75 ];
76
77 beziehungen.forEach((b, index) => {
78 // Alle drei beginnen in der Mitte der Haltlinie - der Regelfall, wenn der
79 // Anwender sie nacheinander auf dieselbe Stelle setzt.
80 const punkte = [start, { x: 0, y: 0 }];
81 const richtung = b.links === null ? arm.ein : drehe(arm.ein, b.links);
82 punkte.push({ x: richtung.x * AUS, y: richtung.y * AUS });
83
84 linien.push({
85 id: `${arm.id}-${b.movement}`,
86 name: `${arm.name} ${b.movement}`,
87 mode: 'kfz',
88 movement: b.movement,
89 breiteMeter: REGELBREITE.kfz,
90 punkte,
91 haltlinieId: mitHaltlinien ? haltlinie.id : null,
92 startT: 0.5 + index * 0.0001,
93 signalGroupId: null,
94 });
95 });
96 }
97
98 return {
99 arbeitsbereiche: [],
100 signalgeber: [],
101 bild: {
102 datenUrl: 'data:image/png;base64,AAAA',
103 breite: 200,
104 hoehe: 200,
105 herkunft: 'Test',
106 geladenAm: FIXED_DATE.toISOString(),
107 },
108 // 1 Bildpunkt = 1 m.
109 kalibrierung: {
110 von: { x: 0, y: 0 },
111 bis: { x: 100, y: 0 },
112 laengeMeter: 100,
113 herkunft: 'georeferenziert',
114 },
115 haltlinien,
116 linien,
117 };
118 }
119
120 function projektMitArmen() {
121 const leer = createEmptyProject('Vierarmig', FIXED_DATE);
122 return {
123 ...leer,
124 intersection: {
125 ...leer.intersection,
126 arms: ARME.map((a) => ({ id: a.id, name: a.name, direction: a.name, lanes: 1, vZul: 50 })),
127 },
128 };
129 }
130
131 /** Paare, die derselben Zufahrt angehoeren. */
132 function ausGleicherZufahrt(aId: string, bId: string): boolean {
133 return (aId.split('-')[0] ?? '') === (bId.split('-')[0] ?? '');
134 }
135
136 describe('Vierarmiger Knotenpunkt mit je drei Fahrbeziehungen', () => {
137 it('meldet ohne Haltlinien zwoelf Konflikte, die keine sind', () => {
138 // Der Zustand vor der Aenderung - und der Zustand jedes uebernommenen
139 // Projekts. Er wird hier festgehalten, damit der Gewinn messbar bleibt.
140 const ableitung = leiteAb(projektMitArmen(), knotenpunkt(false));
141 const falsch = ableitung.vertraeglichkeit.filter(
142 (v) => v.feindlich && ausGleicherZufahrt(v.aLinieId, v.bLinieId),
143 );
144 // Vier Zufahrten mit je drei Stroemen: drei Paare je Zufahrt.
145 expect(falsch).toHaveLength(12);
146 });
147
148 it('meldet mit Haltlinien keinen einzigen davon', () => {
149 const ableitung = leiteAb(projektMitArmen(), knotenpunkt(true));
150 const falsch = ableitung.vertraeglichkeit.filter(
151 (v) => v.feindlich && ausGleicherZufahrt(v.aLinieId, v.bLinieId),
152 );
153 expect(falsch).toHaveLength(0);
154 });
155
156 it('verliert dabei keinen echten Konflikt zwischen den Zufahrten', () => {
157 const ohne = leiteAb(projektMitArmen(), knotenpunkt(false));
158 const mit = leiteAb(projektMitArmen(), knotenpunkt(true));
159
160 const echt = (a: typeof ohne) =>
161 a.vertraeglichkeit
162 .filter((v) => v.feindlich && !ausGleicherZufahrt(v.aLinieId, v.bLinieId))
163 .map((v) => `${v.aLinieId}|${v.bLinieId}`)
164 .sort();
165
166 expect(echt(mit)).toEqual(echt(ohne));
167 // Und es sind wirklich welche da - sonst prueft der Vergleich nichts.
168 expect(echt(mit).length).toBeGreaterThan(10);
169 });
170
171 it('weist die unterdrueckten Beruehrungen aus', () => {
172 const ableitung = leiteAb(projektMitArmen(), knotenpunkt(true));
173 expect(ableitung.unterdrueckt).toHaveLength(12);
174 for (const paar of ableitung.unterdrueckt) {
175 expect(ausGleicherZufahrt(paar.aLinieId, paar.bLinieId)).toBe(true);
176 expect(paar.anzahl).toBeGreaterThan(0);
177 }
178 });
179
180 it('erkennt die einstreifige Zufahrt als gemeinsamen Fahrstreifen', () => {
181 // In dieser Zeichnung laufen alle drei Stroeme einer Zufahrt acht Meter auf
182 // demselben Fahrstreifen und trennen sich erst im Knotenpunkt - der
183 // Regelfall einer einstreifigen Zufahrt. Sie sind vertraeglich, muessen aber
184 // denselben Signalgeber bekommen; getrennt signalisieren laesst sich nur,
185 // was einen eigenen Fahrstreifen hat. Genau das sagt die Begruendung.
186 const ableitung = leiteAb(projektMitArmen(), knotenpunkt(true));
187 const paar = ableitung.vertraeglichkeit.find(
188 (v) => v.aLinieId === 'nord-geradeaus' && v.bLinieId === 'nord-links',
189 );
190 expect(paar).toBeDefined();
191 expect(paar?.feindlich).toBe(false);
192 expect(paar?.grund).toBe('gemeinsamer-fahrstreifen');
193 expect(paar?.begruendung).toContain('dieselbe Signalgruppe');
194 });
195
196 it('schlaegt keine Zwischenzeit zwischen Stroemen derselben Zufahrt vor', () => {
197 const ableitung = leiteAb(projektMitArmen(), knotenpunkt(true));
198 const falsch = ableitung.wege.filter((w) => ausGleicherZufahrt(w.vonLinieId, w.nachLinieId));
199 expect(falsch).toHaveLength(0);
200 });
201
202 it('warnt, solange Fahrlinien keiner Haltlinie zugeordnet sind', () => {
203 const ableitung = leiteAb(projektMitArmen(), knotenpunkt(false));
204 const hinweis = ableitung.hinweise.find((h) => h.code === 'haltlinie-fehlt');
205 expect(hinweis).toBeDefined();
206 // Die Anzeige blendet blosse Hinweise aus - die Schwere muss hoeher sein.
207 expect(hinweis?.severity).toBe('warnung');
208 });
209 });