lsa-planer

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

/ tests domain zufahrtStroeme.test.ts

10,6 KB Rohdatei
tests/domain/zufahrtStroeme.test.ts — 340 Zeilen
1 import { describe, expect, it } from 'vitest';
2 import {
3 REGELBREITE,
4 kreuzenSich,
5 schneide,
6 schneideMitBefund,
7 ueberlappung,
8 vermesseWege,
9 type Fahrlinie,
10 type Strombezug,
11 } from '@/domain/geometrie/vermessung';
12
13 /**
14 * Stroeme aus derselben Zufahrt.
15 *
16 * Anlass ist eine Frage des Anwenders: "Ich kann doch von einer Richtung
17 * geradeaus fahren oder auch links abbiegen, wie werden solche Zustaende
18 * eingezeichnet?" Die Antwort lautet: als zwei Fahrlinien mit gemeinsamer
19 * Haltlinie. Genau dabei fiel auf, dass die Vermassung diesen gemeinsamen
20 * Anfangspunkt als Kreuzung wertete und damit zwei Stroeme als feindlich
21 * meldete, die gleichzeitig Freigabe erhalten muessen.
22 *
23 * Diese Faelle sichern die Ausnahme ab UND ihre Grenzen. Jeder Fall, der
24 * "feindlich" erwartet, verteidigt einen echten Konflikt gegen die Ausnahme -
25 * das ist der wichtigere Teil.
26 */
27
28 const HALTLINIE = 'hl-nord';
29
30 function mitBezug(linie: Fahrlinie, bezug: Partial<Strombezug> = {}): Fahrlinie {
31 return {
32 ...linie,
33 bezug: {
34 haltlinieId: HALTLINIE,
35 haltlinienArt: 'zufahrt',
36 mode: 'kfz',
37 startT: 0.5,
38 ...bezug,
39 },
40 };
41 }
42
43 /** Aus der Nordzufahrt geradeaus. */
44 const GERADEAUS: Fahrlinie = {
45 id: 'nord-geradeaus',
46 punkte: [
47 { x: 0, y: 0 },
48 { x: 0, y: 40 },
49 ],
50 breite: REGELBREITE.kfz,
51 };
52
53 /** Aus derselben Zufahrt links abbiegend, im Bogen. */
54 const LINKS: Fahrlinie = {
55 id: 'nord-links',
56 punkte: [
57 { x: 0, y: 0 },
58 { x: 5, y: 18 },
59 { x: 40, y: 22 },
60 ],
61 breite: REGELBREITE.kfz,
62 };
63
64 describe('Stroeme derselben Zufahrt', () => {
65 it('wertet den gemeinsamen Haltlinienpunkt nicht als Kreuzung', () => {
66 const a = mitBezug(GERADEAUS);
67 const b = mitBezug(LINKS);
68
69 expect(schneide(a, b)).toHaveLength(0);
70 expect(kreuzenSich(a, b)).toBe(false);
71 expect(vermesseWege(a, b)).toBeNull();
72 });
73
74 it('weist die unterdrueckte Beruehrung aus, statt sie zu verschweigen', () => {
75 const befund = schneideMitBefund(mitBezug(GERADEAUS), mitBezug(LINKS));
76 expect(befund.unterdrueckt).toBe(1);
77 expect(befund.schnitte).toHaveLength(0);
78 });
79
80 it('verhaelt sich ohne Zufahrtsangabe wie bisher', () => {
81 // Uebernommene Projekte haben keine Haltlinien. Sie duerfen sich nicht
82 // stillschweigend anders verhalten; ohne Zuordnung entscheidet allein die
83 // Zeichnung, und das ist die sichere Seite.
84 expect(kreuzenSich(GERADEAUS, LINKS)).toBe(true);
85 });
86
87 it('haengt nicht an Millimetern im Startpunkt', () => {
88 // Der eigentliche Gewinn gegenueber einer Abstandstoleranz: Die Entscheidung
89 // faellt ueber die Zugehoerigkeit, nicht ueber die Lage. Ein Millimeter
90 // Versatz beim Klicken darf sie nicht kippen.
91 for (const versatz of [-0.001, 0, 0.001]) {
92 const verschoben: Fahrlinie = {
93 ...LINKS,
94 punkte: [{ x: versatz, y: versatz }, ...LINKS.punkte.slice(1)],
95 };
96 expect(kreuzenSich(mitBezug(GERADEAUS), mitBezug(verschoben))).toBe(false);
97 }
98 });
99
100 it('greift nur bei ein und derselben Haltlinie', () => {
101 // Versetzte Haltlinien, aufgeweiteter Radaufstellstreifen, vorgelagerter
102 // Teilknoten: zwei Objekte, also kein gemeinsamer Aufstellbereich.
103 expect(kreuzenSich(mitBezug(GERADEAUS), mitBezug(LINKS, { haltlinieId: 'hl-nord-2' }))).toBe(
104 true,
105 );
106 });
107
108 it('greift nicht bei fehlender Zuordnung einer der beiden Linien', () => {
109 expect(kreuzenSich(mitBezug(GERADEAUS), mitBezug(LINKS, { haltlinieId: null }))).toBe(true);
110 });
111
112 it('schuetzt den Radstrom derselben Zufahrt', () => {
113 // Der Rechtsabbieger gegen den parallel gefuehrten Radverkehr ist einer der
114 // sicherheitskritischsten Konflikte an einer Lichtsignalanlage. Er darf nie
115 // ausgeblendet werden, auch nicht bei gemeinsamer Haltlinie.
116 const rad = mitBezug({ ...LINKS, breite: REGELBREITE.rad }, { mode: 'rad' });
117 expect(kreuzenSich(mitBezug(GERADEAUS), rad)).toBe(true);
118 });
119
120 it('nimmt Furten aus, die am selben Arm liegen', () => {
121 // Eine Furt gehoert zum Knotenpunktarm, nicht zu dessen Zufahrt. Sie ist
122 // gegenueber den einfahrenden Stroemen ihres eigenen Arms feindlich.
123 const furt = mitBezug({ ...LINKS, breite: REGELBREITE.fuss }, { haltlinienArt: 'querung' });
124 const strom = mitBezug(GERADEAUS, { haltlinienArt: 'querung' });
125 expect(kreuzenSich(strom, furt)).toBe(true);
126 });
127
128 it('erkennt den Linksabbieger vom rechten Fahrstreifen als echte Kreuzung', () => {
129 // Er beginnt rechts neben dem Geradeausstrom und schneidet dessen Bahn zwei
130 // Meter hinter der Haltlinie. Das ist ein echter Konflikt, auch bei
131 // gemeinsamer Haltlinie - die Ausnahme greift nur im gemeinsamen Ursprung.
132 const linksVonRechts = mitBezug({
133 id: 'nord-links-von-rechts',
134 punkte: [
135 { x: 3, y: 0 },
136 { x: -6, y: 6 },
137 { x: -40, y: 8 },
138 ],
139 breite: REGELBREITE.kfz,
140 });
141 expect(kreuzenSich(mitBezug(GERADEAUS), linksVonRechts)).toBe(true);
142 });
143
144 it('unterdrueckt nichts, was einen halben Meter hinter der Haltlinie liegt', () => {
145 // Die Grenze der Ausnahme, ausdruecklich festgehalten: Zwei Stroeme, die
146 // an derselben Haltlinie beginnen und sich erst dahinter schneiden, bleiben
147 // feindlich. Die Entscheidung darueber gehoert dem Planer, nicht der
148 // Geometrie - die Ableitung legt sie ihm einzeln vor.
149 const kreuzendNahDahinter = mitBezug({
150 id: 'nord-kreuzend',
151 punkte: [
152 { x: 2.5, y: 0 },
153 { x: -2.5, y: 5 },
154 { x: -30, y: 8 },
155 ],
156 breite: REGELBREITE.kfz,
157 });
158 expect(kreuzenSich(mitBezug(GERADEAUS), kreuzendNahDahinter)).toBe(true);
159 });
160
161 it('blendet nur den gemeinsamen Anfang aus, nicht spaetere Kreuzungen', () => {
162 // Ein Abbieger, der weiter im Knotenpunkt in die Bahn seines Nachbarn
163 // zurueckschwenkt, ist ein Konflikt.
164 const zurueckSchwenkend = mitBezug({
165 id: 'nord-links-zurueck',
166 punkte: [
167 { x: 0, y: 0 },
168 { x: 12, y: 15 },
169 { x: -12, y: 30 },
170 ],
171 breite: REGELBREITE.kfz,
172 });
173 expect(kreuzenSich(mitBezug(GERADEAUS), zurueckSchwenkend)).toBe(true);
174 });
175
176 it('behandelt die Zusammenfuehrung in eine Ausfahrt weiterhin als Konflikt', () => {
177 // Gemeinsamer ENDpunkt: zwei Stroeme laufen in dieselbe Ausfahrt ein. Eine
178 // pauschale Regel "Beruehrung im Endpunkt zaehlt nie" verschluckte das.
179 const vonWesten: Fahrlinie = {
180 id: 'west',
181 punkte: [
182 { x: -40, y: 60 },
183 { x: 0, y: 40 },
184 ],
185 breite: REGELBREITE.kfz,
186 };
187 expect(kreuzenSich(mitBezug(GERADEAUS), mitBezug(vonWesten, { haltlinieId: 'hl-west' }))).toBe(
188 true,
189 );
190 });
191 });
192
193 describe('Doppelzaehlung und Ueberlagerung', () => {
194 it('zaehlt einen Schnitt auf einem geteilten Stuetzpunkt nur einmal', () => {
195 const knick: Fahrlinie = {
196 id: 'knick',
197 punkte: [
198 { x: -10, y: -10 },
199 { x: 0, y: 0 },
200 { x: 10, y: -10 },
201 ],
202 breite: REGELBREITE.kfz,
203 };
204 const quer: Fahrlinie = {
205 id: 'quer',
206 punkte: [
207 { x: -20, y: 0 },
208 { x: 20, y: 0 },
209 ],
210 breite: REGELBREITE.kfz,
211 };
212 const befund = schneideMitBefund(knick, quer);
213 expect(befund.schnitte).toHaveLength(1);
214 expect(befund.doppelt).toBe(1);
215 });
216
217 it('findet zwei Querungen derselben Furt als zwei Schnitte', () => {
218 // Hier darf NICHT ueber den geometrischen Punkt entdoppelt werden: Der Strom
219 // durchfaehrt die Furt an zwei verschiedenen Stellen seines Wegs.
220 const wende: Fahrlinie = {
221 id: 'wende',
222 punkte: [
223 { x: -10, y: -5 },
224 { x: -10, y: 5 },
225 { x: 10, y: 5 },
226 { x: 10, y: -5 },
227 ],
228 breite: REGELBREITE.kfz,
229 };
230 const furt: Fahrlinie = {
231 id: 'furt',
232 punkte: [
233 { x: -20, y: 0 },
234 { x: 20, y: 0 },
235 ],
236 breite: REGELBREITE.fuss,
237 };
238 expect(schneide(wende, furt)).toHaveLength(2);
239 });
240
241 it('findet zwei Stroeme, die sich einen Fahrstreifen teilen', () => {
242 // Bisher der einzige Fall, in dem die Vermassung in die UNSICHERE Richtung
243 // falsch lag: Gleichlaufende Segmente liefern keinen Punktschnitt, die
244 // Stroeme galten damit als vertraeglich.
245 const vorne: Fahrlinie = {
246 id: 'vorne',
247 punkte: [
248 { x: 0, y: 0 },
249 { x: 40, y: 0 },
250 ],
251 breite: REGELBREITE.kfz,
252 };
253 const hinten: Fahrlinie = {
254 id: 'hinten',
255 punkte: [
256 { x: 10, y: 0 },
257 { x: 50, y: 0 },
258 ],
259 breite: REGELBREITE.kfz,
260 };
261 const ueber = ueberlappung(vorne, hinten);
262 expect(ueber).not.toBeNull();
263 expect(ueber?.laenge).toBeCloseTo(30, 3);
264
265 const wege = vermesseWege(vorne, hinten);
266 expect(wege).not.toBeNull();
267 expect(wege?.konfliktbereich.ausUeberlappung).toBe(true);
268 });
269
270 it('sieht keine Ueberlagerung bei getrennt gefuehrten Nachbarstroemen', () => {
271 // Zwei benachbarte Fahrstreifen: 3,5 m Achsabstand bei 3,25 m Korridorbreite -
272 // die Korridore beruehren sich nicht.
273 const links: Fahrlinie = {
274 id: 'fs-links',
275 punkte: [
276 { x: 0, y: 0 },
277 { x: 40, y: 0 },
278 ],
279 breite: REGELBREITE.kfz,
280 };
281 const rechts: Fahrlinie = {
282 id: 'fs-rechts',
283 punkte: [
284 { x: 0, y: 3.5 },
285 { x: 40, y: 3.5 },
286 ],
287 breite: REGELBREITE.kfz,
288 };
289 expect(ueberlappung(links, rechts)).toBeNull();
290 });
291
292 it('erkennt eine Furt, die laengs einer Fahrlinie verlaeuft', () => {
293 // Gegenlaeufig gezeichnet - die Richtung darf keine Rolle spielen.
294 const fahrlinie: Fahrlinie = {
295 id: 'kfz',
296 punkte: [
297 { x: 0, y: 0 },
298 { x: 40, y: 0 },
299 ],
300 breite: REGELBREITE.kfz,
301 };
302 const furt: Fahrlinie = {
303 id: 'furt-laengs',
304 punkte: [
305 { x: 30, y: 1 },
306 { x: 5, y: 1 },
307 ],
308 breite: REGELBREITE.fuss,
309 };
310 const ueber = ueberlappung(fahrlinie, furt);
311 expect(ueber).not.toBeNull();
312 expect(ueber?.laenge).toBeCloseTo(25, 3);
313 });
314
315 it('weist den Winkel des massgebenden Schnitts aus, nicht den des ersten', () => {
316 // Ein Strom wird zuerst spitz, dann rechtwinklig gekreuzt. Fuer den Raeumweg
317 // zaehlt der hintere Schnitt - und damit dessen Winkel.
318 const raeumend: Fahrlinie = {
319 id: 'raeumend',
320 punkte: [
321 { x: 0, y: 0 },
322 { x: 60, y: 0 },
323 ],
324 breite: REGELBREITE.kfz,
325 };
326 const einfahrend: Fahrlinie = {
327 id: 'einfahrend',
328 punkte: [
329 { x: 5, y: -2 },
330 { x: 25, y: 2 },
331 { x: 45, y: -20 },
332 { x: 45, y: 20 },
333 ],
334 breite: REGELBREITE.kfz,
335 };
336 const wege = vermesseWege(raeumend, einfahrend);
337 expect(wege).not.toBeNull();
338 expect(wege?.winkelGrad).toBeCloseTo(90, 0);
339 });
340 });