lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ src domain geometrie lageplan.ts
| 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 | } |