lsa-planer

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

/ src domain geometrie vermessung.ts

38,9 KB Rohdatei
src/domain/geometrie/vermessung.ts — 983 Zeilen
1 import type { Meters } from '../units';
2 import { isFiniteNumber, roundTo } from '../units';
3 import type { Movement, TrafficMode } from '../rilsa/types';
4
5 /**
6 * Vermessung aus gezeichneter Geometrie.
7 *
8 * Ziel: Raeum- und Einfahrweg nicht mehr von Hand abmessen und abtippen,
9 * sondern aus den im Lageplan gezeichneten Fahrlinien ableiten.
10 *
11 * DIE ENTSCHEIDENDE STELLE
12 *
13 * Ein Konfliktbereich ist keine Punktueberschneidung. Fahrzeuge haben eine
14 * Breite; der Bereich, in dem sich zwei Stroeme ins Gehege kommen, hat deshalb
15 * eine Ausdehnung. Nach RiLSA gilt:
16 *
17 * Raeumweg sr Haltlinie des raeumenden Stroms bis zum ENDE des Konfliktbereichs
18 * Einfahrweg se Haltlinie des einfahrenden Stroms bis zum BEGINN des Konfliktbereichs
19 *
20 * Modelliert wird jeder Strom als Mittellinie mit einer Breite. Kreuzen sich
21 * zwei Mittellinien unter dem Winkel theta, so durchfaehrt Strom A den Korridor
22 * von Strom B ueber die Laenge
23 *
24 * l = b_B / sin(theta)
25 *
26 * mit b_B als Breite von B. Bei rechtwinkliger Kreuzung ist das genau die
27 * Breite von B; bei spitzem Winkel entsprechend mehr - was fachlich richtig ist,
28 * denn ein schraeg kreuzendes Fahrzeug ist laenger im Konfliktbereich.
29 *
30 * Daraus folgt fuer den Schnittpunkt im Abstand d entlang der jeweiligen Linie:
31 *
32 * sr(A) = d_A + (b_B / 2) / sin(theta) bis zur hinteren Kante
33 * se(B) = d_B − (b_A / 2) / sin(theta) bis zur vorderen Kante
34 *
35 * Die Fahrzeuglaenge kommt NICHT hier dazu - sie wird in der
36 * Zwischenzeitberechnung getrennt beruecksichtigt, damit im Pruefbericht
37 * nachvollziehbar bleibt, woraus sich der Raeumweg zusammensetzt.
38 *
39 * Fuer Fussgaengerfurten und Radquerungen gilt statt der Zeichenrichtung die
40 * unguenstigere Richtung - siehe `vermesseWege`.
41 */
42
43 export interface Punkt {
44 readonly x: Meters;
45 readonly y: Meters;
46 }
47
48 /**
49 * Woher der Strom kommt.
50 *
51 * Die einzige fachliche Angabe, die dieses Modul kennt - und es braucht sie:
52 * Ohne sie laesst sich nicht entscheiden, ob eine Beruehrung kurz hinter dem
53 * Start ein Konflikt ist oder das Auffaechern zweier Stroeme aus demselben
54 * Querschnitt. Aus derselben Zufahrt geradeaus und links abzubiegen ergibt
55 * zwei Fahrlinien mit gemeinsamem Anfangspunkt; rein geometrisch ist das eine
56 * Kreuzung, fachlich ist es keine.
57 */
58 export interface Strombezug {
59 /** `null` heisst "keiner Haltlinie zugeordnet" - dann greift keine Ausnahme. */
60 readonly haltlinieId: string | null;
61 readonly haltlinienArt: 'zufahrt' | 'querung' | null;
62 readonly mode: TrafficMode;
63 /**
64 * Fahrbeziehung der gezeichneten Linie. `fahrlinienVon` gibt sie jeder Linie
65 * mit; fehlt sie, ist sie unbekannt, und `querungsart` behandelt sie wie
66 * eine Querung - unbekannt heisst sicher.
67 */
68 readonly movement?: Movement;
69 /** Lage auf der Haltlinie, 0..1 - gibt die Reihenfolge im Querschnitt. */
70 readonly startT: number;
71 }
72
73 /**
74 * Fahrlinie eines Verkehrsstroms.
75 *
76 * Der erste Punkt ist die Haltlinie; von dort werden alle Wege gemessen. Die
77 * weiteren Punkte beschreiben den Fahrweg durch den Knotenpunkt - bei
78 * Abbiegern also einen Bogen, angenaehert durch einen Linienzug.
79 *
80 * AUSNAHME QUERUNG: Eine Furt oder Radquerung wird von beiden Enden aus
81 * gemessen, und es gilt die unguenstigere Richtung (`querungsart`,
82 * `vermesseWege`). Ihr erster Punkt sagt nur, wo die Zeichnung begann.
83 */
84 export interface Fahrlinie {
85 readonly id: string;
86 /** Mindestens zwei Punkte; der erste liegt auf der Haltlinie. */
87 readonly punkte: readonly Punkt[];
88 /** Breite des beanspruchten Korridors in Metern. */
89 readonly breite: Meters;
90 /**
91 * Herkunft des Stroms.
92 *
93 * Bewusst wahlfrei: Fehlt sie, verhaelt sich alles wie zuvor. Damit bleiben
94 * uebernommene Projekte, in denen keine Haltlinien gezeichnet sind, exakt bei
95 * ihrem bisherigen Verhalten - die sichere Seite, denn ohne Zuordnung wird
96 * keine Beruehrung ausgeblendet.
97 */
98 readonly bezug?: Strombezug;
99 }
100
101 /** Uebliche Korridorbreiten, wenn nichts anderes bekannt ist. */
102 export const REGELBREITE = {
103 /** Fahrstreifen Kraftfahrzeugverkehr. */
104 kfz: 3.25 as Meters,
105 /** Radverkehrsanlage. */
106 rad: 1.6 as Meters,
107 /** Fussgaengerfurt. */
108 fuss: 4.0 as Meters,
109 /** Strassenbahn. */
110 oepnv: 3.0 as Meters,
111 } as const;
112
113 /**
114 * Vorbelegte Breite eines gezeichneten Arbeitsbereichs.
115 *
116 * KEIN REGELWERKSWERT und deshalb nicht in `constants.ts`: Die RSA 21 gibt
117 * keine Breite einer Arbeitsstelle vor - sie haengt an der Baustelle. 4,0 m
118 * sind der Anhalt einer gesperrten Fahrspur samt Absperrung; der Bearbeiter
119 * traegt die wirkliche Breite ein.
120 */
121 export const ARBEITSBEREICH_REGELBREITE: Meters = 4.0;
122
123 /** Ein Schnitt zweier Fahrlinien mit den Abstaenden entlang beider Linien. */
124 export interface Schnitt {
125 readonly punkt: Punkt;
126 /** Abstand von der Haltlinie entlang Linie A. */
127 readonly abstandA: Meters;
128 /** Abstand von der Haltlinie entlang Linie B. */
129 readonly abstandB: Meters;
130 /** Schnittwinkel im Bogenmass, immer zwischen 0 und pi/2. */
131 readonly winkel: number;
132 }
133
134 /** Ausdehnung des Konfliktbereichs entlang beider Fahrlinien. */
135 export interface Konfliktbereich {
136 /** Beginn entlang A, von der Haltlinie A aus. */
137 readonly vonA: Meters;
138 /** Ende entlang A. */
139 readonly bisA: Meters;
140 readonly vonB: Meters;
141 readonly bisB: Meters;
142 /** Wie viele Schnittpunkte lagen zugrunde? */
143 readonly schnitte: number;
144 /**
145 * Winkel des Schnitts, der `bisA` bestimmt hat - der fuer den Raeumweg
146 * massgebende. Zuvor nahm die Vermassung den ERSTEN Schnitt; kreuzte ein
147 * Abbieger weiter hinten einen zweiten Strom, gehoerte der ausgewiesene
148 * Winkel zur falschen Stelle, und Rechnung und Anzeige fielen auseinander.
149 *
150 * Bestimmt statt eines Schnitts ein gemeinsames Stueck das Ende entlang A,
151 * steht hier 0 - dieselbe Bedeutung "laengs" wie bei einem Bereich ganz ohne
152 * Punktschnitt. Es gibt also Bereiche mit `schnitte` groesser null und
153 * `winkel` gleich null; einen Schnitt, der `bisA` bestimmt haette, gibt es
154 * dort nicht.
155 */
156 readonly winkel: number;
157 /**
158 * Ist eine Ueberlagerung in den Bereich eingegangen? Auch dann, wenn es
159 * daneben Punktschnitte gab - Kreuzen und Laengslauf schliessen einander
160 * nicht aus, und beides gehoert in dieselbe Huellkurve.
161 */
162 readonly ausUeberlappung: boolean;
163 }
164
165 /** Ergebnis der Vermessung einer gerichteten Beziehung. */
166 export interface VermesseneWege {
167 /**
168 * Raeumweg: Haltlinie des raeumenden Stroms bis zum Ende des Konfliktbereichs.
169 * Bei einer Querung in der unguenstigeren Richtung (siehe `vermesseWege`).
170 */
171 readonly raeumweg: Meters;
172 /**
173 * Einfahrweg: Haltlinie des einfahrenden Stroms bis zum Beginn des
174 * Konfliktbereichs. Bei einer Querung in der unguenstigeren Richtung, beim
175 * Fussgaenger 0 m (siehe `vermesseWege`).
176 */
177 readonly einfahrweg: Meters;
178 /** Schnittwinkel in Grad - fuer die Nachvollziehbarkeit im Bericht. */
179 readonly winkelGrad: number;
180 readonly konfliktbereich: Konfliktbereich;
181 }
182
183 // --- Grundrechnungen --------------------------------------------------------
184
185 export function abstand(a: Punkt, b: Punkt): Meters {
186 return Math.hypot(b.x - a.x, b.y - a.y);
187 }
188
189 /** Gesamtlaenge eines Linienzugs. */
190 export function laenge(linie: Fahrlinie): Meters {
191 let summe = 0;
192 for (let i = 1; i < linie.punkte.length; i += 1) {
193 summe += abstand(linie.punkte[i - 1]!, linie.punkte[i]!);
194 }
195 return summe;
196 }
197
198 /** Untere Schranke des Schnittwinkels; darunter ist "kreuzen" nicht mehr sinnvoll. */
199 const MIN_WINKEL = (10 * Math.PI) / 180;
200 const SIN_MIN = Math.sin(MIN_WINKEL);
201 /** Ab dieser Laenge gilt ein gemeinsames Stueck als Ueberlagerung, nicht als Rauschen. */
202 const MIN_UEBERLAPPUNG: Meters = 1.0;
203 /** Bis zu diesem Winkel gelten zwei Segmente als gleichlaufend. */
204 const COS_PARALLEL = Math.cos((5 * Math.PI) / 180);
205
206 /** Was bei der Schnittsuche gefunden und was verworfen wurde. */
207 export interface Schnittbefund {
208 readonly schnitte: readonly Schnitt[];
209 /** Wie viele Treffer als Auffaecherung verworfen wurden. */
210 readonly unterdrueckt: number;
211 /** Wie viele Treffer doppelt gezaehlt worden waeren. */
212 readonly doppelt: number;
213 }
214
215 /**
216 * Schnittpunkte zweier Fahrlinien - mit Auskunft darueber, was verworfen wurde.
217 *
218 * Geprueft wird segmentweise. Beruehrungen in einem Endpunkt zaehlen weiterhin
219 * mit: Zwei Stroeme, die in dieselbe Ausfahrt einlaufen, treffen sich in ihrem
220 * letzten Punkt und stehen dennoch in Konflikt. Eine Regel "Beruehrung im
221 * Endpunkt zaehlt nie" waere die unsichere Seite - sie verwuerfe unter anderem
222 * eine Fussgaengerfurt, deren erster Punkt auf der Haltlinie eines Kfz-Stroms
223 * liegt.
224 *
225 * Was NICHT mehr zaehlt, ist das Auffaechern zweier Kraftfahrzeugstroeme aus
226 * derselben Haltlinie - und zwar aufgrund ihrer Zugehoerigkeit, nicht aufgrund
227 * einer Toleranz. Jede so verworfene Beruehrung wird gezaehlt und der
228 * Oberflaeche ausgewiesen; das Unterdruecken eines Konflikts ist
229 * sicherheitsrelevant und darf nie stillschweigend geschehen.
230 */
231 export function schneideMitBefund(a: Fahrlinie, b: Fahrlinie): Schnittbefund {
232 const ergebnis: Schnitt[] = [];
233 let unterdrueckt = 0;
234 let doppelt = 0;
235 let laengeA = 0;
236
237 for (let i = 1; i < a.punkte.length; i += 1) {
238 const a1 = a.punkte[i - 1]!;
239 const a2 = a.punkte[i]!;
240 const segmentA = abstand(a1, a2);
241 let laengeB = 0;
242
243 for (let j = 1; j < b.punkte.length; j += 1) {
244 const b1 = b.punkte[j - 1]!;
245 const b2 = b.punkte[j]!;
246 const segmentB = abstand(b1, b2);
247
248 const treffer = segmentSchnitt(a1, a2, b1, b2);
249 if (treffer !== null) {
250 const schnitt: Schnitt = {
251 punkt: treffer.punkt,
252 abstandA: laengeA + treffer.tA * segmentA,
253 abstandB: laengeB + treffer.tB * segmentB,
254 winkel: treffer.winkel,
255 };
256
257 // Verglichen wird das Abstandspaar, nicht der geometrische Punkt: Ein
258 // Strom, der dieselbe Stelle zweimal durchfaehrt - Wendefahrbeziehung,
259 // zweifach gequerte Furt -, liefert dort zwei verschiedene Treffer.
260 // Ueber den Punkt entdoppelt, braeche die Huellkurve zusammen, fuer die
261 // der Konfliktbereich gerade da ist.
262 if (schonErfasst(ergebnis, schnitt)) {
263 doppelt += 1;
264 } else if (istAuffaecherung(a, b, schnitt)) {
265 unterdrueckt += 1;
266 } else {
267 ergebnis.push(schnitt);
268 }
269 }
270 laengeB += segmentB;
271 }
272 laengeA += segmentA;
273 }
274
275 return { schnitte: ergebnis, unterdrueckt, doppelt };
276 }
277
278 /** Schnittpunkte zweier Fahrlinien. */
279 export function schneide(a: Fahrlinie, b: Fahrlinie): Schnitt[] {
280 return [...schneideMitBefund(a, b).schnitte];
281 }
282
283 /**
284 * Ein Stuetzpunkt gehoert zu zwei Segmenten. Faellt ein Schnitt genau darauf,
285 * wird er von beiden gefunden und zaehlte sonst doppelt.
286 */
287 function schonErfasst(bisher: readonly Schnitt[], neu: Schnitt): boolean {
288 return bisher.some(
289 (s) => Math.abs(s.abstandA - neu.abstandA) < 1e-6 && Math.abs(s.abstandB - neu.abstandB) < 1e-6,
290 );
291 }
292
293 /**
294 * Ist der Treffer nur das Auffaechern zweier Stroeme aus demselben Querschnitt?
295 *
296 * Zwei Kraftfahrzeugstroeme, die an derselben Haltlinie nebeneinander stehen -
297 * einer faehrt geradeaus, einer biegt links ab -, fahren gleichzeitig ab und
298 * laufen auseinander. Ihr gemeinsamer Anfang ist keine Konfliktflaeche, sondern
299 * der gemeinsame Ausgangspunkt. Eine Zwischenzeit waere dort auch
300 * schalttechnisch nicht darstellbar: Sie setzt voraus, dass der einfahrende
301 * Strom wartet, waehrend der andere raeumt - bei gemeinsamer Haltlinie und
302 * gleichzeitiger Freigabe gibt es diesen Zustand nicht.
303 *
304 * Die Ausnahme ist eng gefasst. Jede einzelne Bedingung faengt einen Fall ab,
305 * der ein echter Konflikt waere:
306 *
307 * 1. NUR AN EIN UND DERSELBEN HALTLINIE, als Objekt verglichen. Versetzte
308 * Haltlinien, aufgeweitete Radaufstellstreifen und vorgelagerte
309 * Teilknoten sind eigene Objekte; dort greift die Ausnahme nicht. Ohne
310 * Zuordnung greift sie ebenfalls nicht.
311 * 2. NUR KRAFTFAHRZEUG GEGEN KRAFTFAHRZEUG. Der Rechtsabbieger gegen den
312 * parallel gefuehrten Radstrom derselben Zufahrt ist einer der
313 * sicherheitskritischsten Konflikte an einer Lichtsignalanlage.
314 * 3. NUR AN EINER HALTLINIE DER ZUFAHRT. Eine Furt gehoert zum Knotenpunktarm,
315 * nicht zu dessen Zufahrt; sie ist gegenueber den einfahrenden Stroemen
316 * ihres eigenen Arms planmaessig feindlich.
317 * 4. NUR SOLANGE DIE STROEME BIS DAHIN GEMEINSAM LAUFEN. Das ist die
318 * entscheidende Bedingung - siehe unten.
319 *
320 * ZUR VIERTEN BEDINGUNG
321 *
322 * Eine einstreifige Zufahrt, aus der geradeaus, links und rechts gefahren
323 * werden darf, ergibt drei Stroeme auf demselben Fahrstreifen. Sie laufen von
324 * der Haltlinie bis in den Knotenpunkt gemeinsam und trennen sich erst dort.
325 * Ihr Trennpunkt ist keine Konfliktflaeche, sondern das Ende des gemeinsamen
326 * Fahrstreifens - und er liegt nicht bei null, sondern etliche Meter hinter der
327 * Haltlinie.
328 *
329 * Massgebend ist deshalb nicht der Abstand zur Haltlinie, sondern die Frage:
330 * Sind die beiden Stroeme von der Haltlinie bis zu dieser Beruehrung
331 * durchgehend im selben Fahrstreifen gefahren? Wenn ja, ist die Beruehrung ihr
332 * Auseinanderlaufen. Wenn nein, kreuzt einer die Bahn des anderen, und das ist
333 * ein Konflikt - der Linksabbieger vom rechten Fahrstreifen ist genau dieser
334 * Fall.
335 *
336 * Zwei frueher erwogene Fassungen sind verworfen worden, beide aufgrund eigener
337 * Testfaelle: eine Seitenpruefung, die bei einem Millimeter Versatz im
338 * Startpunkt kippte, und eine feste Laengenschranke, die den gemeinsamen
339 * Fahrstreifen nicht erfasste.
340 */
341 function istAuffaecherung(a: Fahrlinie, b: Fahrlinie, schnitt: Schnitt): boolean {
342 const bezugA = a.bezug;
343 const bezugB = b.bezug;
344 if (bezugA === undefined || bezugB === undefined) return false;
345 if (bezugA.haltlinieId === null || bezugA.haltlinieId !== bezugB.haltlinieId) return false;
346 if (bezugA.mode !== 'kfz' || bezugB.mode !== 'kfz') return false;
347 if (bezugA.haltlinienArt !== 'zufahrt') return false;
348
349 return laufenGemeinsam(a, b, Math.max(schnitt.abstandA, schnitt.abstandB));
350 }
351
352 /**
353 * Sind zwei Stroeme von der Haltlinie bis zum Abstand `bis` durchgehend im
354 * selben Fahrstreifen gefahren?
355 *
356 * Verglichen werden die Orte bei gleicher Bogenlaenge: Zwei Fahrzeuge im selben
357 * Fahrstreifen stehen hintereinander in derselben Schlange und legen dieselbe
358 * Strecke zurueck.
359 *
360 * Als "im selben Fahrstreifen" gilt ein Abstand unter einer halben
361 * Korridorbreite. Der Wert ist nicht frei gewaehlt, sondern trennt genau die
362 * beiden Faelle, um die es geht: Stroeme desselben Fahrstreifens liegen
363 * praktisch aufeinander, Stroeme benachbarter Fahrstreifen eine volle
364 * Fahrstreifenbreite auseinander - also mehr als doppelt so weit.
365 *
366 * GENAUIGKEIT: Zwischen zwei Stuetzpunkten verlaufen beide Linien geradlinig,
367 * ihr Abstandsquadrat ist dort also eine nach oben geoeffnete Parabel. Sein
368 * Groesstwert liegt damit immer an einem Stuetzpunkt. Es genuegt, die
369 * Stuetzpunkte beider Linien zu pruefen - abgetastet wird nichts, das Ergebnis
370 * haengt an keiner Schrittweite.
371 */
372 function laufenGemeinsam(a: Fahrlinie, b: Fahrlinie, bis: Meters): boolean {
373 const grenze = Math.min(a.breite, b.breite) / 2;
374 const stellen = new Set<number>([0]);
375
376 for (const linie of [a, b]) {
377 let s = 0;
378 for (let i = 1; i < linie.punkte.length; i += 1) {
379 s += abstand(linie.punkte[i - 1]!, linie.punkte[i]!);
380 if (s > 0 && s <= bis) stellen.add(s);
381 }
382 }
383 if (bis > 0) stellen.add(bis);
384
385 for (const s of stellen) {
386 const pa = punktBeiAbstand(a, s);
387 const pb = punktBeiAbstand(b, s);
388 if (pa === null || pb === null) return false;
389 if (Math.hypot(pa.x - pb.x, pa.y - pb.y) > grenze) return false;
390 }
391 return true;
392 }
393
394 /** Punkt auf der Linie im gegebenen Abstand von der Haltlinie. */
395 export function punktBeiAbstand(linie: Fahrlinie, s: Meters): Punkt | null {
396 if (linie.punkte.length < 2 || !isFiniteNumber(s) || s < 0) return null;
397 let rest = s;
398 for (let i = 1; i < linie.punkte.length; i += 1) {
399 const p = linie.punkte[i - 1]!;
400 const q = linie.punkte[i]!;
401 const l = abstand(p, q);
402 if (l <= 0) continue;
403 if (rest <= l) {
404 const t = rest / l;
405 return { x: p.x + (q.x - p.x) * t, y: p.y + (q.y - p.y) * t };
406 }
407 rest -= l;
408 }
409 return linie.punkte[linie.punkte.length - 1] ?? null;
410 }
411
412 interface SegmentTreffer {
413 readonly punkt: Punkt;
414 /** Lage auf Segment A, 0..1. */
415 readonly tA: number;
416 readonly tB: number;
417 readonly winkel: number;
418 }
419
420 function segmentSchnitt(a1: Punkt, a2: Punkt, b1: Punkt, b2: Punkt): SegmentTreffer | null {
421 const rx = a2.x - a1.x;
422 const ry = a2.y - a1.y;
423 const sx = b2.x - b1.x;
424 const sy = b2.y - b1.y;
425
426 const nenner = rx * sy - ry * sx;
427 // Parallel oder entartet: kein Kreuzungspunkt.
428 if (Math.abs(nenner) < 1e-12) return null;
429
430 const tA = ((b1.x - a1.x) * sy - (b1.y - a1.y) * sx) / nenner;
431 const tB = ((b1.x - a1.x) * ry - (b1.y - a1.y) * rx) / nenner;
432
433 const toleranz = 1e-9;
434 if (tA < -toleranz || tA > 1 + toleranz || tB < -toleranz || tB > 1 + toleranz) return null;
435
436 const laengeR = Math.hypot(rx, ry);
437 const laengeS = Math.hypot(sx, sy);
438 if (laengeR < 1e-12 || laengeS < 1e-12) return null;
439
440 // Schnittwinkel, auf 0..pi/2 gefaltet: fuer die Ausdehnung des
441 // Konfliktbereichs ist nur der spitze Winkel massgebend.
442 const cos = Math.abs((rx * sx + ry * sy) / (laengeR * laengeS));
443 const winkel = Math.acos(Math.min(1, cos));
444
445 return {
446 punkt: { x: a1.x + tA * rx, y: a1.y + tA * ry },
447 tA,
448 tB,
449 winkel: winkel === 0 ? 0 : Math.min(winkel, Math.PI - winkel),
450 };
451 }
452
453 /**
454 * Ausdehnung des Konfliktbereichs entlang beider Fahrlinien.
455 *
456 * Bei mehreren Schnittpunkten - etwa wenn ein Abbieger eine Furt zweimal quert -
457 * wird die Huellkurve gebildet: vom ersten Eintritt bis zum letzten Austritt.
458 * Das ist die sichere Auslegung, weil der Strom den Bereich erst danach
459 * endgueltig verlassen hat.
460 *
461 * In die Huellkurve geht auch eine Ueberlagerung ein, und zwar unabhaengig
462 * davon, ob es daneben Punktschnitte gibt. Kreuzen und Laengslauf schliessen
463 * einander nicht aus: Ein Abbieger, der eine Furt quert und danach neben ihr
464 * herlaeuft, verlaesst deren Korridor erst am Ende des gemeinsamen Stuecks.
465 */
466 export function konfliktbereich(a: Fahrlinie, b: Fahrlinie): Konfliktbereich | null {
467 const schnitte = schneide(a, b);
468 // Kein Punktschnitt heisst nicht "vertraeglich": Zwei Stroeme koennen auch
469 // nebeneinander herlaufen und sich denselben Fahrstreifen teilen. Solche
470 // Ueberlagerungen fand die Vermassung frueher gar nicht - der Fall, in dem
471 // sie in die UNSICHERE Richtung falsch lag.
472 const ueber = ueberlappung(a, b);
473
474 if (schnitte.length === 0) {
475 if (ueber === null) return null;
476 return {
477 vonA: roundTo(Math.max(0, ueber.vonA), 3),
478 bisA: roundTo(ueber.bisA, 3),
479 vonB: roundTo(Math.max(0, ueber.vonB), 3),
480 bisB: roundTo(ueber.bisB, 3),
481 schnitte: 0,
482 // Gleichlaufende Stroeme haben keinen Schnittwinkel. Null bedeutet hier
483 // "laengs", nicht "unbekannt".
484 winkel: 0,
485 ausUeberlappung: true,
486 };
487 }
488
489 let vonA = Number.POSITIVE_INFINITY;
490 let bisA = Number.NEGATIVE_INFINITY;
491 let vonB = Number.POSITIVE_INFINITY;
492 let bisB = Number.NEGATIVE_INFINITY;
493 // Der Winkel des Schnitts, der das Ende entlang A bestimmt: Er gehoert zum
494 // Raeumweg und ist daher der auszuweisende.
495 let winkel = schnitte[0]!.winkel;
496
497 for (const schnitt of schnitte) {
498 // Bei nahezu parallelem Schnitt waechst die Ausdehnung ins Unendliche.
499 // Zehn Grad sind die untere Schranke; darunter ist die Annahme
500 // "kreuzende Stroeme" ohnehin nicht mehr sinnvoll.
501 const wirksam = Math.max(Math.sin(schnitt.winkel), SIN_MIN);
502
503 const halbeA = a.breite / 2 / wirksam;
504 const halbeB = b.breite / 2 / wirksam;
505
506 // A durchfaehrt den Korridor von B - also zaehlt die Breite von B.
507 vonA = Math.min(vonA, schnitt.abstandA - halbeB);
508 if (schnitt.abstandA + halbeB > bisA) {
509 bisA = schnitt.abstandA + halbeB;
510 winkel = schnitt.winkel;
511 }
512 vonB = Math.min(vonB, schnitt.abstandB - halbeA);
513 bisB = Math.max(bisB, schnitt.abstandB + halbeA);
514 }
515
516 // Laeuft neben den Punktschnitten auch ein gemeinsames Stueck, gehoert es zum
517 // selben Konfliktbereich. Sonst endete der Raeumweg kurz hinter dem
518 // Kreuzungspunkt, obwohl der raeumende Strom den fremden Korridor erst am
519 // Ende der Ueberlagerung verlaesst - eine zu kurze Zwischenzeit.
520 if (ueber !== null) {
521 vonA = Math.min(vonA, ueber.vonA);
522 if (ueber.bisA > bisA) {
523 bisA = ueber.bisA;
524 // Bestimmt das gemeinsame Stueck das Ende entlang A, gehoert zum
525 // Raeumweg kein Schnittwinkel. Null bedeutet hier "laengs".
526 winkel = 0;
527 }
528 vonB = Math.min(vonB, ueber.vonB);
529 bisB = Math.max(bisB, ueber.bisB);
530 }
531
532 return {
533 vonA: roundTo(Math.max(0, vonA), 3),
534 bisA: roundTo(bisA, 3),
535 vonB: roundTo(Math.max(0, vonB), 3),
536 bisB: roundTo(bisB, 3),
537 schnitte: schnitte.length,
538 winkel,
539 ausUeberlappung: ueber !== null,
540 };
541 }
542
543 /** Abschnitt, auf dem zwei Fahrlinien nebeneinander oder deckungsgleich laufen. */
544 export interface Ueberlappung {
545 readonly vonA: Meters;
546 readonly bisA: Meters;
547 readonly vonB: Meters;
548 readonly bisB: Meters;
549 /** Laenge des gemeinsamen Stuecks entlang A. */
550 readonly laenge: Meters;
551 }
552
553 /** Ein Stueck, auf dem ein einzelnes Segmentpaar seine Korridore teilt. */
554 interface Teilstueck {
555 vonA: number;
556 bisA: number;
557 vonB: number;
558 bisB: number;
559 }
560
561 /**
562 * Laufen zwei Fahrlinien auf einem Stueck ineinander, statt sich zu kreuzen?
563 *
564 * `segmentSchnitt` verwirft gleichlaufende Segmente - der Nenner wird null -
565 * und liefert dort nichts. Ohne diese Pruefung gelten zwei Stroeme, die sich
566 * einen Fahrstreifen teilen oder deren Furt entlang einer Fahrlinie liegt, als
567 * vertraeglich. Das ist die unsichere Richtung.
568 *
569 * Massgebend ist nicht die Deckungsgleichheit der Mittellinien, sondern der
570 * Abstand gemessen an den Korridorbreiten: Zwei Fahrzeuge kommen sich ins
571 * Gehege, sobald ihre Korridore ueberlappen. Gerechnet wird durch Projektion,
572 * ohne Abtastung - das Ergebnis haengt an keiner Schrittweite.
573 *
574 * Es haengt auch an keiner Stuetzpunktdichte: Die Teilstuecke aller
575 * Segmentpaare werden gesammelt und zu zusammenhaengenden gemeinsamen Stuecken
576 * vereinigt; erst darauf greift die Rauschschwelle.
577 */
578 export function ueberlappung(a: Fahrlinie, b: Fahrlinie): Ueberlappung | null {
579 if (!istGueltig(a) || !istGueltig(b)) return null;
580 const grenzAbstand = (a.breite + b.breite) / 2;
581
582 const teilstuecke: Teilstueck[] = [];
583
584 let laengeA = 0;
585 for (let i = 1; i < a.punkte.length; i += 1) {
586 const a1 = a.punkte[i - 1]!;
587 const a2 = a.punkte[i]!;
588 const la = abstand(a1, a2);
589 if (la < 1e-9) continue;
590 const ux = (a2.x - a1.x) / la;
591 const uy = (a2.y - a1.y) / la;
592
593 let laengeB = 0;
594 for (let j = 1; j < b.punkte.length; j += 1) {
595 const b1 = b.punkte[j - 1]!;
596 const b2 = b.punkte[j]!;
597 const lb = abstand(b1, b2);
598 if (lb < 1e-9) continue;
599
600 const vx = (b2.x - b1.x) / lb;
601 const vy = (b2.y - b1.y) / lb;
602
603 // Nur gleichlaufende Segmente - auch gegenlaeufige, denn eine Furt entlang
604 // einer Fahrlinie kann in beide Richtungen gezeichnet sein.
605 if (Math.abs(ux * vx + uy * vy) < COS_PARALLEL) {
606 laengeB += lb;
607 continue;
608 }
609
610 // Laengs- und Querlage der beiden Endpunkte von B im System von A.
611 const l1 = (b1.x - a1.x) * ux + (b1.y - a1.y) * uy;
612 const q1 = -(b1.x - a1.x) * uy + (b1.y - a1.y) * ux;
613 const l2 = (b2.x - a1.x) * ux + (b2.y - a1.y) * uy;
614 const q2 = -(b2.x - a1.x) * uy + (b2.y - a1.y) * ux;
615
616 // Teilstueck von B, auf dem der Querabstand die Grenze einhaelt. Weil das
617 // Segment gerade ist, ist der Querabstand linear - der Bereich laesst sich
618 // ausrechnen statt abtasten.
619 const nah = naheSpanne(q1, q2, grenzAbstand);
620 if (nah === null) {
621 laengeB += lb;
622 continue;
623 }
624
625 // Dieses Teilstueck auf die Laengsachse von A abbilden und dort auf das
626 // Segment von A beschneiden.
627 const lNah1 = l1 + nah.von * (l2 - l1);
628 const lNah2 = l1 + nah.bis * (l2 - l1);
629 const von = Math.max(0, Math.min(lNah1, lNah2));
630 const bis = Math.min(la, Math.max(lNah1, lNah2));
631 // Nichts uebrig, nachdem auf das Segment von A beschnitten wurde. Die
632 // Rauschschwelle greift hier nicht: Sie gilt dem gemeinsamen Stueck, das
633 // sich erst aus allen Teilstuecken zusammensetzt.
634 if (bis <= von) {
635 laengeB += lb;
636 continue;
637 }
638
639 // Der beschnittene Bereich, zurueckgerechnet auf die Bogenlaenge von B.
640 const spanneL = lNah2 - lNah1;
641 const tVon =
642 Math.abs(spanneL) < 1e-9
643 ? nah.von
644 : nah.von + ((von - lNah1) / spanneL) * (nah.bis - nah.von);
645 const tBis =
646 Math.abs(spanneL) < 1e-9
647 ? nah.bis
648 : nah.von + ((bis - lNah1) / spanneL) * (nah.bis - nah.von);
649
650 teilstuecke.push({
651 vonA: laengeA + von,
652 bisA: laengeA + bis,
653 vonB: laengeB + Math.min(tVon, tBis) * lb,
654 bisB: laengeB + Math.max(tVon, tBis) * lb,
655 });
656
657 laengeB += lb;
658 }
659 laengeA += la;
660 }
661
662 const gemeinsam = vereinigeTeilstuecke(teilstuecke);
663 if (gemeinsam.length === 0) return null;
664
665 let vonA = Number.POSITIVE_INFINITY;
666 let bisA = Number.NEGATIVE_INFINITY;
667 let vonB = Number.POSITIVE_INFINITY;
668 let bisB = Number.NEGATIVE_INFINITY;
669 for (const stueck of gemeinsam) {
670 vonA = Math.min(vonA, stueck.vonA);
671 bisA = Math.max(bisA, stueck.bisA);
672 vonB = Math.min(vonB, stueck.vonB);
673 bisB = Math.max(bisB, stueck.bisB);
674 }
675
676 return {
677 vonA: roundTo(Math.max(0, vonA), 3),
678 bisA: roundTo(bisA, 3),
679 vonB: roundTo(Math.max(0, vonB), 3),
680 bisB: roundTo(bisB, 3),
681 laenge: roundTo(bisA - vonA, 3),
682 };
683 }
684
685 /**
686 * Fasst die Teilstuecke der einzelnen Segmentpaare zu zusammenhaengenden
687 * gemeinsamen Stuecken zusammen und verwirft, was unter der Rauschschwelle
688 * bleibt.
689 *
690 * Die Schranke gehoert an das gemeinsame Stueck, nicht an das einzelne
691 * Segmentpaar. Wer eine Fahrlinie mit dichten Stuetzpunkten nachfaehrt - in der
692 * Arbeitsstufe "normal" sind das rund 0,6 m je Anschlag -, zerlegt dieselbe
693 * Ueberlagerung in lauter kurze Teilstuecke. Geprueft am einzelnen Paar fiele
694 * jedes davon durch, und zwei Stroeme mit ueber zig Meter deckungsgleichen
695 * Korridoren galten als vertraeglich: kein Konflikt, keine Zwischenzeit.
696 */
697 function vereinigeTeilstuecke(teile: readonly Teilstueck[]): Teilstueck[] {
698 const zusammen: Teilstueck[] = [];
699
700 for (const teil of [...teile].sort((x, y) => x.vonA - y.vonA)) {
701 const letztes = zusammen[zusammen.length - 1];
702 // Aufeinanderfolgende Segmente stossen im Stuetzpunkt aneinander; die
703 // Toleranz faengt allein den Rundungsfehler dieser Naht ab.
704 if (letztes !== undefined && teil.vonA <= letztes.bisA + 1e-6) {
705 letztes.bisA = Math.max(letztes.bisA, teil.bisA);
706 letztes.vonB = Math.min(letztes.vonB, teil.vonB);
707 letztes.bisB = Math.max(letztes.bisB, teil.bisB);
708 } else {
709 zusammen.push({ ...teil });
710 }
711 }
712
713 return zusammen.filter((stueck) => stueck.bisA - stueck.vonA >= MIN_UEBERLAPPUNG);
714 }
715
716 /**
717 * Der Teil eines Segments, auf dem der Querabstand die Grenze einhaelt.
718 * `q1` und `q2` sind die vorzeichenbehafteten Querabstaende der Endpunkte; der
719 * Abstand verlaeuft dazwischen linear.
720 */
721 function naheSpanne(q1: number, q2: number, grenze: Meters): { von: number; bis: number } | null {
722 const nah1 = Math.abs(q1) <= grenze;
723 const nah2 = Math.abs(q2) <= grenze;
724 if (nah1 && nah2) return { von: 0, bis: 1 };
725
726 const differenz = q2 - q1;
727 if (Math.abs(differenz) < 1e-12) return null;
728
729 // Schnittstellen mit den beiden Grenzgeraden +grenze und -grenze.
730 const stellen = [(grenze - q1) / differenz, (-grenze - q1) / differenz]
731 .filter((t) => t >= 0 && t <= 1)
732 .sort((x, y) => x - y);
733
734 if (nah1) return { von: 0, bis: stellen[0] ?? 0 };
735 if (nah2) return { von: stellen[stellen.length - 1] ?? 1, bis: 1 };
736 // Beide Endpunkte ausserhalb: Das Segment kann die Roehre nur durchqueren,
737 // wenn es beide Grenzgeraden schneidet.
738 if (stellen.length < 2) return null;
739 return { von: stellen[0]!, bis: stellen[stellen.length - 1]! };
740 }
741
742 /**
743 * Ist diese Linie eine Querung, deren Geh- oder Fahrrichtung die Zeichnung
744 * nicht festlegt - und wenn ja, welche?
745 *
746 * WARUM ES DIESE UNTERSCHEIDUNG GIBT (Fassung 5.43.0): Eine Furt oder
747 * Radquerung wird als EINE Linie gezeichnet, begangen wird sie aber von beiden
748 * Bordkanten aus. Gemessen wurde bis dahin allein ab ihrem ersten Punkt, und
749 * damit hingen Raeum- und Einfahrweg an der Bordkante, an der der Bearbeiter zu
750 * zeichnen begann: Je Furt fielen zwei von vier Beziehungen zu kurz aus, bis
751 * -9 s. Eine Gegenlinie in derselben Signalgruppe lehnt `verbindeMitGruppe` ab
752 * (gleich schaltende Geber stehen an einem Querschnitt), und eine eigene Gruppe
753 * fuer die Gegenrichtung erzeugte einen Konflikt Furt gegen Furt. Die
754 * Gegenrichtung muss deshalb HIER mitgemessen werden.
755 *
756 * 'fuss' jede Fussgaengerlinie - Fussgaenger queren immer.
757 * 'rad' jede Radlinie, AUSSER sie beginnt an der Haltlinie einer ZUFAHRT
758 * und faehrt dort nachweislich geradeaus, links oder rechts. Ein
759 * solcher Radstrom faehrt in einer Richtung (RiLSA 2015, Fall 5 mit
760 * Bild 14: Einrichtungsradweg) und bleibt gerichtet. Querung ist
761 * also, was an einer Aufstelllinie der Art "querung" beginnt, was
762 * keiner Haltlinie zugeordnet ist - und was die Fahrbeziehung
763 * "querung" traegt, gleich an welcher Haltlinie. Unbekannt (keine
764 * Art, keine Fahrbeziehung) heisst sicher.
765 * null alles andere, auch Kraftfahrzeuge und OePNV - und eine Linie ohne
766 * `bezug`: Ohne ihn ist nicht einmal die Verkehrsart bekannt, und
767 * `fahrlinienVon` gibt ihn jeder Linie des Lageplans mit.
768 *
769 * WARUM DIE FAHRBEZIEHUNG MITENTSCHEIDET (Fassung 5.43.0): Bis
770 * dahin sah diese Funktion allein die Haltlinienart. Eine Radlinie mit der
771 * Fahrbeziehung "querung", deren erster Punkt auf einer Haltlinie der Art
772 * "zufahrt" liegt, blieb gerichtet - und genau so entsteht sie leicht: Der
773 * Haltliniendialog stellt "zufahrt" voreingestellt ein, und der erste Punkt
774 * einer Linie rastet auf die naechste Haltlinie jeder Art ein. Die
775 * Zeichenrichtung entschied dort weiter ueber bis zu 3 s.
776 */
777 export function querungsart(linie: Fahrlinie): 'fuss' | 'rad' | null {
778 const bezug = linie.bezug;
779 if (bezug === undefined) return null;
780 if (bezug.mode === 'fuss') return 'fuss';
781 if (bezug.mode !== 'rad') return null;
782 const gerichtet =
783 bezug.haltlinienArt === 'zufahrt' &&
784 bezug.movement !== undefined &&
785 bezug.movement !== 'querung';
786 return gerichtet ? null : 'rad';
787 }
788
789 /**
790 * Dieselbe Linie, von ihrem anderen Ende aus.
791 *
792 * Ohne `bezug`: Die Haltlinie gehoert zum ersten Punkt der gezeichneten Linie,
793 * nicht zu dem der umgekehrten. Wirkung hat das keine - `bezug` braucht nur
794 * die Auffaecherung, und die gilt allein Kraftfahrzeugen an einer Zufahrt, nie
795 * einer Querung.
796 */
797 function umgekehrt(linie: Fahrlinie): Fahrlinie {
798 return { id: linie.id, punkte: [...linie.punkte].reverse(), breite: linie.breite };
799 }
800
801 /**
802 * Misst Raeum- und Einfahrweg fuer die gerichtete Beziehung
803 * "`raeumend` raeumt, `einfahrend` faehrt ein".
804 *
805 * Gibt `null` zurueck, wenn sich die Fahrlinien nicht kreuzen - dann sind die
806 * Stroeme vertraeglich und es ist keine Zwischenzeit zu bilden.
807 *
808 * QUERUNGEN IN DER UNGUENSTIGEREN RICHTUNG (siehe `querungsart`). Fuer einen
809 * querenden Strom gilt nicht die Zeichenrichtung, sondern je Beziehung die
810 * Richtung mit der LAENGEREN Zwischenzeit: der groessere Raeumweg und der
811 * kleinere Einfahrweg beider Richtungen. Beide Richtungen werden als Linie
812 * gemessen, nicht als "Laenge minus Abstand" gerechnet - so gilt fuer Linien
813 * mit Stuetzpunkten, fuer mehrfach gekreuzte Querungen und fuer einen
814 * Korridor, der ueber ein Linienende hinausreicht, dieselbe Rechnung wie fuer
815 * die gezeichnete Richtung, und das Ergebnis ist fuer eine Linie und ihre
816 * Umkehrung dasselbe. DAS IST EINE AUSLEGUNG, kein Satz der RiLSA 2015: Sie
817 * verlangt die Rechnung fuer alle nichtvertraeglichen Kombinationen mit der
818 * groessten Zwischenzeit als massgebender (Abschnitt 2.5), misst den Weg des
819 * Fussgaengers vom Beginn der Furt (2.5.1), zeigt in Bild 15 (Fall 6) den
820 * Fussgaengerstrom mit Doppelpfeil und in Bild 5 und 6 ein Weg-Zeit-Bild je
821 * Gehrichtung. Welche Bordkante "Beginn" ist, sagt sie nicht.
822 *
823 * FUER DEN FUSSGAENGER ZUSAETZLICH, beides nie kuerzer als die RiLSA:
824 *
825 * Einfahrweg 0 m. Beginnt die Konfliktflaeche am Fahrbahnrand, ist te = 0 s;
826 * mit 1,5 m/s wird nur gerechnet, wenn der raeumende Verkehr den
827 * Randfahrstreifen nicht benutzen darf (Abschnitt 2.5.3). Ob er es darf,
828 * zeigt keine Zeichnung. Gemessen ergaebe sich selbst am Randfahrstreifen
829 * ein Rest - der Korridor von 3,25 m endet vor der Bordkante eines 3,5 m
830 * breiten Streifens 0,125 m, das sind 0,08 s, und die kosten an einer
831 * Rundungsgrenze eine ganze Sekunde. VOR EINEM STROM IM EIGENEN
832 * FAHRSTREIFEN EINER ZUFAHRT - etwa einem Linksabbieger, der den
833 * Randfahrstreifen nicht benutzen darf - ist der Einfahrweg 0 m eine
834 * Verschaerfung dieses Programms und keine Umsetzung der RiLSA; an der
835 * mitgelieferten Beispieldatei ergibt das viermal 5 s statt 3 s.
836 *
837 * Raeumweg mindestens die ganze gezeichnete Furt. Aus demselben Grund in der
838 * Gegenrichtung: Ob der einfahrende Verkehr den Fahrstreifen am fernen Rand
839 * benutzt, zeigt die Zeichnung ebenso wenig - auf einer Ausfahrt darf er
840 * jeden. Bild 15 fuehrt s0 ueber die ganze Fahrbahnbreite, und selbst am
841 * Randfahrstreifen kostete der gemessene Rest 0,10 s.
842 *
843 * Wer den Sonderfall der RiLSA belegen kann - gesperrter Randfahrstreifen -,
844 * traegt den Weg an der Beziehung von Hand ein; der Pruefbericht fragt dann
845 * nach der Begruendung (`zwischenzeiten.kuerzer-als-lageplan`).
846 *
847 * FUER DIE RADQUERUNG nur den Rest am Rand: Liegt zwischen Linienende und
848 * Konfliktbereich kein ganzer Korridor des anderen Stroms, beginnt bzw. endet
849 * der Konfliktbereich am Rand - kein zweiter Fahrstreifen passt dazwischen,
850 * und der Rest ist das Mass, um das der Korridor schmaler gezeichnet ist als
851 * der Fahrstreifen. Eine Einfahrzeit von 0 s nennt die RiLSA fuer den
852 * Radverkehr nicht; gemessen wird deshalb weiter, wo ein Fahrstreifen
853 * dazwischen liegt.
854 *
855 * `konfliktbereich` bleibt der Bereich entlang der GEZEICHNETEN Linien.
856 */
857 export function vermesseWege(raeumend: Fahrlinie, einfahrend: Fahrlinie): VermesseneWege | null {
858 if (!istGueltig(raeumend) || !istGueltig(einfahrend)) return null;
859
860 // Der Konfliktbereich fuehrt den massgebenden Winkel selbst mit; die
861 // Schnittsuche wurde hier zuvor ein zweites Mal gerechnet, und der genommene
862 // Winkel gehoerte zum ersten statt zum massgebenden Schnitt.
863 const bereich = konfliktbereich(raeumend, einfahrend);
864 if (bereich === null) return null;
865
866 // Der raeumende Strom muss bis zum ENDE des Bereichs.
867 let raeumweg = bereich.bisA;
868 let winkel = bereich.winkel;
869 const raeumendQuert = querungsart(raeumend);
870 if (raeumendQuert !== null) {
871 const gegen = konfliktbereich(umgekehrt(raeumend), einfahrend);
872 if (gegen !== null && gegen.bisA > raeumweg) {
873 raeumweg = gegen.bisA;
874 winkel = gegen.winkel;
875 }
876 const ganzeLinie = laenge(raeumend);
877 if (raeumendQuert === 'fuss' || ganzeLinie - raeumweg < einfahrend.breite) {
878 raeumweg = Math.max(raeumweg, ganzeLinie);
879 }
880 }
881
882 // Der einfahrende Strom erreicht den Bereich an seiner VORDEREN Kante.
883 let einfahrweg = bereich.vonB;
884 const einfahrendQuert = querungsart(einfahrend);
885 if (einfahrendQuert === 'fuss') {
886 einfahrweg = 0;
887 } else if (einfahrendQuert === 'rad') {
888 const gegen = konfliktbereich(raeumend, umgekehrt(einfahrend));
889 if (gegen !== null) einfahrweg = Math.min(einfahrweg, gegen.vonB);
890 if (einfahrweg < raeumend.breite) einfahrweg = 0;
891 }
892
893 return {
894 raeumweg: roundTo(raeumweg, 2),
895 einfahrweg: roundTo(einfahrweg, 2),
896 winkelGrad: roundTo((winkel * 180) / Math.PI, 1),
897 konfliktbereich: bereich,
898 };
899 }
900
901 /**
902 * Kreuzen sich zwei Fahrlinien in einem Punkt?
903 *
904 * Beantwortet ausschliesslich die Punktschnittfrage. Ob zwei Stroeme feindlich
905 * sind, entscheidet `beurteile` in ableitung.ts - dort kommt die Ueberlagerung
906 * hinzu, die hier nicht erfasst wird.
907 */
908 export function kreuzenSich(a: Fahrlinie, b: Fahrlinie): boolean {
909 return istGueltig(a) && istGueltig(b) && schneideMitBefund(a, b).schnitte.length > 0;
910 }
911
912 function istGueltig(linie: Fahrlinie): boolean {
913 if (linie.punkte.length < 2) return false;
914 if (!isFiniteNumber(linie.breite) || linie.breite <= 0) return false;
915 return linie.punkte.every((p) => isFiniteNumber(p.x) && isFiniteNumber(p.y));
916 }
917
918 // --- Massstab ---------------------------------------------------------------
919
920 /**
921 * Umrechnung Bildpunkte in Meter.
922 *
923 * Ein eingelesener Lageplan hat keinen bekannten Massstab. Der Anwender zieht
924 * deshalb eine Strecke bekannter Laenge - eine Fahrbahnbreite, ein Gebaeude,
925 * ein Massstabsbalken - und gibt die zugehoerige Laenge in Metern an.
926 */
927 export interface Massstab {
928 /** Meter je Bildpunkt. */
929 readonly meterJeBildpunkt: number;
930 }
931
932 export function massstabAusStrecke(
933 von: Punkt,
934 bis: Punkt,
935 laengeInMetern: Meters,
936 ): Massstab | null {
937 const bildpunkte = abstand(von, bis);
938 if (bildpunkte < 1e-6) return null;
939 if (!isFiniteNumber(laengeInMetern) || laengeInMetern <= 0) return null;
940 return { meterJeBildpunkt: laengeInMetern / bildpunkte };
941 }
942
943 /** Rechnet einen in Bildpunkten gezeichneten Punkt in Meter um. */
944 export function inMeter(punkt: Punkt, massstab: Massstab): Punkt {
945 return { x: punkt.x * massstab.meterJeBildpunkt, y: punkt.y * massstab.meterJeBildpunkt };
946 }
947
948 /** Rechnet eine ganze Fahrlinie in Meter um. */
949 export function linieInMeter(linie: Fahrlinie, massstab: Massstab): Fahrlinie {
950 return { ...linie, punkte: linie.punkte.map((p) => inMeter(p, massstab)) };
951 }
952
953 /**
954 * Ein Massstabsbalken: eine runde Meterzahl und ihre Laenge in Zeichenpunkten.
955 *
956 * WARUM DAS IN DER FACHSCHICHT STEHT: Der Balken wird jetzt an zwei Stellen
957 * gebraucht - auf dem Bildschirm in der Lageplanansicht und auf dem gedruckten
958 * Blatt. Zwei Fassungen derselben Stufenwahl liefen auseinander, und ein
959 * Balken, der auf Papier eine andere Zahl nennt als am Bildschirm, ist
960 * schlimmer als keiner.
961 *
962 * DIE STUFEN SIND RUND, WEIL EIN BALKEN ABGELESEN WIRD: "50 m" laesst sich
963 * halbieren und verdoppeln, "43,7 m" nicht.
964 */
965 export interface Massstabsbalken {
966 /** Beschriftung des Balkens in Metern. */
967 readonly meter: Meters;
968 /** Laenge des Balkens in Zeichenpunkten. */
969 readonly laenge: number;
970 }
971
972 /** Runde Stufen, unter denen der Balken seine Beschriftung waehlt. */
973 const BALKENSTUFEN: readonly number[] = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000];
974
975 export function massstabsbalken(meterJeZeichenpunkt: number, zielLaenge: number): Massstabsbalken {
976 if (!isFiniteNumber(meterJeZeichenpunkt) || meterJeZeichenpunkt <= 0) {
977 return { meter: 0, laenge: 0 };
978 }
979 const roh = zielLaenge * meterJeZeichenpunkt;
980 const meter =
981 BALKENSTUFEN.find((stufe) => stufe >= roh) ?? BALKENSTUFEN[BALKENSTUFEN.length - 1]!;
982 return { meter, laenge: meter / meterJeZeichenpunkt };
983 }