lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ src domain geometrie ableitung.ts
| 1 | import { createId } from '../id'; |
| 2 | import { roundTo, type Meters, type Seconds } from '../units'; |
| 3 | import { computeIntergreen } from '../rilsa/intergreen'; |
| 4 | import { intergreenKey } from '../plan/signalPlan'; |
| 5 | import { resolveRilsaSettings } from '../rilsa/settings'; |
| 6 | import { TRAFFIC_MODE_LABELS } from '../rilsa/constants'; |
| 7 | import type { CalculationNote, Movement } from '../rilsa/types'; |
| 8 | import { defaultVehicleClass } from '../model/factory'; |
| 9 | import { |
| 10 | hatHandvorgaben, |
| 11 | WEGNACHWEIS_HANDEINGABE, |
| 12 | wegnachweisAusLageplan, |
| 13 | type Conflict, |
| 14 | type Project, |
| 15 | type SignalGroup, |
| 16 | } from '../model/project'; |
| 17 | import { |
| 18 | fahrlinienVon, |
| 19 | istAuswertbar, |
| 20 | planlinieVon, |
| 21 | type Lageplan, |
| 22 | type Planlinie, |
| 23 | } from './lageplan'; |
| 24 | import { |
| 25 | querungsart, |
| 26 | schneideMitBefund, |
| 27 | ueberlappung, |
| 28 | vermesseWege, |
| 29 | type Fahrlinie, |
| 30 | } from './vermessung'; |
| 31 | |
| 32 | /** |
| 33 | * Ableitung der Planungsdaten aus dem Lageplan. |
| 34 | * |
| 35 | * GRUNDSATZ: NICHTS WIRD STILL UEBERSCHRIEBEN |
| 36 | * |
| 37 | * Die Ableitung erzeugt Vorschlaege und stellt sie dem gegenueber, was im |
| 38 | * Projekt steht. Uebernommen wird nur, was der Anwender ausdruecklich |
| 39 | * bestaetigt. Andernfalls koennte eine unbedacht verschobene Linie eine von |
| 40 | * Hand geprueffte Vermassung zunichtemachen - und das faellt bei zwoelf |
| 41 | * Konfliktbeziehungen niemandem auf. |
| 42 | * |
| 43 | * Was sich ableiten laesst: |
| 44 | * - Signalgruppen aus den gezeichneten Stroemen (Vorschlag) |
| 45 | * - Vertraeglichkeit aus dem Schnitt der Fahrlinien (belastbar) |
| 46 | * - Raeum-/Einfahrweg aus der Geometrie (belastbar) |
| 47 | * |
| 48 | * Was sich NICHT ableiten laesst: die Phaseneinteilung. Welche vertraeglichen |
| 49 | * Gruppen man zu einer Phase zusammenfasst, ist eine Ingenieurentscheidung. |
| 50 | */ |
| 51 | |
| 52 | export interface SignalgruppenVorschlag { |
| 53 | readonly planlinieId: string; |
| 54 | readonly name: string; |
| 55 | readonly linie: Planlinie; |
| 56 | /** Bereits mit einer Signalgruppe verknuepft? */ |
| 57 | readonly signalGroupId: string | null; |
| 58 | readonly status: 'neu' | 'verknuepft' | 'name-abweichend'; |
| 59 | } |
| 60 | |
| 61 | export interface WegeVorschlag { |
| 62 | /** Raeumende Linie. */ |
| 63 | readonly vonLinieId: string; |
| 64 | readonly nachLinieId: string; |
| 65 | readonly vonName: string; |
| 66 | readonly nachName: string; |
| 67 | readonly raeumweg: Meters; |
| 68 | readonly einfahrweg: Meters; |
| 69 | readonly winkelGrad: number; |
| 70 | /** Was im Projekt steht, sofern die Beziehung dort erfasst ist. */ |
| 71 | readonly bestehend: { readonly raeumweg: Meters; readonly einfahrweg: Meters } | null; |
| 72 | /** Weicht der gemessene Wert vom erfassten ab? */ |
| 73 | readonly abweichung: boolean; |
| 74 | } |
| 75 | |
| 76 | /** Warum zwei Stroeme feindlich oder vertraeglich sind. */ |
| 77 | export type Vertraeglichkeitsgrund = |
| 78 | 'kreuzung' | 'ueberlagerung' | 'gemeinsamer-fahrstreifen' | 'auffaecherung' | 'kein-schnitt'; |
| 79 | |
| 80 | export interface Beurteilung { |
| 81 | readonly feindlich: boolean; |
| 82 | /** |
| 83 | * Widersprechen Zeichnung und fachliche Erwartung? Dann wird der Vorschlag |
| 84 | * gefuehrt, aber nicht vorausgewaehlt - die Entscheidung gehoert dem Planer. |
| 85 | */ |
| 86 | readonly zuPruefen: boolean; |
| 87 | readonly grund: Vertraeglichkeitsgrund; |
| 88 | /** Anwendertext fuer Tabelle und Pruefbericht. */ |
| 89 | readonly begruendung: string; |
| 90 | } |
| 91 | |
| 92 | export interface VertraeglichkeitVorschlag { |
| 93 | readonly aLinieId: string; |
| 94 | readonly bLinieId: string; |
| 95 | readonly aName: string; |
| 96 | readonly bName: string; |
| 97 | /** Kreuzen sich die Fahrlinien? */ |
| 98 | readonly feindlich: boolean; |
| 99 | readonly zuPruefen: boolean; |
| 100 | readonly grund: Vertraeglichkeitsgrund; |
| 101 | readonly begruendung: string; |
| 102 | /** Wie ist es im Projekt erfasst? `null`, wenn beide Gruppen fehlen. */ |
| 103 | readonly imProjekt: boolean | null; |
| 104 | readonly abweichung: boolean; |
| 105 | } |
| 106 | |
| 107 | /** |
| 108 | * Eine Beruehrung, die als Auffaecherung gewertet und deshalb nicht als |
| 109 | * Konflikt gefuehrt wurde. |
| 110 | * |
| 111 | * Wird ausdruecklich ausgewiesen: Das Unterdruecken eines Konflikts ist |
| 112 | * sicherheitsrelevant und darf nie stillschweigend geschehen. |
| 113 | */ |
| 114 | export interface UnterdruecktesPaar { |
| 115 | readonly aLinieId: string; |
| 116 | readonly bLinieId: string; |
| 117 | readonly aName: string; |
| 118 | readonly bName: string; |
| 119 | readonly anzahl: number; |
| 120 | } |
| 121 | |
| 122 | export interface Ableitung { |
| 123 | readonly auswertbar: boolean; |
| 124 | readonly signalgruppen: readonly SignalgruppenVorschlag[]; |
| 125 | readonly vertraeglichkeit: readonly VertraeglichkeitVorschlag[]; |
| 126 | readonly wege: readonly WegeVorschlag[]; |
| 127 | readonly unterdrueckt: readonly UnterdruecktesPaar[]; |
| 128 | readonly hinweise: readonly CalculationNote[]; |
| 129 | } |
| 130 | |
| 131 | /** |
| 132 | * Beurteilt ein Strompaar. |
| 133 | * |
| 134 | * Drei Faelle, in dieser Reihenfolge: |
| 135 | * 1. Punktschnitt -> feindlich. Die Auffaecherung aus derselben Zufahrt ist |
| 136 | * hier bereits herausgerechnet (siehe vermessung.ts). |
| 137 | * 2. Ueberlagerung -> feindlich, ausser die Stroeme teilen sich einen |
| 138 | * Fahrstreifen derselben Zufahrt; dann sind sie vertraeglich, muessen aber |
| 139 | * dieselbe Signalgruppe bekommen. |
| 140 | * 3. Sonst -> vertraeglich. |
| 141 | */ |
| 142 | export function beurteile(a: Fahrlinie, b: Fahrlinie, nameA: string, nameB: string): Beurteilung { |
| 143 | const befund = schneideMitBefund(a, b); |
| 144 | const gleicheHaltlinie = |
| 145 | a.bezug?.haltlinieId != null && a.bezug.haltlinieId === b.bezug?.haltlinieId; |
| 146 | const nurKfz = a.bezug?.mode === 'kfz' && b.bezug?.mode === 'kfz'; |
| 147 | |
| 148 | const ersterSchnitt = befund.schnitte[0]; |
| 149 | if (ersterSchnitt !== undefined) { |
| 150 | // Zwei Kfz-Stroeme derselben Haltlinie, die sich trotzdem schneiden: |
| 151 | // fachlich moeglich - Linksabbieger aus Seitenlage, Wendefahrbeziehung -, |
| 152 | // ebenso haeufig aber ein zu weit ausholend gezeichneter Bogen. Das ist |
| 153 | // eine Entscheidung des Planers und wird nicht vorausgewaehlt. |
| 154 | const zuPruefen = gleicheHaltlinie && nurKfz; |
| 155 | return { |
| 156 | feindlich: true, |
| 157 | zuPruefen, |
| 158 | grund: 'kreuzung', |
| 159 | begruendung: zuPruefen |
| 160 | ? `„${nameA}“ und „${nameB}“ beginnen an derselben Haltlinie, kreuzen sich aber ` + |
| 161 | `${ersterSchnitt.abstandA.toFixed(1)} m dahinter. Das ist ein echter Konflikt, wenn ein ` + |
| 162 | 'Strom die Bahn des anderen schneidet – und ein Zeichenfehler, wenn der Bogen zu weit ' + |
| 163 | 'ausholt. Bitte entscheiden Sie.' |
| 164 | : `Die Fahrlinien von „${nameA}“ und „${nameB}“ kreuzen sich.`, |
| 165 | }; |
| 166 | } |
| 167 | |
| 168 | const ueber = ueberlappung(a, b); |
| 169 | if (ueber !== null) { |
| 170 | if (gleicheHaltlinie && nurKfz) { |
| 171 | return { |
| 172 | feindlich: false, |
| 173 | zuPruefen: false, |
| 174 | grund: 'gemeinsamer-fahrstreifen', |
| 175 | begruendung: |
| 176 | `„${nameA}“ und „${nameB}“ benutzen über ${ueber.laenge.toFixed(1)} m denselben ` + |
| 177 | 'Fahrstreifen. Sie sind verträglich, müssen aber dieselbe Signalgruppe erhalten – ' + |
| 178 | 'getrennt signalisieren lässt sich nur, was einen eigenen Fahrstreifen hat.', |
| 179 | }; |
| 180 | } |
| 181 | return { |
| 182 | feindlich: true, |
| 183 | zuPruefen: false, |
| 184 | grund: 'ueberlagerung', |
| 185 | begruendung: |
| 186 | `Die Korridore von „${nameA}“ und „${nameB}“ überdecken sich auf ` + |
| 187 | `${ueber.laenge.toFixed(1)} m. Sie beanspruchen dieselbe Fläche.`, |
| 188 | }; |
| 189 | } |
| 190 | |
| 191 | return { |
| 192 | feindlich: false, |
| 193 | zuPruefen: false, |
| 194 | grund: befund.unterdrueckt > 0 ? 'auffaecherung' : 'kein-schnitt', |
| 195 | begruendung: |
| 196 | befund.unterdrueckt > 0 |
| 197 | ? `„${nameA}“ und „${nameB}“ fahren von derselben Haltlinie ab und fächern auf. Ströme ` + |
| 198 | 'derselben Zufahrt sind untereinander verträglich – sie bekommen gleichzeitig Grün.' |
| 199 | : `Die Fahrlinien von „${nameA}“ und „${nameB}“ berühren sich nicht.`, |
| 200 | }; |
| 201 | } |
| 202 | |
| 203 | /** Wertet den Lageplan aus und stellt das Ergebnis dem Projekt gegenueber. */ |
| 204 | export function leiteAb(project: Project, lageplan: Lageplan): Ableitung { |
| 205 | const hinweise: CalculationNote[] = []; |
| 206 | |
| 207 | if (lageplan.bild === null) { |
| 208 | hinweise.push({ |
| 209 | severity: 'hinweis', |
| 210 | code: 'kein-bild', |
| 211 | message: 'Es ist noch kein Lageplan eingelesen.', |
| 212 | }); |
| 213 | } |
| 214 | if (lageplan.kalibrierung === null) { |
| 215 | hinweise.push({ |
| 216 | severity: 'fehler', |
| 217 | code: 'kein-massstab', |
| 218 | message: |
| 219 | 'Der Maßstab ist nicht festgelegt. Ziehen Sie eine Strecke bekannter Länge im Plan und ' + |
| 220 | 'geben Sie ihre Länge in Metern an - ohne Maßstab lassen sich keine Wege ermitteln.', |
| 221 | }); |
| 222 | } |
| 223 | |
| 224 | const zeichnungen = lageplan.linien.filter((l) => l.punkte.length >= 2); |
| 225 | const unfertig = lageplan.linien.length - zeichnungen.length; |
| 226 | if (unfertig > 0) { |
| 227 | hinweise.push({ |
| 228 | severity: 'warnung', |
| 229 | code: 'linie-unfertig', |
| 230 | message: `${unfertig} Fahrlinie(n) bestehen aus weniger als zwei Punkten und werden nicht ausgewertet.`, |
| 231 | }); |
| 232 | } |
| 233 | |
| 234 | // Ohne Haltlinie entscheidet allein die Zeichnung - und dann gilt der |
| 235 | // gemeinsame Anfangspunkt zweier Stroeme derselben Zufahrt als Kreuzung. |
| 236 | // Der Hinweis traegt bewusst die Schwere "warnung": Die Anzeige blendet |
| 237 | // blosse Hinweise aus, und das hier ist die einzige Rueckmeldung gegen den |
| 238 | // wahrscheinlichsten Anwenderfehler ueberhaupt. |
| 239 | const ohneHaltlinie = zeichnungen.filter((l) => l.haltlinieId === null); |
| 240 | if (ohneHaltlinie.length > 0 && lageplan.linien.length > 1) { |
| 241 | hinweise.push({ |
| 242 | severity: 'warnung', |
| 243 | code: 'haltlinie-fehlt', |
| 244 | message: |
| 245 | `${ohneHaltlinie.length} Fahrlinie(n) beginnen an keiner Haltlinie. Solange das so ist, ` + |
| 246 | 'entscheidet allein die Zeichnung über die Verträglichkeit; Ströme derselben Zufahrt ' + |
| 247 | 'können dadurch als feindlich vorgeschlagen werden, obwohl sie gleichzeitig Grün ' + |
| 248 | 'bekommen müssen. Zeichnen Sie die Haltlinie der Zufahrt ein und setzen Sie die ' + |
| 249 | 'Fahrlinien darauf an.', |
| 250 | }); |
| 251 | } |
| 252 | |
| 253 | if (!istAuswertbar(lageplan)) { |
| 254 | return { |
| 255 | auswertbar: false, |
| 256 | signalgruppen: [], |
| 257 | vertraeglichkeit: [], |
| 258 | wege: [], |
| 259 | unterdrueckt: [], |
| 260 | hinweise, |
| 261 | }; |
| 262 | } |
| 263 | |
| 264 | const fahrlinien = fahrlinienVon(lageplan); |
| 265 | const nachId = new Map(fahrlinien.map((f) => [f.id, f])); |
| 266 | |
| 267 | return { |
| 268 | auswertbar: true, |
| 269 | signalgruppen: signalgruppenVorschlaege(project, zeichnungen), |
| 270 | vertraeglichkeit: vertraeglichkeitVorschlaege(project, zeichnungen, nachId), |
| 271 | wege: wegeVorschlaege(project, zeichnungen, nachId, hinweise), |
| 272 | unterdrueckt: unterdrueckteBeruehrungen(zeichnungen, nachId), |
| 273 | hinweise, |
| 274 | }; |
| 275 | } |
| 276 | |
| 277 | /** |
| 278 | * Sammelt die Beruehrungen, die als Auffaecherung gewertet wurden. |
| 279 | * |
| 280 | * Sie erscheinen in der Oberflaeche als eigener, aufklappbarer Abschnitt. Ein |
| 281 | * Pruefer muss nachvollziehen koennen, welche Beruehrungen das Programm nicht |
| 282 | * als Konflikt gefuehrt hat und warum. |
| 283 | */ |
| 284 | function unterdrueckteBeruehrungen( |
| 285 | linien: readonly Planlinie[], |
| 286 | fahrlinien: ReadonlyMap<string, Fahrlinie>, |
| 287 | ): UnterdruecktesPaar[] { |
| 288 | const ergebnis: UnterdruecktesPaar[] = []; |
| 289 | |
| 290 | for (let i = 0; i < linien.length; i += 1) { |
| 291 | for (let j = i + 1; j < linien.length; j += 1) { |
| 292 | const a = linien[i]!; |
| 293 | const b = linien[j]!; |
| 294 | const fa = fahrlinien.get(a.id); |
| 295 | const fb = fahrlinien.get(b.id); |
| 296 | if (!fa || !fb) continue; |
| 297 | |
| 298 | const befund = schneideMitBefund(fa, fb); |
| 299 | if (befund.unterdrueckt === 0) continue; |
| 300 | ergebnis.push({ |
| 301 | aLinieId: a.id, |
| 302 | bLinieId: b.id, |
| 303 | aName: a.name, |
| 304 | bName: b.name, |
| 305 | anzahl: befund.unterdrueckt, |
| 306 | }); |
| 307 | } |
| 308 | } |
| 309 | |
| 310 | return ergebnis; |
| 311 | } |
| 312 | |
| 313 | // --- Signalgruppen ---------------------------------------------------------- |
| 314 | |
| 315 | function signalgruppenVorschlaege( |
| 316 | project: Project, |
| 317 | linien: readonly Planlinie[], |
| 318 | ): SignalgruppenVorschlag[] { |
| 319 | return linien.map((linie) => { |
| 320 | const verknuepft = |
| 321 | linie.signalGroupId === null |
| 322 | ? undefined |
| 323 | : project.signalGroups.find((g) => g.id === linie.signalGroupId); |
| 324 | |
| 325 | return { |
| 326 | planlinieId: linie.id, |
| 327 | name: linie.name, |
| 328 | linie, |
| 329 | signalGroupId: verknuepft?.id ?? null, |
| 330 | status: |
| 331 | verknuepft === undefined |
| 332 | ? 'neu' |
| 333 | : verknuepft.name === linie.name |
| 334 | ? 'verknuepft' |
| 335 | : 'name-abweichend', |
| 336 | }; |
| 337 | }); |
| 338 | } |
| 339 | |
| 340 | /** |
| 341 | * Legt fuer die gewaehlten Linien Signalgruppen an und verknuepft sie. |
| 342 | * Bereits verknuepfte Linien bleiben unberuehrt. |
| 343 | */ |
| 344 | export function uebernimmSignalgruppen( |
| 345 | project: Project, |
| 346 | lageplan: Lageplan, |
| 347 | planlinienIds: readonly string[], |
| 348 | ): { project: Project; lageplan: Lageplan; angelegt: number } { |
| 349 | const gewaehlt = new Set(planlinienIds); |
| 350 | const neueGruppen: SignalGroup[] = []; |
| 351 | const zuordnung = new Map<string, string>(); |
| 352 | |
| 353 | for (const linie of lageplan.linien) { |
| 354 | if (!gewaehlt.has(linie.id)) continue; |
| 355 | if ( |
| 356 | linie.signalGroupId !== null && |
| 357 | project.signalGroups.some((g) => g.id === linie.signalGroupId) |
| 358 | ) { |
| 359 | continue; |
| 360 | } |
| 361 | |
| 362 | const gruppe: SignalGroup = { |
| 363 | id: createId('sg'), |
| 364 | name: linie.name, |
| 365 | mode: linie.mode, |
| 366 | movement: linie.movement, |
| 367 | armId: null, |
| 368 | vZul: project.intersection.vZul, |
| 369 | lanes: 1, |
| 370 | vehicleClass: defaultVehicleClass(linie.mode), |
| 371 | minGreenOverride: null, |
| 372 | maxGreenOverride: null, |
| 373 | reducedMobility: false, |
| 374 | // Ausdruecklich `false` wie in factory.ts (createSignalGroup): Eine aus |
| 375 | // dem Lageplan angelegte Gruppe ist eine neue Aussage des Bearbeiters, |
| 376 | // kein Altbestand ohne das Feld (Fassung 5.4.0, Befund C1). |
| 377 | blindenzusatz: false, |
| 378 | color: farbeFuer(project.signalGroups.length + neueGruppen.length), |
| 379 | }; |
| 380 | neueGruppen.push(gruppe); |
| 381 | zuordnung.set(linie.id, gruppe.id); |
| 382 | } |
| 383 | |
| 384 | return { |
| 385 | project: { ...project, signalGroups: [...project.signalGroups, ...neueGruppen] }, |
| 386 | lageplan: { |
| 387 | ...lageplan, |
| 388 | linien: lageplan.linien.map((l) => |
| 389 | zuordnung.has(l.id) ? { ...l, signalGroupId: zuordnung.get(l.id)! } : l, |
| 390 | ), |
| 391 | }, |
| 392 | angelegt: neueGruppen.length, |
| 393 | }; |
| 394 | } |
| 395 | |
| 396 | /** |
| 397 | * Ordnet eine gezeichnete Fahrlinie einer BESTEHENDEN Signalgruppe zu. |
| 398 | * |
| 399 | * Damit wird der Regelfall erstmals abbildbar: ein Signalgeber fuer geradeaus |
| 400 | * und rechts. Eine Signalgruppe ist die Menge gleich schaltender Signalgeber, |
| 401 | * kein Verkehrsstrom - sie darf mehrere Stroeme bedienen. Bisher legte die |
| 402 | * Ableitung stur je Linie eine eigene Gruppe an; wer zwei Stroeme auf einen |
| 403 | * Signalgeber legen wollte, konnte es gar nicht ausdruecken. |
| 404 | * |
| 405 | * Drei Sperren, jede aus einem eigenen Grund: |
| 406 | * |
| 407 | * 1. NUR EINE VERKEHRSART je Gruppe. Fuer Fussgaenger, Rad und Kraftfahrzeuge |
| 408 | * gelten verschiedene Mindestfreigabe- und Raeumzeiten; eine gemeinsame |
| 409 | * Gruppe koennte sie nicht zugleich einhalten. |
| 410 | * 2. NUR STROEME EINER HALTLINIE. Gleich schaltende Signalgeber stehen an |
| 411 | * einem Querschnitt. Stroeme verschiedener Haltlinien zusammenzulegen |
| 412 | * hiesse, zwei Aufstellbereiche mit einem Geber zu regeln. |
| 413 | * 3. NIE SICH KREUZENDE STROEME. Sie erhielten sonst gleichzeitig Freigabe |
| 414 | * fuer einander feindliche Bewegungen - und zwar unbemerkt, denn zwischen |
| 415 | * Stroemen derselben Gruppe gibt es keine Zwischenzeit. |
| 416 | * |
| 417 | * Die Zuordnung wird nicht erzwungen: Ist sie unzulaessig, bleibt das Projekt |
| 418 | * unveraendert und der Grund wird zurueckgegeben. |
| 419 | */ |
| 420 | export function verbindeMitGruppe( |
| 421 | project: Project, |
| 422 | lageplan: Lageplan, |
| 423 | linieId: string, |
| 424 | gruppeId: string | null, |
| 425 | ): { lageplan: Lageplan; hinweis: CalculationNote | null } { |
| 426 | const linie = planlinieVon(lageplan, linieId); |
| 427 | if (linie === undefined) return { lageplan, hinweis: null }; |
| 428 | |
| 429 | const loesen = (): { lageplan: Lageplan; hinweis: CalculationNote | null } => ({ |
| 430 | lageplan: { |
| 431 | ...lageplan, |
| 432 | linien: lageplan.linien.map((l) => |
| 433 | l.id === linieId ? { ...l, signalGroupId: gruppeId } : l, |
| 434 | ), |
| 435 | }, |
| 436 | hinweis: null, |
| 437 | }); |
| 438 | |
| 439 | if (gruppeId === null) return loesen(); |
| 440 | |
| 441 | const gruppe = project.signalGroups.find((g) => g.id === gruppeId); |
| 442 | if (gruppe === undefined) return { lageplan, hinweis: null }; |
| 443 | |
| 444 | const verweigere = ( |
| 445 | code: string, |
| 446 | message: string, |
| 447 | ): { lageplan: Lageplan; hinweis: CalculationNote } => ({ |
| 448 | lageplan, |
| 449 | hinweis: { severity: 'warnung', code, message }, |
| 450 | }); |
| 451 | |
| 452 | if (gruppe.mode !== linie.mode) { |
| 453 | return verweigere( |
| 454 | 'gruppe-fremde-verkehrsart', |
| 455 | `„${gruppe.name}“ schaltet ${TRAFFIC_MODE_LABELS[gruppe.mode]}, „${linie.name}“ ist ` + |
| 456 | `${TRAFFIC_MODE_LABELS[linie.mode]}. Eine Signalgruppe bedient nur eine Verkehrsart – ` + |
| 457 | 'die Mindestfreigabe- und Räumzeiten unterscheiden sich.', |
| 458 | ); |
| 459 | } |
| 460 | |
| 461 | const geschwister = lageplan.linien.filter( |
| 462 | (l) => l.id !== linieId && l.signalGroupId === gruppeId, |
| 463 | ); |
| 464 | |
| 465 | const fremdeHaltlinie = geschwister.find((l) => l.haltlinieId !== linie.haltlinieId); |
| 466 | if (fremdeHaltlinie !== undefined) { |
| 467 | return verweigere( |
| 468 | 'gruppe-fremde-haltlinie', |
| 469 | `„${gruppe.name}“ schaltet bereits „${fremdeHaltlinie.name}“ an einer anderen Haltlinie. ` + |
| 470 | 'Gleich schaltende Signalgeber stehen an einem Querschnitt.', |
| 471 | ); |
| 472 | } |
| 473 | |
| 474 | const fahrlinien = new Map(fahrlinienVon(lageplan).map((f) => [f.id, f])); |
| 475 | const eigene = fahrlinien.get(linieId); |
| 476 | if (eigene !== undefined) { |
| 477 | for (const bruder of geschwister) { |
| 478 | const andere = fahrlinien.get(bruder.id); |
| 479 | if (andere === undefined) continue; |
| 480 | if (beurteile(eigene, andere, linie.name, bruder.name).feindlich) { |
| 481 | return verweigere( |
| 482 | 'gruppe-kreuzende-stroeme', |
| 483 | `„${linie.name}“ und „${bruder.name}“ kreuzen sich. Sie dürfen nicht denselben ` + |
| 484 | 'Signalgeber bekommen – zwischen Strömen einer Gruppe gibt es keine Zwischenzeit, ' + |
| 485 | 'der Konflikt bliebe ungesichert.', |
| 486 | ); |
| 487 | } |
| 488 | } |
| 489 | } |
| 490 | |
| 491 | return loesen(); |
| 492 | } |
| 493 | |
| 494 | const FARBEN = [ |
| 495 | '#2f6fd0', |
| 496 | '#e07b39', |
| 497 | '#4a9e6a', |
| 498 | '#a05ec4', |
| 499 | '#c4526a', |
| 500 | '#3d9bb0', |
| 501 | '#8a7b3a', |
| 502 | '#6b6f7a', |
| 503 | ] as const; |
| 504 | |
| 505 | function farbeFuer(index: number): string { |
| 506 | return FARBEN[index % FARBEN.length] ?? '#2f6fd0'; |
| 507 | } |
| 508 | |
| 509 | // --- Vertraeglichkeit ------------------------------------------------------- |
| 510 | |
| 511 | function vertraeglichkeitVorschlaege( |
| 512 | project: Project, |
| 513 | linien: readonly Planlinie[], |
| 514 | fahrlinien: ReadonlyMap<string, Fahrlinie>, |
| 515 | ): VertraeglichkeitVorschlag[] { |
| 516 | const ergebnis: VertraeglichkeitVorschlag[] = []; |
| 517 | |
| 518 | for (let i = 0; i < linien.length; i += 1) { |
| 519 | for (let j = i + 1; j < linien.length; j += 1) { |
| 520 | const a = linien[i]!; |
| 521 | const b = linien[j]!; |
| 522 | const fa = fahrlinien.get(a.id); |
| 523 | const fb = fahrlinien.get(b.id); |
| 524 | if (!fa || !fb) continue; |
| 525 | |
| 526 | const urteil = beurteile(fa, fb, a.name, b.name); |
| 527 | const imProjekt = konfliktImProjekt(project, a, b); |
| 528 | |
| 529 | ergebnis.push({ |
| 530 | aLinieId: a.id, |
| 531 | bLinieId: b.id, |
| 532 | aName: a.name, |
| 533 | bName: b.name, |
| 534 | feindlich: urteil.feindlich, |
| 535 | zuPruefen: urteil.zuPruefen, |
| 536 | grund: urteil.grund, |
| 537 | begruendung: urteil.begruendung, |
| 538 | imProjekt, |
| 539 | abweichung: imProjekt !== null && imProjekt !== urteil.feindlich, |
| 540 | }); |
| 541 | } |
| 542 | } |
| 543 | |
| 544 | return ergebnis; |
| 545 | } |
| 546 | |
| 547 | /** `null`, wenn eine der beiden Linien noch keiner Signalgruppe zugeordnet ist. */ |
| 548 | function konfliktImProjekt(project: Project, a: Planlinie, b: Planlinie): boolean | null { |
| 549 | if (a.signalGroupId === null || b.signalGroupId === null) return null; |
| 550 | const gruppenDa = |
| 551 | project.signalGroups.some((g) => g.id === a.signalGroupId) && |
| 552 | project.signalGroups.some((g) => g.id === b.signalGroupId); |
| 553 | if (!gruppenDa) return null; |
| 554 | |
| 555 | return project.conflicts.some( |
| 556 | (c) => |
| 557 | (c.fromId === a.signalGroupId && c.toId === b.signalGroupId) || |
| 558 | (c.fromId === b.signalGroupId && c.toId === a.signalGroupId), |
| 559 | ); |
| 560 | } |
| 561 | |
| 562 | // --- Wege ------------------------------------------------------------------- |
| 563 | |
| 564 | function wegeVorschlaege( |
| 565 | project: Project, |
| 566 | linien: readonly Planlinie[], |
| 567 | fahrlinien: ReadonlyMap<string, Fahrlinie>, |
| 568 | hinweise: CalculationNote[], |
| 569 | ): WegeVorschlag[] { |
| 570 | const ergebnis: WegeVorschlag[] = []; |
| 571 | |
| 572 | for (const von of linien) { |
| 573 | for (const nach of linien) { |
| 574 | if (von.id === nach.id) continue; |
| 575 | // Zwei Stroeme derselben Signalgruppe schalten stets gleich. Zwischen |
| 576 | // ihnen gibt es keinen Signalwechsel und damit keine Zwischenzeit - sie |
| 577 | // waere schalttechnisch nicht darstellbar. Kreuzen sie sich, ist nicht |
| 578 | // die Zwischenzeit das Problem, sondern die Buendelung. |
| 579 | if (von.signalGroupId !== null && von.signalGroupId === nach.signalGroupId) continue; |
| 580 | const fv = fahrlinien.get(von.id); |
| 581 | const fn = fahrlinien.get(nach.id); |
| 582 | if (!fv || !fn) continue; |
| 583 | |
| 584 | // Nur feindliche Beziehungen brauchen eine Zwischenzeit. Ohne diese |
| 585 | // Pruefung entstuenden Vorschlaege fuer Stroeme, die gleichzeitig Gruen |
| 586 | // bekommen - etwa fuer die drei Fahrbeziehungen einer einstreifigen |
| 587 | // Zufahrt, deren Korridore sich zwangslaeufig ueberdecken. |
| 588 | if (!beurteile(fv, fn, von.name, nach.name).feindlich) continue; |
| 589 | |
| 590 | const wege = vermesseWege(fv, fn); |
| 591 | if (wege === null) continue; |
| 592 | |
| 593 | // Nicht bei einer Querung: Dort ist der Einfahrweg 0 m die Regel und |
| 594 | // kein Zeichenfehler - der einfahrende Fussgaenger steht am |
| 595 | // Fahrbahnrand, und die Vermessung setzt ihn deshalb auf 0 m |
| 596 | // (`vermesseWege`). Die Meldung stuende sonst an jeder Furt. |
| 597 | if (wege.einfahrweg <= 0 && querungsart(fn) === null) { |
| 598 | hinweise.push({ |
| 599 | severity: 'warnung', |
| 600 | code: 'einfahrweg-null', |
| 601 | message: |
| 602 | `Für "${von.name}" nach "${nach.name}" liegt der Konfliktbereich unmittelbar an der ` + |
| 603 | 'Haltlinie des einfahrenden Stroms. Prüfen Sie, ob die Haltlinie richtig gesetzt ist.', |
| 604 | }); |
| 605 | } |
| 606 | |
| 607 | const bestehend = bestehendeWege(project, von, nach); |
| 608 | ergebnis.push({ |
| 609 | vonLinieId: von.id, |
| 610 | nachLinieId: nach.id, |
| 611 | vonName: von.name, |
| 612 | nachName: nach.name, |
| 613 | raeumweg: wege.raeumweg, |
| 614 | einfahrweg: wege.einfahrweg, |
| 615 | winkelGrad: wege.winkelGrad, |
| 616 | bestehend, |
| 617 | abweichung: |
| 618 | bestehend !== null && |
| 619 | (Math.abs(bestehend.raeumweg - wege.raeumweg) > 0.05 || |
| 620 | Math.abs(bestehend.einfahrweg - wege.einfahrweg) > 0.05), |
| 621 | }); |
| 622 | } |
| 623 | } |
| 624 | |
| 625 | return ergebnis; |
| 626 | } |
| 627 | |
| 628 | function bestehendeWege( |
| 629 | project: Project, |
| 630 | von: Planlinie, |
| 631 | nach: Planlinie, |
| 632 | ): { raeumweg: Meters; einfahrweg: Meters } | null { |
| 633 | if (von.signalGroupId === null || nach.signalGroupId === null) return null; |
| 634 | const konflikt = project.conflicts.find( |
| 635 | (c) => c.fromId === von.signalGroupId && c.toId === nach.signalGroupId, |
| 636 | ); |
| 637 | if (!konflikt) return null; |
| 638 | return { |
| 639 | raeumweg: roundTo(konflikt.clearingDistance, 2), |
| 640 | einfahrweg: roundTo(konflikt.enteringDistance, 2), |
| 641 | }; |
| 642 | } |
| 643 | |
| 644 | /** |
| 645 | * Uebernimmt Vertraeglichkeit und Wege der gewaehlten Beziehungen in das |
| 646 | * Projekt. Nicht gewaehlte Beziehungen bleiben unveraendert. |
| 647 | * |
| 648 | * DIE EINZIGE STELLE IM PROGRAMM, die den Herkunftsnachweis "aus dem Lageplan" |
| 649 | * vergibt - und sie vergibt ihn nur fuer die Beziehungen, deren Wege sie im |
| 650 | * selben Zug schreibt. Jede spaetere Aenderung an diesen Werten setzt ihn ueber |
| 651 | * `aendereKonflikt` wieder zurueck. |
| 652 | * |
| 653 | * `now` ist einspeisbar, damit der Zeitstempel in Tests reproduzierbar bleibt. |
| 654 | */ |
| 655 | export function uebernimmWege( |
| 656 | project: Project, |
| 657 | lageplan: Lageplan, |
| 658 | ableitung: Ableitung, |
| 659 | gewaehlt: readonly string[], |
| 660 | now: Date = new Date(), |
| 661 | ): { project: Project; uebernommen: number } { |
| 662 | const schluessel = new Set(gewaehlt); |
| 663 | // Mit der Anlagenart des Projekts (Fassung 5.4.0, Befund C10) - |
| 664 | // derselbe Kennwertsatz, mit dem auch der Signalzeitenplan rechnet. |
| 665 | const defaults = resolveRilsaSettings( |
| 666 | project.settings.rilsa, |
| 667 | undefined, |
| 668 | project.anlagenart, |
| 669 | ).defaults; |
| 670 | |
| 671 | /** |
| 672 | * Verdichtung auf Signalgruppenpaare. |
| 673 | * |
| 674 | * Eine Signalgruppe kann mehrere Stroeme bedienen - geradeaus und rechts aus |
| 675 | * einem Fahrstreifen etwa. Dann fallen fuer EIN Gruppenpaar mehrere |
| 676 | * Strompaare an, und es gilt das unguenstigste von ihnen: Die Gruppe erbt die |
| 677 | * groesste Zwischenzeit, die einer ihrer Stroeme verlangt. |
| 678 | * |
| 679 | * Massgebend ist die groesste ZWISCHENZEIT, nicht der laengste Raeumweg. |
| 680 | * Raeum- und Einfahrgeschwindigkeit haengen an Verkehrsart und Fahrbeziehung |
| 681 | * des einzelnen Stroms; ein kurzer Weg eines Abbiegers kann laenger dauern |
| 682 | * als ein langer Weg geradeaus. Deshalb wird je Paar gerechnet und verglichen. |
| 683 | * |
| 684 | * UEBERNOMMEN WIRD DIE FAHRBEZIEHUNG MIT (Fassung 5.11.0). Bis dahin erbte |
| 685 | * die Gruppe nur die GEOMETRIE des Siegerpaares; der Planaufbau rechnete die |
| 686 | * Beziehung danach mit der Fahrbeziehung der GRUPPE neu (signalPlan.ts, |
| 687 | * `raeumenderStrom`). Wo der Rechtsabbieger den Ausschlag gab, raeumte der |
| 688 | * Plan dessen Bogen mit 10,0 statt 7,0 m/s - die Zusage zwei Absaetze weiter |
| 689 | * oben war gebrochen, und die Bemerkung nannte neben 7 s gedruckter |
| 690 | * Zwischenzeit die massgebenden 9 s. Deshalb schreibt die Uebernahme jetzt |
| 691 | * auch `raeumbeziehung`, sobald das Siegerpaar eine andere Fahrbeziehung hat |
| 692 | * als seine Gruppe - und entfernt das Feld, sobald es dieselbe hat. |
| 693 | * |
| 694 | * Ohne diese Verdichtung schriebe schlicht das zuletzt behandelte Strompaar |
| 695 | * den Wert - welches das ist, haengt an der Reihenfolge der Linien. |
| 696 | * |
| 697 | * DAS PAAR WIRD VERSCHACHTELT GEFUEHRT, nicht als zusammengeklebte |
| 698 | * Zeichenkette (Fassung 5.10.0) - dieselbe Form wie in `parseConflicts`. Hier |
| 699 | * stand `${fromId}|${toId}`, und der Schluessel wurde weiter unten mit |
| 700 | * `split('|')` wieder zerlegt. Eine Signalgruppenkennung kommt ungeprueft aus |
| 701 | * der Datei (nur leer und doppelt wird beanstandet, kein Zeichenrepertoire), |
| 702 | * und mit einem "|" darin ging beides kaputt: Die Paare ("a|b", "c") und |
| 703 | * ("a", "b|c") ergaben denselben Schluessel "a|b|c" - eine der beiden |
| 704 | * Uebernahmen verschwand still -, und das Zerlegen lieferte fuer "sg|a|sg-b" |
| 705 | * die Beziehung "sg" -> "a", also zwei Kennungen, die es im Projekt gar nicht |
| 706 | * gibt. Die gemeinte Beziehung blieb ohne die vermessenen Wege stehen; ihre |
| 707 | * Zwischenzeit haengt dann weiter am alten Raeumweg - die gefaehrliche |
| 708 | * Richtung, wenn der kuerzer ist. |
| 709 | * |
| 710 | * Verschachtelt braucht es kein Zerlegen mehr: Beide Kennungen stehen als |
| 711 | * Schluessel der aeusseren und der inneren Karte. |
| 712 | */ |
| 713 | /** |
| 714 | * Das Siegerpaar eines Gruppenpaares. |
| 715 | * |
| 716 | * `raeumbeziehung` steht nur da, wo die Fahrbeziehung des raeumenden STROMS |
| 717 | * eine andere ist als die seiner Gruppe; sonst `undefined`, und die |
| 718 | * Beziehung fuehrt das Feld gar nicht (siehe `Conflict.raeumbeziehung`). |
| 719 | */ |
| 720 | interface Siegerpaar { |
| 721 | readonly weg: WegeVorschlag; |
| 722 | readonly zwischenzeit: Seconds; |
| 723 | /** |
| 724 | * Das ungerundete tue + tr - te desselben Paares. |
| 725 | * |
| 726 | * ES ENTSCHEIDET DEN GLEICHSTAND (Fassung 5.24.0). Verglichen wird die |
| 727 | * aufgerundete Zwischenzeit, und die trifft haeufig zusammen: Am |
| 728 | * mitgelieferten Beispielknotenpunkt liegen fuer dasselbe Gruppenpaar |
| 729 | * 4,100 s und 4,591 s nebeneinander, beide 5 s (bis 5.42.1 4,013 s, als |
| 730 | * der einfahrende Fussgaenger noch 0,13 m Einfahrweg hatte). Bis 5.23.0 |
| 731 | * gewann der zuerst gezeichnete Strom - also die Reihenfolge der Linien im Lageplan, |
| 732 | * die ueber die Sache nichts aussagt. Uebernommen wurde damit auch die |
| 733 | * GEOMETRIE des Unterlegenen, und die bleibt stehen: Aendert sich spaeter |
| 734 | * die Gelbzeit, die zulaessige Hoechstgeschwindigkeit oder der |
| 735 | * Ueberfahrzeit-Ansatz, laufen die beiden auseinander, und der Plan |
| 736 | * rechnet mit dem schwaecheren Ansatz weiter. Der ungerundete Wert |
| 737 | * unterscheidet die beiden und ist keine zweite Entscheidung, sondern |
| 738 | * dieselbe eine Stelle genauer gelesen. |
| 739 | */ |
| 740 | readonly roh: Seconds; |
| 741 | readonly raeumbeziehung: Movement | undefined; |
| 742 | } |
| 743 | |
| 744 | const beste = new Map<string, Map<string, Siegerpaar>>(); |
| 745 | |
| 746 | for (const weg of ableitung.wege) { |
| 747 | if (!schluessel.has(wegSchluessel(weg))) continue; |
| 748 | |
| 749 | const von = planlinieVon(lageplan, weg.vonLinieId); |
| 750 | const nach = planlinieVon(lageplan, weg.nachLinieId); |
| 751 | if (!von?.signalGroupId || !nach?.signalGroupId) continue; |
| 752 | |
| 753 | // Letzte Sperre vor dem Schreiben: Eine Signalgruppe kann nicht mit sich |
| 754 | // selbst in Konflikt stehen. Bisher fing das erst das Wiedereinlesen ab - |
| 755 | // der Fehler war im Arbeitsstand vorhanden und nach dem Speichern weg. |
| 756 | if (von.signalGroupId === nach.signalGroupId) continue; |
| 757 | |
| 758 | const gruppeVon = project.signalGroups.find((g) => g.id === von.signalGroupId); |
| 759 | const gruppeNach = project.signalGroups.find((g) => g.id === nach.signalGroupId); |
| 760 | if (!gruppeVon || !gruppeNach) continue; |
| 761 | |
| 762 | /* |
| 763 | * DIE BEIDEN RECHENANSAETZE DES PROJEKTS GEHOEREN IN DIESEN VERGLEICH. |
| 764 | * |
| 765 | * Hier wird entschieden, WELCHER Weg des Gruppenpaares massgebend ist und |
| 766 | * damit gespeichert wird. Die Entscheidung fiel zuvor nach |
| 767 | * einer anderen Rechnung als der des Plans: ohne den gewaehlten |
| 768 | * Ueberfahrzeit-Ansatz und ohne das Merkmal "enger Innenradius" der bereits |
| 769 | * vorhandenen Beziehung. Nachgerechnet: zwei Wege desselben Paares, |
| 770 | * geradeaus s0 = 30 m und abbiegend s0 = 20 m, je te = 0 - mit 'gelbzeit' |
| 771 | * ist der abbiegende Weg massgebend (6,714 s gegen 6,600 s), mit 'fest' der |
| 772 | * geradeausfahrende (6,600 s gegen 5,714 s). Uebernommen wurde in beiden |
| 773 | * Faellen s0 = 20 m, und der Plan druckte danach 6 s statt 7 s. |
| 774 | * |
| 775 | * Das Merkmal kommt aus der VORHANDENEN Beziehung: Eine neu angelegte |
| 776 | * fuehrt es nicht (createConflict laesst das Feld weg), und eine bestehende |
| 777 | * behaelt es - der Vergleich muss deshalb mit dem Merkmal rechnen, das |
| 778 | * hinterher auch gilt. |
| 779 | */ |
| 780 | const bestehenderKonflikt = project.conflicts.find( |
| 781 | (c) => c.fromId === von.signalGroupId && c.toId === nach.signalGroupId, |
| 782 | ); |
| 783 | |
| 784 | const ergebnisPaar = computeIntergreen( |
| 785 | { |
| 786 | mode: von.mode, |
| 787 | movement: von.movement, |
| 788 | vehicleClass: gruppeVon.vehicleClass, |
| 789 | clearingDistance: weg.raeumweg, |
| 790 | vZul: gruppeVon.vZul, |
| 791 | reducedMobility: gruppeVon.reducedMobility, |
| 792 | ueberfahrzeitAnsatz: project.settings.ueberfahrzeitAnsatz, |
| 793 | ...(bestehenderKonflikt?.engerRadius === true ? { engerRadius: true } : {}), |
| 794 | ...(bestehenderKonflikt?.haltVorKnoten === true ? { haltVorKnoten: true } : {}), |
| 795 | }, |
| 796 | { |
| 797 | mode: nach.mode, |
| 798 | movement: nach.movement, |
| 799 | enteringDistance: weg.einfahrweg, |
| 800 | }, |
| 801 | defaults, |
| 802 | ); |
| 803 | const zwischenzeit = ergebnisPaar.intergreen; |
| 804 | const roh = ergebnisPaar.raw; |
| 805 | |
| 806 | let zuVon = beste.get(von.signalGroupId); |
| 807 | if (zuVon === undefined) { |
| 808 | zuVon = new Map<string, Siegerpaar>(); |
| 809 | beste.set(von.signalGroupId, zuVon); |
| 810 | } |
| 811 | const bisher = zuVon.get(nach.signalGroupId); |
| 812 | /* |
| 813 | * Erst die aufgerundete Zwischenzeit - sie ist die Zahl, die im Plan |
| 814 | * steht -, bei Gleichstand der ungerundete Wert. Siehe `Siegerpaar.roh`: |
| 815 | * Bis 5.23.0 stand hier nur der erste Vergleich, und was gleich aussah, |
| 816 | * entschied die Zeichenreihenfolge. |
| 817 | */ |
| 818 | const besser = |
| 819 | bisher === undefined || |
| 820 | zwischenzeit > bisher.zwischenzeit || |
| 821 | (zwischenzeit === bisher.zwischenzeit && roh > bisher.roh); |
| 822 | if (besser) { |
| 823 | zuVon.set(nach.signalGroupId, { |
| 824 | weg, |
| 825 | zwischenzeit, |
| 826 | roh, |
| 827 | // Mit derselben Fahrbeziehung, mit der die Zwischenzeit oben gerechnet |
| 828 | // wurde - sonst rechnet der Plan aus demselben Weg eine andere Zahl. |
| 829 | raeumbeziehung: von.movement === gruppeVon.movement ? undefined : von.movement, |
| 830 | }); |
| 831 | } |
| 832 | } |
| 833 | |
| 834 | let ergebnis = project; |
| 835 | let uebernommen = 0; |
| 836 | const nachweis = wegnachweisAusLageplan(now); |
| 837 | |
| 838 | for (const [fromId, zuVon] of beste) { |
| 839 | for (const [toId, eintrag] of zuVon) { |
| 840 | uebernommen += 1; |
| 841 | |
| 842 | /** |
| 843 | * Die Bemerkung haelt fest, WELCHES Strompaar den Ausschlag gab - eine |
| 844 | * Angabe, die sonst nirgends steht und die man braucht, um die Zahl im |
| 845 | * Lageplan wiederzufinden. |
| 846 | * |
| 847 | * Sie ist bewusst als Vermerk ueber den Vorgang formuliert und nicht mehr |
| 848 | * als Aussage ueber den heutigen Wert ("Aus dem Lageplan vermessen"): Ein |
| 849 | * Satz in der Gegenwartsform bliebe stehen, wenn der Raeumweg danach von |
| 850 | * Hand geaendert wird, und widerspraeche dann dem Herkunftsnachweis, der |
| 851 | * gleich daneben im Ausdruck steht. Woher die Wege stammen, sagt allein |
| 852 | * `wegnachweis`. |
| 853 | */ |
| 854 | const note = |
| 855 | `Bei der Übernahme aus dem Lageplan maßgebend: "${eintrag.weg.vonName}" räumt vor ` + |
| 856 | `"${eintrag.weg.nachName}" (${eintrag.zwischenzeit} s).`; |
| 857 | const vorhanden = ergebnis.conflicts.find((c) => c.fromId === fromId && c.toId === toId); |
| 858 | |
| 859 | if (vorhanden) { |
| 860 | ergebnis = { |
| 861 | ...ergebnis, |
| 862 | conflicts: ergebnis.conflicts.map((c) => |
| 863 | // Geschrieben wird ueber DASSELBE Paar, ueber das oben gesucht |
| 864 | // wurde - nicht ueber `vorhanden.id`. Traegt eine zweite Beziehung |
| 865 | // dieselbe Kennung, bekaeme sie sonst Raeumweg, Einfahrweg und |
| 866 | // Bemerkung dieser hier mit, obwohl sie zu ganz anderen |
| 867 | // Signalgruppen gehoert und niemand sie gewaehlt hat. Ist der |
| 868 | // fremde Raeumweg kuerzer als der wahre, faellt ihre Zwischenzeit |
| 869 | // zu kurz aus - die gefaehrliche Richtung. Die Neuvergabe der |
| 870 | // Kennungen beim Einlesen (schema.ts) entschaerft nur Daten, die |
| 871 | // durch parseProject laufen; hier haengt das Verhalten nun an gar |
| 872 | // keiner Eindeutigkeit mehr. |
| 873 | c.fromId === fromId && c.toId === toId |
| 874 | ? mitRaeumbeziehung( |
| 875 | { |
| 876 | ...c, |
| 877 | clearingDistance: eintrag.weg.raeumweg, |
| 878 | enteringDistance: eintrag.weg.einfahrweg, |
| 879 | // Die Wege stammen jetzt aus dem Lageplan - die Zwischenzeit |
| 880 | // aber nur dann, wenn nicht daneben eine Geschwindigkeit oder |
| 881 | // die Zwischenzeit selbst von Hand vorgegeben ist. Diese |
| 882 | // Vorgaben werden nicht angetastet: Sie zu loeschen hiesse, |
| 883 | // eine bewusste Entscheidung stillschweigend zu verwerfen. |
| 884 | wegnachweis: hatHandvorgaben(c) ? WEGNACHWEIS_HANDEINGABE : nachweis, |
| 885 | note, |
| 886 | }, |
| 887 | eintrag.raeumbeziehung, |
| 888 | ) |
| 889 | : c, |
| 890 | ), |
| 891 | }; |
| 892 | } else { |
| 893 | ergebnis = { |
| 894 | ...ergebnis, |
| 895 | conflicts: [ |
| 896 | ...ergebnis.conflicts, |
| 897 | { |
| 898 | id: createId('cf'), |
| 899 | fromId, |
| 900 | toId, |
| 901 | clearingDistance: eintrag.weg.raeumweg, |
| 902 | enteringDistance: eintrag.weg.einfahrweg, |
| 903 | clearingSpeedOverride: null, |
| 904 | enteringSpeedOverride: null, |
| 905 | crossingTimeOverride: null, |
| 906 | manualIntergreen: null, |
| 907 | // Nur, wenn der Sieger eine andere Fahrbeziehung hat als seine |
| 908 | // Gruppe - eine neue Beziehung fuehrt das Feld sonst nicht. |
| 909 | ...(eintrag.raeumbeziehung === undefined |
| 910 | ? {} |
| 911 | : { raeumbeziehung: eintrag.raeumbeziehung }), |
| 912 | wegnachweis: nachweis, |
| 913 | note, |
| 914 | }, |
| 915 | ], |
| 916 | }; |
| 917 | } |
| 918 | } |
| 919 | } |
| 920 | |
| 921 | return { project: ergebnis, uebernommen }; |
| 922 | } |
| 923 | |
| 924 | /** |
| 925 | * Setzt die Fahrbeziehung des massgebenden Raeumstroms an einer Beziehung - |
| 926 | * oder ENTFERNT sie. |
| 927 | * |
| 928 | * Das Entfernen ist der Grund, aus dem es diese Funktion gibt: Ein blosses |
| 929 | * `{ ...c, raeumbeziehung }` liesse eine aeltere Angabe stehen, wenn diesmal |
| 930 | * ein Strom mit der Fahrbeziehung seiner Gruppe gewonnen hat. Der Plan raeumte |
| 931 | * den neuen Weg dann mit der Geschwindigkeit des alten Siegers - beim Wechsel |
| 932 | * von 'rechts' auf 'geradeaus' zu langsam, beim umgekehrten Wechsel zu |
| 933 | * schnell, und das ist die gefaehrliche Richtung. |
| 934 | * |
| 935 | * `exactOptionalPropertyTypes` verbietet den kuerzeren Weg ueber ein |
| 936 | * ausdrueckliches `undefined`; das Feld wird deshalb aus der Kopie |
| 937 | * herausgelassen. |
| 938 | */ |
| 939 | function mitRaeumbeziehung(conflict: Conflict, raeumbeziehung: Movement | undefined): Conflict { |
| 940 | const { raeumbeziehung: _bisher, ...ohne } = conflict; |
| 941 | return raeumbeziehung === undefined ? ohne : { ...ohne, raeumbeziehung }; |
| 942 | } |
| 943 | |
| 944 | /** |
| 945 | * Eindeutiger Schluessel einer gerichteten Beziehung im Vorschlag. |
| 946 | * |
| 947 | * EINDEUTIG AUCH BEI EINER KENNUNG MIT DEM TRENNZEICHEN (Fassung 5.11.0): Hier |
| 948 | * stand `${vonLinieId}->${nachLinieId}`. Eine PLANLINIENKENNUNG kommt noch |
| 949 | * ungeprueter aus der Datei als eine Signalgruppenkennung - `parseLageplan` |
| 950 | * vergibt nur die leere und die doppelte neu und prueft kein |
| 951 | * Zeichenrepertoire -, und mit "->" darin fielen die Paare ("a->b", "c") und |
| 952 | * ("a", "b->c") auf denselben Schluessel "a->b->c". |
| 953 | * |
| 954 | * Der Schaden lag nicht in der Ableitung, sondern in der Uebernahme: Die |
| 955 | * Auswahl des Anwenders geht als Menge dieser Schluessel durch `uebernimmWege` |
| 956 | * (Zeile mit `schluessel.has(...)`). Bei einer Kollision wurde ein zweites, |
| 957 | * NICHT angehaktes Strompaar mitgeschrieben - sein von Hand eingetragener, |
| 958 | * bewusst laengerer Raeumweg fiel auf den gemessenen, die Zwischenzeit wurde |
| 959 | * kuerzer (die gefaehrliche Richtung), und die Beziehung trug danach den |
| 960 | * Herkunftsnachweis "aus dem Lageplan", obwohl niemand sie ausgewaehlt hatte. |
| 961 | * Umgekehrt blieb eine angehakte Beziehung ungeschrieben. |
| 962 | * |
| 963 | * Genommen wird dafuer `intergreenKey` - dieselbe Maskierung, mit der |
| 964 | * `signalPlan.ts` und `validation/engine.ts` seit Befund 61 ihre Paarschluessel |
| 965 | * bilden. Der Trennstrich ist danach der einzige im Schluessel, und die |
| 966 | * Zuordnung ist umkehrbar. Der Schluessel ist ein reines Auswahlmerkmal: Er |
| 967 | * wird nirgends zerlegt, sondern nur verglichen. |
| 968 | */ |
| 969 | export function wegSchluessel(weg: Pick<WegeVorschlag, 'vonLinieId' | 'nachLinieId'>): string { |
| 970 | return intergreenKey(weg.vonLinieId, weg.nachLinieId); |
| 971 | } |
| 972 | |
| 973 | /** Warum ein fuer sich vertraegliches Strompaar seine Beziehung nicht aufhebt. */ |
| 974 | export type Aufhebungssperre = 'kreuzender-strom' | 'unfertige-linie'; |
| 975 | |
| 976 | /** |
| 977 | * Anwendertext je Sperre. |
| 978 | * |
| 979 | * Er entsteht hier und nicht in der Ansicht: Ob eine Beziehung stehen bleibt, |
| 980 | * ist eine fachliche Entscheidung, und ihr Grund gehoert zu ihr - wie die |
| 981 | * `begruendung` einer Beurteilung weiter oben. |
| 982 | */ |
| 983 | const AUFHEBUNGSSPERRE_GRUND: Readonly<Record<Aufhebungssperre, string>> = { |
| 984 | 'kreuzender-strom': |
| 985 | 'ein anderer Strom derselben Signalgruppen kreuzt – die Beziehung bleibt bestehen', |
| 986 | 'unfertige-linie': 'ein Strom derselben Signalgruppen ist noch nicht fertig gezeichnet', |
| 987 | }; |
| 988 | |
| 989 | /** Ein Strompaar, das vertraeglich und erfasst ist, dessen Beziehung aber bleibt. */ |
| 990 | export interface GesperrtesPaar { |
| 991 | readonly paar: VertraeglichkeitVorschlag; |
| 992 | readonly sperre: Aufhebungssperre; |
| 993 | readonly begruendung: string; |
| 994 | } |
| 995 | |
| 996 | /** Ein Signalgruppenpaar, dessen erfasste Beziehung der Plan nicht mehr traegt. */ |
| 997 | export interface AufhebbaresGruppenpaar { |
| 998 | readonly ersteId: string; |
| 999 | readonly zweiteId: string; |
| 1000 | /** Die vertraeglichen, im Projekt erfassten Strompaare dieses Gruppenpaares. */ |
| 1001 | readonly paare: readonly VertraeglichkeitVorschlag[]; |
| 1002 | } |
| 1003 | |
| 1004 | export interface Aufhebbarkeit { |
| 1005 | readonly aufhebbar: readonly AufhebbaresGruppenpaar[]; |
| 1006 | readonly gesperrt: readonly GesperrtesPaar[]; |
| 1007 | } |
| 1008 | |
| 1009 | /** |
| 1010 | * VERDICHTUNG AUF SIGNALGRUPPENPAARE - wie in uebernimmWege. |
| 1011 | * |
| 1012 | * Die Vorschlaege stehen je STROMPAAR, der Konflikt steht je |
| 1013 | * SIGNALGRUPPENPAAR. Eine Signalgruppe darf mehrere Stroeme fuehren - ein |
| 1014 | * Signalgeber fuer geradeaus und rechts ist der Regelfall -, und dann gehoeren |
| 1015 | * zu einem Gruppenpaar mehrere Strompaare. Massgebend ist das unguenstigste: |
| 1016 | * Ein Gruppenpaar ist feindlich, sobald EIN Strompaar feindlich ist. |
| 1017 | * Aufgehoben wird deshalb nur, wenn ALLE Strompaare dieses Gruppenpaares |
| 1018 | * vertraeglich sind. |
| 1019 | * |
| 1020 | * Ohne diese Verdichtung riss ein vertraegliches Strompaar (geradeaus gegen |
| 1021 | * eine Furt) die Beziehung der beiden Gruppen mit - und mit ihr die |
| 1022 | * Zwischenzeit des feindlichen Strompaares derselben Gruppen (Rechtsabbieger |
| 1023 | * gegen dieselbe Furt). Zwischen Stroemen einer Gruppe gibt es keine |
| 1024 | * Zwischenzeit; der Konflikt waere danach ungesichert, und niemand saehe es. |
| 1025 | * |
| 1026 | * "ALLE Strompaare" heisst wirklich alle - auch die, ueber die der Plan noch |
| 1027 | * nichts sagt. Eine Fahrlinie mit weniger als zwei Punkten beschreibt keinen |
| 1028 | * Weg; leiteAb nimmt sie von der Auswertung aus und meldet "linie-unfertig". |
| 1029 | * Zu ihr steht kein Strompaar in den Vorschlaegen, und ohne die Sperre unten |
| 1030 | * saehe ein Gruppenpaar allein deshalb vertraeglich aus, weil der halb |
| 1031 | * gezeichnete Strom fehlt. Gruppen mit einer unfertigen Linie sind deshalb vom |
| 1032 | * Aufheben ausgenommen, bis die Linie zu Ende gezeichnet ist. |
| 1033 | * |
| 1034 | * DIESE FUNKTION IST DIE EINZIGE BESCHREIBUNG DIESER ENTSCHEIDUNG. Sie wird |
| 1035 | * ausgewiesen, weil nicht nur `hebeVertraeglicheAuf` sie braucht, sondern auch |
| 1036 | * die Ansicht: Der Warnbalken "n erfasste Konflikte kreuzen sich im Plan nicht" |
| 1037 | * und der Bestaetigungstext filterten zuvor selbst je Linienpaar. Sie zaehlten |
| 1038 | * damit Paare, die gar nicht aufgehoben wurden, kuendigten sie im Fenster an |
| 1039 | * und meldeten danach "0 Konflikt(e) aufgehoben" - der Balken blieb stehen und |
| 1040 | * war ueber diesen Weg nie abzutragen. |
| 1041 | * |
| 1042 | * `gesperrt` fuehrt die uebrigen Paare mit ihrem Grund: Ein vertraegliches |
| 1043 | * Strompaar, dessen Beziehung stehen bleibt, ist kein Versehen, sondern ein |
| 1044 | * Ergebnis - und der Anwender muss es nachlesen koennen, ohne in den Quelltext |
| 1045 | * zu sehen. |
| 1046 | */ |
| 1047 | export function pruefeAufhebbarkeit(lageplan: Lageplan, ableitung: Ableitung): Aufhebbarkeit { |
| 1048 | interface Sammlung { |
| 1049 | readonly ersteId: string; |
| 1050 | readonly zweiteId: string; |
| 1051 | /** Ist irgendein Strompaar dieser beiden Gruppen feindlich? */ |
| 1052 | feindlich: boolean; |
| 1053 | /** Die vertraeglichen Strompaare, deren Beziehung im Projekt erfasst ist. */ |
| 1054 | readonly erfasstVertraeglich: VertraeglichkeitVorschlag[]; |
| 1055 | } |
| 1056 | |
| 1057 | // Gruppen, zu denen eine nicht auswertbare Fahrlinie gehoert. Zu ihnen fehlen |
| 1058 | // Strompaare in den Vorschlaegen; ueber sie laesst sich nichts aussagen. |
| 1059 | const unfertigeGruppen = new Set<string>(); |
| 1060 | for (const linie of lageplan.linien) { |
| 1061 | if (linie.punkte.length < 2 && linie.signalGroupId !== null) { |
| 1062 | unfertigeGruppen.add(linie.signalGroupId); |
| 1063 | } |
| 1064 | } |
| 1065 | |
| 1066 | /* |
| 1067 | * VERSCHACHTELT, nicht als zusammengeklebte Zeichenkette (Fassung 5.10.0) - |
| 1068 | * dieselbe Form wie in `parseConflicts` und in `uebernimmWege`. Hier stand |
| 1069 | * `${ersteId}|${zweiteId}`. Mit einem "|" in einer Signalgruppenkennung |
| 1070 | * fielen die Gruppenpaare ("a|b", "c") und ("a", "b|c") auf denselben |
| 1071 | * Schluessel: Ein feindliches Strompaar des einen sperrte die Beziehung des |
| 1072 | * anderen mit einer Begruendung, die zu fremden Signalgruppen gehoert, und |
| 1073 | * `aufhebbar` wies das zuerst eingetragene Gruppenpaar aus - das zweite |
| 1074 | * verschwand samt seinen Strompaaren. Umgekehrt entging dem `unfertig` eine |
| 1075 | * unfertige Linie des verdeckten Paares, und eine Beziehung liess sich |
| 1076 | * aufheben, obwohl ueber sie noch nichts feststeht. |
| 1077 | */ |
| 1078 | const gruppenpaare = new Map<string, Map<string, Sammlung>>(); |
| 1079 | |
| 1080 | for (const paar of ableitung.vertraeglichkeit) { |
| 1081 | const a = planlinieVon(lageplan, paar.aLinieId); |
| 1082 | const b = planlinieVon(lageplan, paar.bLinieId); |
| 1083 | if (!a?.signalGroupId || !b?.signalGroupId) continue; |
| 1084 | // Zwei Stroeme derselben Gruppe schalten stets gleich. Zwischen ihnen gibt |
| 1085 | // es keine Beziehung, die sich aufheben liesse. |
| 1086 | if (a.signalGroupId === b.signalGroupId) continue; |
| 1087 | |
| 1088 | // Der Schluessel ist ungerichtet: Beide Fahrtrichtungen der Beziehung |
| 1089 | // gehoeren zu demselben Gruppenpaar und werden zusammen beurteilt. |
| 1090 | const ersteId = a.signalGroupId < b.signalGroupId ? a.signalGroupId : b.signalGroupId; |
| 1091 | const zweiteId = a.signalGroupId < b.signalGroupId ? b.signalGroupId : a.signalGroupId; |
| 1092 | |
| 1093 | let zuErster = gruppenpaare.get(ersteId); |
| 1094 | if (zuErster === undefined) { |
| 1095 | zuErster = new Map<string, Sammlung>(); |
| 1096 | gruppenpaare.set(ersteId, zuErster); |
| 1097 | } |
| 1098 | let eintrag = zuErster.get(zweiteId); |
| 1099 | if (eintrag === undefined) { |
| 1100 | eintrag = { ersteId, zweiteId, feindlich: false, erfasstVertraeglich: [] }; |
| 1101 | zuErster.set(zweiteId, eintrag); |
| 1102 | } |
| 1103 | |
| 1104 | if (paar.feindlich) eintrag.feindlich = true; |
| 1105 | else if (paar.imProjekt === true) eintrag.erfasstVertraeglich.push(paar); |
| 1106 | } |
| 1107 | |
| 1108 | const aufhebbar: AufhebbaresGruppenpaar[] = []; |
| 1109 | const gesperrt: GesperrtesPaar[] = []; |
| 1110 | |
| 1111 | for (const zuErster of gruppenpaare.values()) { |
| 1112 | for (const eintrag of zuErster.values()) { |
| 1113 | // Nichts erfasst, nichts aufzuheben - das Gruppenpaar taucht in keiner der |
| 1114 | // beiden Listen auf. |
| 1115 | if (eintrag.erfasstVertraeglich.length === 0) continue; |
| 1116 | |
| 1117 | // Ein ungeprueftes Strompaar wiegt wie ein feindliches: Solange eine der |
| 1118 | // beiden Gruppen eine unfertige Linie fuehrt, bleibt die Beziehung stehen. |
| 1119 | const unfertig = |
| 1120 | unfertigeGruppen.has(eintrag.ersteId) || unfertigeGruppen.has(eintrag.zweiteId); |
| 1121 | |
| 1122 | if (!eintrag.feindlich && !unfertig) { |
| 1123 | aufhebbar.push({ |
| 1124 | ersteId: eintrag.ersteId, |
| 1125 | zweiteId: eintrag.zweiteId, |
| 1126 | paare: eintrag.erfasstVertraeglich, |
| 1127 | }); |
| 1128 | continue; |
| 1129 | } |
| 1130 | |
| 1131 | // Der kreuzende Strom ist die belegte Aussage und geht dem noch |
| 1132 | // ungezeichneten vor: Er bleibt der Grund, auch wenn daneben eine Linie |
| 1133 | // unfertig ist. |
| 1134 | const sperre: Aufhebungssperre = eintrag.feindlich ? 'kreuzender-strom' : 'unfertige-linie'; |
| 1135 | for (const paar of eintrag.erfasstVertraeglich) { |
| 1136 | gesperrt.push({ paar, sperre, begruendung: AUFHEBUNGSSPERRE_GRUND[sperre] }); |
| 1137 | } |
| 1138 | } |
| 1139 | } |
| 1140 | |
| 1141 | return { aufhebbar, gesperrt }; |
| 1142 | } |
| 1143 | |
| 1144 | /** |
| 1145 | * Hebt Konflikte auf, die der Lageplan als vertraeglich ausweist. |
| 1146 | * Getrennte Funktion, weil das Aufheben eines Konflikts sicherheitsrelevant ist |
| 1147 | * und deshalb einzeln bestaetigt werden muss. |
| 1148 | * |
| 1149 | * WAS aufgehoben wird, entscheidet `pruefeAufhebbarkeit` - dieselbe Funktion, |
| 1150 | * aus der die Ansicht ihren Warnbalken und ihren Bestaetigungstext bildet. |
| 1151 | */ |
| 1152 | export function hebeVertraeglicheAuf( |
| 1153 | project: Project, |
| 1154 | lageplan: Lageplan, |
| 1155 | ableitung: Ableitung, |
| 1156 | ): { project: Project; aufgehoben: number } { |
| 1157 | let ergebnis = project; |
| 1158 | let anzahl = 0; |
| 1159 | |
| 1160 | for (const gruppenpaar of pruefeAufhebbarkeit(lageplan, ableitung).aufhebbar) { |
| 1161 | const vorher = ergebnis.conflicts.length; |
| 1162 | ergebnis = { |
| 1163 | ...ergebnis, |
| 1164 | conflicts: ergebnis.conflicts.filter( |
| 1165 | (c) => |
| 1166 | !( |
| 1167 | (c.fromId === gruppenpaar.ersteId && c.toId === gruppenpaar.zweiteId) || |
| 1168 | (c.fromId === gruppenpaar.zweiteId && c.toId === gruppenpaar.ersteId) |
| 1169 | ), |
| 1170 | ), |
| 1171 | }; |
| 1172 | if (ergebnis.conflicts.length !== vorher) anzahl += 1; |
| 1173 | } |
| 1174 | |
| 1175 | return { project: ergebnis, aufgehoben: anzahl }; |
| 1176 | } |