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