lsa-planer

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

/ src domain geometrie ableitung.ts

45,4 KB Rohdatei
src/domain/geometrie/ableitung.ts — 1176 Zeilen
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 }