lsa-planer

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

/ src domain geometrie lageplan.ts

24,1 KB Rohdatei
src/domain/geometrie/lageplan.ts — 611 Zeilen
1 import type { Meters, Seconds } from '../units';
2 import { roundTo } from '../units';
3 import { RILSA_DEFAULTS, type RilsaDefaults } from '../rilsa/constants';
4 import type { Movement, TrafficMode } from '../rilsa/types';
5 import {
6 REGELBREITE,
7 abstand,
8 linieInMeter,
9 massstabAusStrecke,
10 type Fahrlinie,
11 type Massstab,
12 type Punkt,
13 } from './vermessung';
14
15 /**
16 * Lageplan als Grundlage der Vermessung.
17 *
18 * Gedanke dahinter: Raeum- und Einfahrweg sind heute die einzige Groesse, die
19 * der Anwender ausserhalb des Programms ermitteln und abtippen muss. Zeichnet
20 * er stattdessen die Fahrlinien in einen massstaeblichen Plan, lassen sich
21 * beide Wege - und darueber hinaus die Vertraeglichkeit - unmittelbar ableiten.
22 *
23 * KOORDINATEN
24 *
25 * Alle gezeichneten Punkte werden in BILDPUNKTEN des Hintergrundbilds
26 * gespeichert, nicht in Metern. Grund: Der Anwender zeichnet auf dem Bild, und
27 * die Zeichnung muss deckungsgleich bleiben, auch wenn der Massstab spaeter
28 * korrigiert wird. Die Umrechnung in Meter geschieht bei jeder Auswertung neu.
29 */
30
31 /** Hintergrundbild des Lageplans. */
32 export interface LageplanBild {
33 /** Bilddaten als Daten-URL - das Projekt bleibt dadurch in sich geschlossen. */
34 readonly datenUrl: string;
35 readonly breite: number;
36 readonly hoehe: number;
37 /** Woher das Bild stammt: Dateiname oder Bezeichnung des Kartendienstes. */
38 readonly herkunft: string;
39 readonly geladenAm: string;
40 }
41
42 /**
43 * Festlegung des Massstabs.
44 *
45 * Bei einem eingelesenen Bild zieht der Anwender eine Strecke bekannter Laenge.
46 * Bei einem georeferenzierten Luftbild ist der Massstab bekannt und wird
47 * unmittelbar uebernommen.
48 */
49 export interface Kalibrierung {
50 readonly von: Punkt;
51 readonly bis: Punkt;
52 readonly laengeMeter: Meters;
53 readonly herkunft: 'gemessen' | 'georeferenziert';
54 }
55
56 /** Art einer gezeichneten Halt- oder Aufstelllinie. */
57 export type HaltlinienArt = 'zufahrt' | 'querung';
58
59 /**
60 * Halt- oder Aufstelllinie: der Querschnitt, an dem Stroeme beginnen.
61 *
62 * WARUM EIN EIGENES OBJEKT
63 *
64 * Aus einer Zufahrt fahren mehrere Stroeme ab - geradeaus, links, rechts. Sie
65 * werden als je eine Fahrlinie gezeichnet und beginnen alle an derselben
66 * Haltlinie. Ob zwei Stroeme am selben Querschnitt beginnen, entscheidet
67 * darueber, ob sie feindlich sind: Stroeme derselben Zufahrt muessen
68 * gleichzeitig Freigabe erhalten koennen.
69 *
70 * Waere die Haltlinie je Strom neu gezeichnet, liesse sich diese Frage nur
71 * ueber Abstaende beantworten - und ein Millimeter entschiede dann ueber eine
72 * sicherheitsrelevante Angabe. Als eigenes Objekt wird daraus ein Vergleich
73 * von Kennungen.
74 *
75 * MEHRERE Haltlinien je Arm sind ausdruecklich zugelassen: versetzte
76 * Haltlinien, aufgeweiteter Radaufstellstreifen, vorgelagerter Teilknoten.
77 *
78 * `art` trennt zwei fachlich verschiedene Faelle:
79 * 'zufahrt' einfahrender Fahrverkehr; `armId` ist der Quellarm.
80 * 'querung' Furt oder Radquerung; `armId` ist der GEQUERTE Arm. Solche
81 * Stroeme gehoeren zum Arm, aber nicht zu dessen Zufahrt - eine
82 * Furt ist gegenueber den Stroemen ihres eigenen Arms feindlich.
83 */
84 export interface Haltlinie {
85 readonly id: string;
86 readonly name: string;
87 readonly art: HaltlinienArt;
88 /** Zugehoeriger Knotenpunktarm, sofern zugeordnet. */
89 readonly armId: string | null;
90 /** Endpunkte in Bildpunkten. */
91 readonly von: Punkt;
92 readonly bis: Punkt;
93 }
94
95 /** Eine gezeichnete Fahrlinie im Lageplan. Sie beschreibt genau EINEN Strom. */
96 export interface Planlinie {
97 readonly id: string;
98 readonly name: string;
99 readonly mode: TrafficMode;
100 readonly movement: Movement;
101 /** Breite des beanspruchten Korridors in Metern. */
102 readonly breiteMeter: Meters;
103 /**
104 * Punkte in Bildpunkten; der erste liegt auf der Haltlinie.
105 *
106 * Der Linienzug bleibt vollstaendig. Ist eine Haltlinie zugeordnet, wird der
107 * erste Punkt aus ihr und `startT` abgeleitet und von `normalisiere` gerade
108 * gehalten. Die Redundanz ist gewollt: Sie haelt die Linie zeichenbar, auch
109 * wenn der Verweis einmal nicht aufloesbar ist.
110 */
111 readonly punkte: readonly Punkt[];
112 /**
113 * Haltlinie, an der der Strom beginnt.
114 *
115 * `null` heisst "nicht zugeordnet". Dann entscheidet allein die Zeichnung -
116 * die sichere Seite, und der Zustand aller uebernommenen Projekte.
117 */
118 readonly haltlinieId: string | null;
119 /** Lage des Startpunkts auf der Haltlinie: 0 bei `von`, 1 bei `bis`. */
120 readonly startT: number;
121 /**
122 * Verknuepfte Signalgruppe.
123 *
124 * MEHRERE Linien duerfen auf dieselbe Gruppe zeigen. Eine Signalgruppe ist
125 * die Menge gleich schaltender Signalgeber, kein Verkehrsstrom - ein
126 * Signalgeber bedient haeufig geradeaus und rechts zugleich.
127 */
128 readonly signalGroupId: string | null;
129 }
130
131 /**
132 * Ein Arbeitsbereich: die Arbeitsstelle als Band laengs der Fahrbahn.
133 *
134 * WOFUER (gewuenscht am 11.09.2026 aus dem Betrieb): An einer einstreifigen
135 * Verkehrsfuehrung steht die Arbeitsstelle zuerst fest; Haltlinien,
136 * Aufstellung und Signalgeber richten sich nach ihr. Wer sie zeichnen kann,
137 * setzt die Haltlinien danach - und der Pruefer sieht auf dem Lageplanblatt,
138 * warum sie dort liegen.
139 *
140 * ER IST KEIN VERKEHRSSTROM, und das ist der Grund fuer ein eigenes Objekt
141 * statt eines Merkmals an `Planlinie`. Eine Planlinie traegt Verkehrsart,
142 * Fahrbeziehung und Signalgruppe und geht ueber `fahrlinienVon` in die
143 * Vermessung; ein Band, das dort mitliefe, erzeugte Kreuzungspunkte,
144 * Konfliktbereiche und am Ende Zwischenzeiten gegen eine Baustelle. Der
145 * Arbeitsbereich traegt deshalb keines dieser drei Felder und erscheint in
146 * `fahrlinienVon` nie.
147 *
148 * ER RECHNET NICHT MIT. Weder Zwischenzeit noch Freigabezeit noch Umlaufzeit
149 * kennen ihn. Abgeleitet werden aus ihm zwei Auskuenfte - seine Laenge und der
150 * Abstand jeder Haltlinie zu seinem naechsten Ende (siehe
151 * `geometrie/arbeitsbereich.ts`); beide werden angezeigt und gedruckt, nicht
152 * uebernommen.
153 */
154 export interface Arbeitsbereich {
155 readonly id: string;
156 readonly name: string;
157 /** Breite des Bandes in Metern. */
158 readonly breiteMeter: Meters;
159 /** Punkte in Bildpunkten; ab zwei Punkten ergibt sich ein Band. */
160 readonly punkte: readonly Punkt[];
161 }
162
163 /**
164 * Ein Signalgeber: Standort und Blickrichtung eines Ampelkopfs.
165 *
166 * WOFUER (Stufe 3 des Signallageplan-Entwurfs vom 12.09.2026): Zur
167 * verkehrstechnischen Unterlage gehoert ein bemasster Signallageplan, und
168 * dieses Programm wies ihn bisher als fehlend aus. Ihm fehlte genau ein
169 * Gegenstand - der Signalgeber mit Ort und Ausrichtung; Haltlinien,
170 * Fahrlinien, Massstab und Blatt gab es laengst.
171 *
172 * ER RECHNET NICHT MIT, und das ist keine vorlaeufige Einschraenkung, sondern
173 * die Bedingung, unter der es ihn gibt. Wie der Arbeitsbereich traegt er
174 * weder `mode` noch `movement`, erscheint in `fahrlinienVon` nie und erzeugt
175 * keine Konfliktbeziehung. Der Grund ist schaerfer als dort: Aus einem
176 * Signalgeberstandort LIESSE sich rechnen - der Abstand zur Haltlinie, eine
177 * Sichtweite, ein Raeumweganfang -, und jede dieser Groessen beruehrt die
178 * Zwischenzeit. Was Zwischenzeiten verkuerzt, ist die gefaehrliche Richtung.
179 * Solange die Regelwerke dazu nicht im Original vorliegen, wird hier
180 * gezeichnet und nicht gerechnet.
181 *
182 * DIE KENNUNG TRAEGT DER ANWENDER EIN. Ein Schema K1, K2, K3 nach Anlagenform
183 * waere bequem und an einer Engstelle mit zwei gleichwertigen Zulaeufen
184 * falsch: Welcher Zulauf K1 wird, entschiede der Feldindex, und ein
185 * Gleichstand darf nicht die Feldreihenfolge entscheiden. Vorbelegt wird aus
186 * dem Namen der verknuepften Signalgruppe, geaendert wird von Hand.
187 */
188 export interface Signalgeber {
189 readonly id: string;
190 /** Kennung auf dem Blatt, z. B. "K1" - vom Anwender vergeben. */
191 readonly name: string;
192 /** Standort in Bildpunkten. */
193 readonly punkt: Punkt;
194 /**
195 * Blickrichtung in Grad, 0 = nach rechts, im Uhrzeigersinn wachsend.
196 *
197 * DIESELBE ZAEHLUNG WIE IN DEN BILDPUNKTEN, in denen y nach unten waechst -
198 * ein Pfeil mit 90 Grad zeigt auf dem Bildschirm nach unten. Eine zweite
199 * Zaehlung fuer die Anzeige waere eine Umrechnung, die auseinanderlaufen
200 * kann; wer die Richtung lesen will, liest sie am gezeichneten Pfeil.
201 */
202 readonly ausrichtung: number;
203 /**
204 * Verknuepfte Signalgruppe, oder null.
205 *
206 * MEHRERE Signalgeber duerfen auf dieselbe Gruppe zeigen - eine Signalgruppe
207 * IST die Menge gleich schaltender Geber. Die Verknuepfung dient der
208 * Beschriftung und der Pruefung, nicht der Rechnung.
209 */
210 readonly signalGroupId: string | null;
211 }
212
213 export interface Lageplan {
214 readonly bild: LageplanBild | null;
215 readonly kalibrierung: Kalibrierung | null;
216 readonly haltlinien: readonly Haltlinie[];
217 readonly linien: readonly Planlinie[];
218 /**
219 * Gezeichnete Arbeitsstellen.
220 *
221 * Pflichtfeld wie die vier darueber: Ein wahlfreies Feld waere an jeder der
222 * 37 Stellen, die einen Lageplan bilden, stillschweigend leer - und an einer
223 * davon waere es dann ein Versehen statt einer Aussage.
224 */
225 readonly arbeitsbereiche: readonly Arbeitsbereich[];
226 /** Gezeichnete Signalgeberstandorte. Pflichtfeld aus demselben Grund. */
227 readonly signalgeber: readonly Signalgeber[];
228 }
229
230 export const LEERER_LAGEPLAN: Lageplan = {
231 bild: null,
232 kalibrierung: null,
233 haltlinien: [],
234 linien: [],
235 arbeitsbereiche: [],
236 signalgeber: [],
237 };
238
239 /** Punkt auf einer Haltlinie; `t` wird auf 0..1 begrenzt. */
240 export function punktAufHaltlinie(h: Haltlinie, t: number): Punkt {
241 const g = Number.isFinite(t) ? Math.min(1, Math.max(0, t)) : 0.5;
242 return {
243 x: h.von.x + (h.bis.x - h.von.x) * g,
244 y: h.von.y + (h.bis.y - h.von.y) * g,
245 };
246 }
247
248 /**
249 * Lage eines Punktes auf einer Haltlinie, als `t` zwischen 0 und 1.
250 *
251 * Gebraucht beim nachtraeglichen Zuordnen: Wird einer bereits gezeichneten
252 * Fahrlinie eine Haltlinie zugewiesen, muss ihr Anfangspunkt auf diese Linie
253 * wandern. Genommen wird der Lotfusspunkt - die naechstgelegene Stelle, also
254 * die, die der Anwender gemeint hat.
255 */
256 export function lotAuf(h: Haltlinie, punkt: Punkt | undefined): number {
257 if (punkt === undefined) return 0.5;
258 const dx = h.bis.x - h.von.x;
259 const dy = h.bis.y - h.von.y;
260 const laengeQuadrat = dx * dx + dy * dy;
261 // Entartete Haltlinie: Es gibt keine Richtung, also auch keine Lage darauf.
262 if (laengeQuadrat < 1e-9) return 0.5;
263 const t = ((punkt.x - h.von.x) * dx + (punkt.y - h.von.y) * dy) / laengeQuadrat;
264 return Math.min(1, Math.max(0, t));
265 }
266
267 /** Haltlinie einer Fahrlinie, sofern zugeordnet und vorhanden. */
268 export function haltlinieVon(lageplan: Lageplan, linie: Planlinie): Haltlinie | null {
269 if (linie.haltlinieId === null) return null;
270 return lageplan.haltlinien.find((h) => h.id === linie.haltlinieId) ?? null;
271 }
272
273 /** Stroeme einer Haltlinie, nach `startT` sortiert - ihre Reihenfolge im Querschnitt. */
274 export function stroemeAn(lageplan: Lageplan, haltlinieId: string): Planlinie[] {
275 return lageplan.linien
276 .filter((l) => l.haltlinieId === haltlinieId)
277 .sort((a, b) => a.startT - b.startT);
278 }
279
280 /** Alle Linien, die von dieser Signalgruppe geschaltet werden. */
281 export function linienDerGruppe(lageplan: Lageplan, gruppeId: string): Planlinie[] {
282 return lageplan.linien.filter((l) => l.signalGroupId === gruppeId);
283 }
284
285 /**
286 * Haelt die abgeleiteten Groessen gerade.
287 *
288 * Der erste Punkt einer Fahrlinie folgt aus ihrer Haltlinie und `startT`. Wird
289 * die Haltlinie verschoben, wandern alle ihre Stroeme mit - das ist der
290 * eigentliche Gewinn des eigenen Objekts: Die Haltlinie einer Zufahrt wird
291 * einmal gesetzt und einmal korrigiert, nicht je Strom.
292 *
293 * Ueberschrieben wird dabei keine Anwenderangabe: `punkte[0]` ist bei
294 * zugeordneter Haltlinie eine Rechengroesse. Verweise ins Leere bleiben
295 * unangetastet - sie werden beim Einlesen gemeldet und geloest, nicht hier
296 * stillschweigend.
297 */
298 export function normalisiere(lageplan: Lageplan): Lageplan {
299 if (lageplan.haltlinien.length === 0) return lageplan;
300 return {
301 ...lageplan,
302 linien: lageplan.linien.map((linie) => {
303 const h = haltlinieVon(lageplan, linie);
304 if (h === null || linie.punkte.length === 0) return linie;
305 return { ...linie, punkte: [punktAufHaltlinie(h, linie.startT), ...linie.punkte.slice(1)] };
306 }),
307 };
308 }
309
310 /** Regelbreite je Verkehrsart - Vorbelegung beim Zeichnen. */
311 export function regelbreite(mode: TrafficMode): Meters {
312 switch (mode) {
313 case 'kfz':
314 return REGELBREITE.kfz;
315 case 'rad':
316 return REGELBREITE.rad;
317 case 'fuss':
318 return REGELBREITE.fuss;
319 case 'oepnv':
320 return REGELBREITE.oepnv;
321 }
322 }
323
324 /** Ist der Lageplan auswertbar? */
325 export function istAuswertbar(lageplan: Lageplan): boolean {
326 return massstabVon(lageplan) !== null && lageplan.linien.some((l) => l.punkte.length >= 2);
327 }
328
329 /** Massstab aus der Kalibrierung. */
330 export function massstabVon(lageplan: Lageplan): Massstab | null {
331 const k = lageplan.kalibrierung;
332 if (k === null) return null;
333 return massstabAusStrecke(k.von, k.bis, k.laengeMeter);
334 }
335
336 /**
337 * Nachweis der Planungsgrundlage.
338 *
339 * Fuer die Pruefung der Unterlagen ist entscheidend, worauf die Raeum- und
340 * Einfahrwege beruhen: Dieselbe Zahl kann aus einem massstaeblichen Luftbild
341 * stammen oder von Hand geschaetzt sein. Ohne diesen Nachweis liess sich eine
342 * Zwischenzeit im Ausdruck nicht nachrechnen. Gebuendelt wird das hier und
343 * nicht in der Ausgabe - die Ausgabe rechnet nicht.
344 */
345 export interface Planungsgrundlage {
346 /** Liegt ueberhaupt eine Bildunterlage vor? */
347 readonly hatBild: boolean;
348 /** Herausgeber bzw. Dateiname; leer, wenn kein Bild vorliegt. */
349 readonly herkunft: string;
350 /** ISO-Zeitstempel des Ladens; leer, wenn kein Bild vorliegt. */
351 readonly geladenAm: string;
352 /** Fehlt das Ladedatum, stuende in der Ausgabe der 1.1.1970 - das ist kein Datum. */
353 readonly ladedatumBekannt: boolean;
354 readonly breiteBildpunkte: number;
355 readonly hoeheBildpunkte: number;
356 readonly massstabHerkunft: 'gemessen' | 'georeferenziert' | null;
357 /** Meter je Bildpunkt; null, solange kein Massstab festgelegt ist. */
358 readonly meterJeBildpunkt: number | null;
359 /** Laenge der Bezugsstrecke in Metern; null ohne Kalibrierung. */
360 readonly bezugslaengeMeter: Meters | null;
361 /** Dieselbe Strecke in Bildpunkten - daraus folgt der Massstab. */
362 readonly bezugslaengeBildpunkte: number | null;
363 /** Kantenlaenge des Ausschnitts am Boden: Bildbreite mal Massstab. */
364 readonly kantenlaengeMeter: Meters | null;
365 readonly anzahlHaltlinien: number;
366 readonly anzahlFahrlinien: number;
367 /** Sind daraus ueberhaupt Wege ableitbar? */
368 readonly auswertbar: boolean;
369 /** Wofuer die Aufloesung ausreicht; null, solange kein Massstab feststeht. */
370 readonly messgenauigkeit: Messgenauigkeit | null;
371 }
372
373 /**
374 * Stellt die Angaben zur Planungsgrundlage fuer Anzeige und Ausdruck zusammen.
375 *
376 * `defaults` wird bis zur Messgenauigkeit durchgereicht: Der Anwender darf die
377 * Raeum- und Einfahrgeschwindigkeiten anpassen, und mit ihnen verschiebt sich,
378 * welche Aufloesung noch ausreicht. Wer die Vorgaben des Projekts hat, gibt sie
379 * mit; sonst gilt das Regelwerk.
380 */
381 export function planungsgrundlage(
382 lageplan: Lageplan,
383 defaults: RilsaDefaults = RILSA_DEFAULTS,
384 ): Planungsgrundlage {
385 const bild = lageplan.bild;
386 const kalibrierung = lageplan.kalibrierung;
387 const massstab = massstabVon(lageplan);
388 return {
389 hatBild: bild !== null,
390 herkunft: bild?.herkunft ?? '',
391 geladenAm: bild?.geladenAm ?? '',
392 ladedatumBekannt: bild !== null && Date.parse(bild.geladenAm) > 0,
393 breiteBildpunkte: bild?.breite ?? 0,
394 hoeheBildpunkte: bild?.hoehe ?? 0,
395 massstabHerkunft: kalibrierung?.herkunft ?? null,
396 meterJeBildpunkt: massstab?.meterJeBildpunkt ?? null,
397 bezugslaengeMeter: kalibrierung?.laengeMeter ?? null,
398 bezugslaengeBildpunkte:
399 kalibrierung === null ? null : abstand(kalibrierung.von, kalibrierung.bis),
400 kantenlaengeMeter:
401 bild === null || massstab === null ? null : bild.breite * massstab.meterJeBildpunkt,
402 anzahlHaltlinien: lageplan.haltlinien.length,
403 anzahlFahrlinien: lageplan.linien.length,
404 auswertbar: istAuswertbar(lageplan),
405 messgenauigkeit: messgenauigkeit(lageplan, defaults),
406 };
407 }
408
409 // --- Messgenauigkeit --------------------------------------------------------
410
411 /**
412 * Wofuer die Aufloesung der Unterlage ausreicht.
413 *
414 * 'voll' auch Fussgaengerfurten sind abgreifbar
415 * 'nur-kfz' nur Kraftfahrzeugstroeme; eine Furt waere zu grob vermessen
416 * 'uebersicht' zum Betrachten und Einordnen, nicht zum Vermessen
417 */
418 export type Messeignung = 'voll' | 'nur-kfz' | 'uebersicht';
419
420 /** Was ein Zeichenfehler von zwei Bildpunkten in der Zwischenzeit anrichtet. */
421 export interface Messgenauigkeit {
422 readonly meterJeBildpunkt: number;
423 readonly eignung: Messeignung;
424 /** Fehler der Zwischenzeit bei einer Fussgaengerfurt, in Sekunden. */
425 readonly fehlerFussSekunden: Seconds;
426 /** Fehler der Zwischenzeit bei einem abbiegenden Kraftfahrzeugstrom, in Sekunden. */
427 readonly fehlerKfzSekunden: Seconds;
428 }
429
430 /**
431 * Zeichenfehler, der der Beurteilung zugrunde liegt - in Bildpunkten.
432 *
433 * Zwei Bildpunkte sind das, was beim Zeichnen zusammenkommt: einer fuer das
434 * Treffen der Kante, einer fuer das Erkennen, wo die Kante im Bild ueberhaupt
435 * liegt. Weiter zu unterstellen waere schoengerechnet.
436 *
437 * Die Annahme gilt fuer beide Wege, auf denen gezeichnet wird. Der feinste
438 * Schritt des Tastaturzeigers ist ausdruecklich auf einen Bildpunkt der
439 * Unterlage begrenzt (siehe zeigerSchritt() in src/ui/lageplan/leinwand.ts) und
440 * bleibt damit innerhalb dessen, was hier unterstellt ist - die Einstufung im
441 * Ausdruck gilt also fuer Maus wie Tastatur.
442 */
443 export const ZEICHENFEHLER_BILDPUNKTE = 2;
444
445 /**
446 * Groesster hinnehmbarer Fehler der Zwischenzeit aus dem Zeichnen - in Sekunden.
447 *
448 * Ein Viertel der Rundungsstufe: Die Zwischenzeit wird auf ganze Sekunden
449 * AUFGERUNDET. Bleibt der Zeichenfehler darunter, kann er das ausgegebene
450 * Ergebnis nur dann um eine Sekunde kippen, wenn der rechnerische Wert ohnehin
451 * dicht an einer Stufengrenze liegt. Wird die Schranke ueberschritten, verliert
452 * das Aufrunden seine Sicherheitswirkung - der Fehler frisst sie auf.
453 */
454 export const MESSFEHLER_SCHRANKE: Seconds = 0.25;
455
456 /**
457 * Was ein Zeichenfehler von zwei Bildpunkten in der Zwischenzeit kostet.
458 *
459 * WARUM DAS EINE SICHERHEITSGROESSE IST
460 *
461 * Aus dem Lageplan werden Raeum- und Einfahrweg VERMESSEN, und beide gehen
462 * unmittelbar in die Zwischenzeit ein:
463 *
464 * tz = tue + (sr + Fahrzeuglaenge) / vr - se / ve
465 *
466 * Die Uebertragung ist linear und damit vollstaendig ausrechenbar: Ein Fehler
467 * von d Metern auf dem Raeumweg wirkt mit d/vr Sekunden, einer auf dem
468 * Einfahrweg mit d/ve - und zwar mit umgekehrtem Vorzeichen, weshalb sich die
469 * beiden im unguenstigen Fall addieren statt aufzuheben. Beide Wege werden
470 * getrennt gezeichnet; der Zeichenfehler faellt deshalb zweimal an.
471 *
472 * Entscheidend ist, dass die Geschwindigkeit im Nenner steht. Derselbe
473 * Zeichenfehler kostet bei einem Fussgaenger fast das Zehnfache dessen, was er
474 * bei einem Kraftfahrzeug kostet. Ein Ausschnitt, auf dem sich Fahrspuren noch
475 * bequem abgreifen lassen, ist fuer eine Fussgaengerfurt laengst zu grob - und
476 * ein Luftbild sieht in beiden Faellen gleich aus. Genau davor warnt diese
477 * Groesse: Der eintretende Schaden ist eine zu KURZE Zwischenzeit an einer
478 * Furt, also die unsichere Richtung.
479 *
480 * Die Geschwindigkeiten stammen aus den Kennwerten und werden hier bewusst
481 * nicht wiederholt: Der Anwender darf sie anpassen, und eine zweite Fassung
482 * liefe still auseinander.
483 *
484 * Ohne Massstab gibt es keine Genauigkeit - dann null statt einer erfundenen
485 * Zahl.
486 */
487 export function messgenauigkeit(
488 lageplan: Lageplan,
489 defaults: RilsaDefaults = RILSA_DEFAULTS,
490 ): Messgenauigkeit | null {
491 const massstab = massstabVon(lageplan);
492 if (massstab === null) return null;
493
494 const g = ZEICHENFEHLER_BILDPUNKTE * massstab.meterJeBildpunkt;
495 /*
496 * WER RAEUMT UND WER FAEHRT EIN - die Paarung entscheidet ueber die Zahl.
497 *
498 * Zuvor stand hier die Einfahrgeschwindigkeit der Fussgaenger. Das beschreibt
499 * einen Konflikt, in dem ein Fussgaenger raeumt und ein FUSSGAENGER einfaehrt
500 * - eine Beziehung, die als signalisierter Konflikt praktisch nicht vorkommt.
501 * Die Zahl war dadurch um zwei Drittel zu gross: 1/1,2 + 1/1,5 = 1,50 statt
502 * 1/1,0 + 1/11,1 = 1,09. Kein einziges amtliches Luftbild erreichte damit die
503 * Stufe "voll" - auch das beste Angebot mit 10 cm je Bildpunkt nicht. Eine
504 * Warnung, die immer steht, wird nicht mehr gelesen.
505 *
506 * Der gefaehrliche Konflikt ist der umgekehrte und haeufige: Ein Fussgaenger
507 * raeumt die Furt, waehrend ein Kraftfahrzeug einfaehrt. Deshalb die
508 * Raeumgeschwindigkeit der Fussgaenger gegen die Einfahrgeschwindigkeit der
509 * Kraftfahrzeuge - dieselbe Paarung, die der Kfz-Wert unten schon benutzt.
510 *
511 * Angesetzt wird dabei der mobilitaetseingeschraenkte Fussgaenger mit 1,0 m/s.
512 * Er ist der Bemessungsfall, wo solche Stroeme vorkommen, und er ist die
513 * sichere Seite: Wer langsamer raeumt, bezahlt denselben Wegfehler mit mehr
514 * Sekunden. Der Unterschied zum Regelfall betraegt 11 Prozent und verschiebt
515 * keine der Stufen fuer die Dienste, die es tatsaechlich gibt.
516 */
517 const fehlerFussSekunden = zeitfehler(
518 g,
519 defaults.clearingSpeed.fussReducedMobility,
520 defaults.enteringSpeed.kfz,
521 );
522 // Abbiegende Kraftfahrzeuge raeumen langsamer als geradeausfahrende; von den
523 // Kfz-Werten ist das der unguenstigere und damit der massgebende.
524 const fehlerKfzSekunden = zeitfehler(
525 g,
526 defaults.clearingSpeed.kfzTurning,
527 defaults.enteringSpeed.kfz,
528 );
529
530 return {
531 meterJeBildpunkt: massstab.meterJeBildpunkt,
532 eignung: haeltSchranke(fehlerFussSekunden)
533 ? 'voll'
534 : haeltSchranke(fehlerKfzSekunden)
535 ? 'nur-kfz'
536 : 'uebersicht',
537 fehlerFussSekunden,
538 fehlerKfzSekunden,
539 };
540 }
541
542 /**
543 * Zeitfehler aus einem Wegfehler `g`, der auf Raeum- UND Einfahrweg anfaellt.
544 *
545 * Gerundet wird auf sechs Stellen, damit angezeigter Wert und Einstufung an
546 * genau derselben Zahl haengen. Ohne das ergaebe die Rechnung an der Schranke
547 * 0,24999999999999997 s: angezeigt stuende dort "0,25 s", eingestuft waere aber
548 * die schlechtere Stufe - ein Widerspruch, den niemand aufloesen koennte.
549 *
550 * Eine Geschwindigkeit von 0 ergibt einen unendlichen Fehler und damit die
551 * schlechteste Einstufung. Das ist die sichere Richtung; die Kennwertpruefung
552 * laesst solche Werte ohnehin nicht durch.
553 */
554 function zeitfehler(
555 g: Meters,
556 raeumgeschwindigkeit: number,
557 einfahrgeschwindigkeit: number,
558 ): Seconds {
559 return roundTo(g * (1 / raeumgeschwindigkeit + 1 / einfahrgeschwindigkeit), 6);
560 }
561
562 /** Genau auf der Schranke gilt als eingehalten - sie ist eine Obergrenze. */
563 function haeltSchranke(fehler: Seconds): boolean {
564 return fehler <= MESSFEHLER_SCHRANKE;
565 }
566
567 /**
568 * Wandelt die gezeichneten Linien in Fahrlinien mit Metermassen um.
569 * Linien mit weniger als zwei Punkten entfallen - sie beschreiben keinen Weg.
570 *
571 * Mitgegeben wird der Strombezug: Ohne ihn kann die Vermassung nicht
572 * unterscheiden, ob eine Beruehrung kurz hinter dem Start eine Kreuzung ist
573 * oder das Auffaechern zweier Stroeme aus derselben Zufahrt.
574 */
575 export function fahrlinienVon(lageplan: Lageplan): Fahrlinie[] {
576 const massstab = massstabVon(lageplan);
577 if (massstab === null) return [];
578
579 return lageplan.linien
580 .filter((linie) => linie.punkte.length >= 2)
581 .map((linie) => {
582 const h = haltlinieVon(lageplan, linie);
583 return linieInMeter(
584 {
585 id: linie.id,
586 punkte: linie.punkte,
587 breite: linie.breiteMeter,
588 bezug: {
589 haltlinieId: linie.haltlinieId,
590 haltlinienArt: h?.art ?? null,
591 mode: linie.mode,
592 // Entscheidet mit, ob eine Radlinie als Querung in beiden Richtungen
593 // vermessen wird (`querungsart` in vermessung.ts).
594 movement: linie.movement,
595 startT: linie.startT,
596 },
597 },
598 massstab,
599 );
600 });
601 }
602
603 /** Sucht die gezeichnete Linie zu einer Fahrlinie. */
604 export function planlinieVon(lageplan: Lageplan, id: string): Planlinie | undefined {
605 return lageplan.linien.find((l) => l.id === id);
606 }
607
608 /** Sucht einen Arbeitsbereich nach seiner Kennung. */
609 export function arbeitsbereichVon(lageplan: Lageplan, id: string): Arbeitsbereich | undefined {
610 return lageplan.arbeitsbereiche.find((a) => a.id === id);
611 }