import { roundTo } from '../units'; import { intergreenKey, type SignalPlan } from './signalPlan'; import { programmanzahl } from '../model/programme'; import type { Project } from '../model/project'; import type { Seconds } from '../units'; /** * Zwei Planfaelle nebeneinander - Bestand gegen Entwurf, Entwurf gegen Entwurf. * * WOZU DAS DA IST, UND ES IST NICHT DIE BEQUEMLICHKEIT: Wer heute einen * Raeumweg berichtigt, eine Fahrbeziehung aendert oder einen Kennwert setzt, * bekommt einen neu gerechneten Plan und sieht den Endstand. Welche * Zwischenzeit dabei KUERZER geworden ist, sagt ihm niemand - und das ist die * gefaehrliche Richtung. Dieser Vergleich weist die Verkuerzungen eigens aus, * gezaehlt und einzeln benannt. * * ER RECHNET NICHTS NEU. Beide Seiten kommen als fertige Plaene herein; diese * Datei stellt sie gegenueber. Damit kann kein Vergleich entstehen, den das * Programm nicht auch anzeigen wuerde. * * ER AENDERT NICHTS. Es gibt keine Funktion, die einen Wert von der einen auf * die andere Seite uebernimmt - weder hier noch in der Ansicht. Ein Knopf * "uebernehmen" wuerde eine Zwischenzeit aus einem fremden Zusammenhang in * diese Planung tragen, ohne dass ihre Wege dazu passen muessten. */ /** Woher die Zuordnung zweier Signalgruppen stammt. */ export type Zuordnungsart = 'kennung' | 'name' | 'nur-links' | 'nur-rechts'; /** In welche Richtung sich eine Zeit geaendert hat. */ export type Aenderungsrichtung = 'kuerzer' | 'laenger' | 'gleich' | 'unbestimmt'; export interface Zeitvergleich { readonly links: Seconds | null; readonly rechts: Seconds | null; /** rechts minus links; null, wo eine Seite fehlt. */ readonly differenz: Seconds | null; readonly richtung: Aenderungsrichtung; } export interface Gruppenvergleich { readonly name: string; readonly art: Zuordnungsart; readonly freigabezeit: Zeitvergleich; readonly mindestfreigabezeit: Zeitvergleich; } export interface Zwischenzeitvergleich { readonly von: string; readonly nach: string; readonly art: Zuordnungsart; readonly zeit: Zeitvergleich; } export interface Variantenvergleich { /** Bezeichnung der beiden Planfaelle, wie sie in der Ansicht stehen. */ readonly namen: { readonly links: string; readonly rechts: string }; /** Verglichen wird je Seite das AKTIVE Tagesprogramm; hier steht, welches. */ readonly programme: { readonly links: string; readonly rechts: string }; readonly umlaufzeit: Zeitvergleich; readonly gruppen: readonly Gruppenvergleich[]; readonly zwischenzeiten: readonly Zwischenzeitvergleich[]; /** * Wie viele Zwischenzeiten rechts KUERZER sind als links. * * Die eine Zahl, um derentwillen es diese Ansicht gibt. Sie steht oben und * nicht am Ende einer Tabelle. */ readonly verkuerzteZwischenzeiten: number; /** Wie viele Signalgruppen oder Beziehungen nur auf einer Seite vorkommen. */ readonly unpaarig: number; /** * Was den Vergleich einschraenkt - Migration, mehrere Programme, Zuordnung * ueber den Namen. Jeder Satz ist ein Vorbehalt gegen die Zahlen daneben. */ readonly vorbehalte: readonly string[]; } /** Die Seite eines Vergleichs: Projekt, Plan und wie sie heisst. */ export interface Vergleichsseite { readonly name: string; readonly project: Project; readonly plan: SignalPlan; /** * Wurde dieses Projekt beim Einlesen aus einer aelteren Schemafassung * hochgezogen? Dann sind seine Zwischenzeiten NEU GERECHNET und nicht die * der Datei - ein Unterschied kann von der Migration kommen statt von der * Planung. */ readonly migriert?: boolean; /** Schemafassung der Quelldatei, wo bekannt. */ readonly herkunftsfassung?: string; } function zeit(links: Seconds | null, rechts: Seconds | null): Zeitvergleich { if (links === null || rechts === null) { return { links, rechts, differenz: null, richtung: 'unbestimmt' }; } const differenz = roundTo(rechts - links, 3); return { links, rechts, differenz, richtung: differenz < 0 ? 'kuerzer' : differenz > 0 ? 'laenger' : 'gleich', }; } /** * Zuordnung der Signalgruppen beider Seiten. * * ZUERST UEBER DIE KENNUNG, DANN UEBER DEN NAMEN. Zwei Planfaelle desselben * Projekts entstehen fast immer durch "Speichern unter" und tragen dieselben * Kennungen; dort ist die Zuordnung eindeutig. Zwei unabhaengig erfasste * Projekte haben keine gemeinsame Kennung, und dann bleibt nur der Name. Der * Name ist die SCHWAECHERE Auskunft - er kann doppelt vergeben sein und * beschreibt nicht, dass es derselbe Strom ist. Deshalb steht die Art an jeder * Zeile, und eine Zuordnung ueber den Namen bringt einen Vorbehalt mit. * * Ein doppelt vergebener Name ordnet gar nicht zu: Lieber zwei Zeilen "nur * links" und "nur rechts" als eine geratene Gegenueberstellung. */ interface Zuordnung { readonly links: string; readonly rechts: string | null; /** * UEBER WELCHEN WEG das Paar gefunden wurde - nicht daraus geschlossen, ob * die Kennungen gleich sind. Zwei Planfaelle desselben Projekts tragen * dieselben Kennungen; faende die Zuordnung sie ueber den Namen, sagte ein * Vergleich der Kennungen trotzdem "ueber die Kennung". Die Zeile behauptete * dann eine Sicherheit, die die Zuordnung nicht hatte. */ readonly art: 'kennung' | 'name'; } function ordneGruppenZu( links: SignalPlan, rechts: SignalPlan, ): { readonly paare: readonly Zuordnung[]; readonly nurRechts: readonly string[] } { const rechtsNachKennung = new Map(rechts.groups.map((g) => [g.groupId, g])); const namenRechts = new Map(); for (const g of rechts.groups) namenRechts.set(g.name, (namenRechts.get(g.name) ?? 0) + 1); const namenLinks = new Map(); for (const g of links.groups) namenLinks.set(g.name, (namenLinks.get(g.name) ?? 0) + 1); const paare: Zuordnung[] = []; const belegt = new Set(); for (const gruppe of links.groups) { const ueberKennung = rechtsNachKennung.get(gruppe.groupId); if (ueberKennung !== undefined) { paare.push({ links: gruppe.groupId, rechts: ueberKennung.groupId, art: 'kennung' }); belegt.add(ueberKennung.groupId); continue; } const eindeutig = namenRechts.get(gruppe.name) === 1 && namenLinks.get(gruppe.name) === 1; const ueberNamen = eindeutig ? rechts.groups.find((g) => g.name === gruppe.name && !belegt.has(g.groupId)) : undefined; if (ueberNamen !== undefined) { paare.push({ links: gruppe.groupId, rechts: ueberNamen.groupId, art: 'name' }); belegt.add(ueberNamen.groupId); continue; } paare.push({ links: gruppe.groupId, rechts: null, art: 'kennung' }); } return { paare, nurRechts: rechts.groups.filter((g) => !belegt.has(g.groupId)).map((g) => g.groupId), }; } /** Stellt zwei Planfaelle gegenueber. Rechnet nichts und aendert nichts. */ export function vergleicheVarianten( links: Vergleichsseite, rechts: Vergleichsseite, ): Variantenvergleich { const { paare, nurRechts } = ordneGruppenZu(links.plan, rechts.plan); const linksNach = new Map(paare.map((z) => [z.links, z.rechts])); const gruppenLinks = new Map(links.plan.groups.map((g) => [g.groupId, g])); const gruppenRechts = new Map(rechts.plan.groups.map((g) => [g.groupId, g])); const gruppen: Gruppenvergleich[] = []; let ueberNamen = 0; for (const zuordnung of paare) { const l = gruppenLinks.get(zuordnung.links); if (l === undefined) continue; const r = zuordnung.rechts === null ? undefined : gruppenRechts.get(zuordnung.rechts); const art: Zuordnungsart = r === undefined ? 'nur-links' : zuordnung.art; if (art === 'name') ueberNamen += 1; gruppen.push({ name: l.name, art, freigabezeit: zeit(l.totalGreen, r?.totalGreen ?? null), mindestfreigabezeit: zeit(l.times.minGreen, r?.times.minGreen ?? null), }); } for (const id of nurRechts) { const r = gruppenRechts.get(id); if (r === undefined) continue; gruppen.push({ name: r.name, art: 'nur-rechts', freigabezeit: zeit(null, r.totalGreen), mindestfreigabezeit: zeit(null, r.times.minGreen), }); } const zwischenzeiten: Zwischenzeitvergleich[] = []; let verkuerzt = 0; const gesehenRechts = new Set(); for (const eintrag of links.plan.intergreens.values()) { const vonRechts = linksNach.get(eintrag.fromId) ?? null; const nachRechts = linksNach.get(eintrag.toId) ?? null; // Der Schluessel der Gegenseite entsteht nur, wo BEIDE Gruppen zugeordnet // sind: Eine Beziehung, von der eine Haelfte fehlt, hat drueben keine. const gegenschluessel = vonRechts === null || nachRechts === null ? null : intergreenKey(vonRechts, nachRechts); const gegenstueck = gegenschluessel === null ? undefined : rechts.plan.intergreens.get(gegenschluessel); if (gegenschluessel !== null && gegenstueck !== undefined) gesehenRechts.add(gegenschluessel); const vergleich = zeit(eintrag.value, gegenstueck?.value ?? null); if (vergleich.richtung === 'kuerzer') verkuerzt += 1; zwischenzeiten.push({ von: gruppenLinks.get(eintrag.fromId)?.name ?? eintrag.fromId, nach: gruppenLinks.get(eintrag.toId)?.name ?? eintrag.toId, art: gegenstueck === undefined ? 'nur-links' : 'kennung', zeit: vergleich, }); } for (const [schluessel, eintrag] of rechts.plan.intergreens) { if (gesehenRechts.has(schluessel)) continue; zwischenzeiten.push({ von: gruppenRechts.get(eintrag.fromId)?.name ?? eintrag.fromId, nach: gruppenRechts.get(eintrag.toId)?.name ?? eintrag.toId, art: 'nur-rechts', zeit: zeit(null, eintrag.value), }); } const unpaarig = gruppen.filter((g) => g.art === 'nur-links' || g.art === 'nur-rechts').length + zwischenzeiten.filter((z) => z.art === 'nur-links' || z.art === 'nur-rechts').length; return { namen: { links: links.name, rechts: rechts.name }, programme: { links: links.project.program.name, rechts: rechts.project.program.name }, umlaufzeit: zeit(links.plan.cycleTime, rechts.plan.cycleTime), gruppen, zwischenzeiten, verkuerzteZwischenzeiten: verkuerzt, unpaarig, vorbehalte: vorbehalte(links, rechts, ueberNamen, unpaarig), }; } /** * Was diesem Vergleich an Aussagekraft fehlt - jeder Satz einzeln. * * Sie stehen in der Ansicht ueber den Tabellen und nicht darunter: Ein * Unterschied, der von der Migration oder von einer geratenen Zuordnung kommt, * sieht wie ein Planungsunterschied aus. */ function vorbehalte( links: Vergleichsseite, rechts: Vergleichsseite, ueberNamen: number, unpaarig: number, ): string[] { const saetze: string[] = []; for (const seite of [links, rechts]) { if (seite.migriert === true) { saetze.push( `„${seite.name}" stammt aus einer älteren Programmfassung` + (seite.herkunftsfassung === undefined ? '' : ` (Stand ${seite.herkunftsfassung})`) + '. Beim Einlesen wurden die Zwischenzeiten NEU GERECHNET und nicht aus der Datei ' + 'übernommen. Ein Unterschied kann deshalb von der Umstellung kommen statt von der ' + 'Planung.', ); } } for (const seite of [links, rechts]) { if (programmanzahl(seite.project) > 1) { saetze.push( `„${seite.name}" führt ${String(programmanzahl(seite.project))} Tagesprogramme. ` + `Verglichen wird nur das geöffnete („${seite.project.program.name}"); die übrigen ` + 'stehen in diesem Vergleich nicht.', ); } } if (ueberNamen > 0) { saetze.push( `${String(ueberNamen)} Signalgruppen sind über ihren NAMEN einander zugeordnet, nicht über ` + 'ihre Kennung. Der Name sagt nicht, dass es derselbe Strom ist – prüfen Sie diese Zeilen ' + 'einzeln.', ); } if (unpaarig > 0) { saetze.push( `${String(unpaarig)} Zeilen kommen nur auf einer Seite vor. Sie stehen mit einem Strich ` + 'auf der anderen und gehen in keine Differenz ein.', ); } return saetze; }