lsa-planer

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

/ src domain plan vergleich.ts

11,8 KB Rohdatei
src/domain/plan/vergleich.ts — 303 Zeilen
1 import { roundTo } from '../units';
2 import { intergreenKey, type SignalPlan } from './signalPlan';
3 import { programmanzahl } from '../model/programme';
4 import type { Project } from '../model/project';
5 import type { Seconds } from '../units';
6
7 /**
8 * Zwei Planfaelle nebeneinander - Bestand gegen Entwurf, Entwurf gegen Entwurf.
9 *
10 * WOZU DAS DA IST, UND ES IST NICHT DIE BEQUEMLICHKEIT: Wer heute einen
11 * Raeumweg berichtigt, eine Fahrbeziehung aendert oder einen Kennwert setzt,
12 * bekommt einen neu gerechneten Plan und sieht den Endstand. Welche
13 * Zwischenzeit dabei KUERZER geworden ist, sagt ihm niemand - und das ist die
14 * gefaehrliche Richtung. Dieser Vergleich weist die Verkuerzungen eigens aus,
15 * gezaehlt und einzeln benannt.
16 *
17 * ER RECHNET NICHTS NEU. Beide Seiten kommen als fertige Plaene herein; diese
18 * Datei stellt sie gegenueber. Damit kann kein Vergleich entstehen, den das
19 * Programm nicht auch anzeigen wuerde.
20 *
21 * ER AENDERT NICHTS. Es gibt keine Funktion, die einen Wert von der einen auf
22 * die andere Seite uebernimmt - weder hier noch in der Ansicht. Ein Knopf
23 * "uebernehmen" wuerde eine Zwischenzeit aus einem fremden Zusammenhang in
24 * diese Planung tragen, ohne dass ihre Wege dazu passen muessten.
25 */
26
27 /** Woher die Zuordnung zweier Signalgruppen stammt. */
28 export type Zuordnungsart = 'kennung' | 'name' | 'nur-links' | 'nur-rechts';
29
30 /** In welche Richtung sich eine Zeit geaendert hat. */
31 export type Aenderungsrichtung = 'kuerzer' | 'laenger' | 'gleich' | 'unbestimmt';
32
33 export interface Zeitvergleich {
34 readonly links: Seconds | null;
35 readonly rechts: Seconds | null;
36 /** rechts minus links; null, wo eine Seite fehlt. */
37 readonly differenz: Seconds | null;
38 readonly richtung: Aenderungsrichtung;
39 }
40
41 export interface Gruppenvergleich {
42 readonly name: string;
43 readonly art: Zuordnungsart;
44 readonly freigabezeit: Zeitvergleich;
45 readonly mindestfreigabezeit: Zeitvergleich;
46 }
47
48 export interface Zwischenzeitvergleich {
49 readonly von: string;
50 readonly nach: string;
51 readonly art: Zuordnungsart;
52 readonly zeit: Zeitvergleich;
53 }
54
55 export interface Variantenvergleich {
56 /** Bezeichnung der beiden Planfaelle, wie sie in der Ansicht stehen. */
57 readonly namen: { readonly links: string; readonly rechts: string };
58 /** Verglichen wird je Seite das AKTIVE Tagesprogramm; hier steht, welches. */
59 readonly programme: { readonly links: string; readonly rechts: string };
60 readonly umlaufzeit: Zeitvergleich;
61 readonly gruppen: readonly Gruppenvergleich[];
62 readonly zwischenzeiten: readonly Zwischenzeitvergleich[];
63 /**
64 * Wie viele Zwischenzeiten rechts KUERZER sind als links.
65 *
66 * Die eine Zahl, um derentwillen es diese Ansicht gibt. Sie steht oben und
67 * nicht am Ende einer Tabelle.
68 */
69 readonly verkuerzteZwischenzeiten: number;
70 /** Wie viele Signalgruppen oder Beziehungen nur auf einer Seite vorkommen. */
71 readonly unpaarig: number;
72 /**
73 * Was den Vergleich einschraenkt - Migration, mehrere Programme, Zuordnung
74 * ueber den Namen. Jeder Satz ist ein Vorbehalt gegen die Zahlen daneben.
75 */
76 readonly vorbehalte: readonly string[];
77 }
78
79 /** Die Seite eines Vergleichs: Projekt, Plan und wie sie heisst. */
80 export interface Vergleichsseite {
81 readonly name: string;
82 readonly project: Project;
83 readonly plan: SignalPlan;
84 /**
85 * Wurde dieses Projekt beim Einlesen aus einer aelteren Schemafassung
86 * hochgezogen? Dann sind seine Zwischenzeiten NEU GERECHNET und nicht die
87 * der Datei - ein Unterschied kann von der Migration kommen statt von der
88 * Planung.
89 */
90 readonly migriert?: boolean;
91 /** Schemafassung der Quelldatei, wo bekannt. */
92 readonly herkunftsfassung?: string;
93 }
94
95 function zeit(links: Seconds | null, rechts: Seconds | null): Zeitvergleich {
96 if (links === null || rechts === null) {
97 return { links, rechts, differenz: null, richtung: 'unbestimmt' };
98 }
99 const differenz = roundTo(rechts - links, 3);
100 return {
101 links,
102 rechts,
103 differenz,
104 richtung: differenz < 0 ? 'kuerzer' : differenz > 0 ? 'laenger' : 'gleich',
105 };
106 }
107
108 /**
109 * Zuordnung der Signalgruppen beider Seiten.
110 *
111 * ZUERST UEBER DIE KENNUNG, DANN UEBER DEN NAMEN. Zwei Planfaelle desselben
112 * Projekts entstehen fast immer durch "Speichern unter" und tragen dieselben
113 * Kennungen; dort ist die Zuordnung eindeutig. Zwei unabhaengig erfasste
114 * Projekte haben keine gemeinsame Kennung, und dann bleibt nur der Name. Der
115 * Name ist die SCHWAECHERE Auskunft - er kann doppelt vergeben sein und
116 * beschreibt nicht, dass es derselbe Strom ist. Deshalb steht die Art an jeder
117 * Zeile, und eine Zuordnung ueber den Namen bringt einen Vorbehalt mit.
118 *
119 * Ein doppelt vergebener Name ordnet gar nicht zu: Lieber zwei Zeilen "nur
120 * links" und "nur rechts" als eine geratene Gegenueberstellung.
121 */
122 interface Zuordnung {
123 readonly links: string;
124 readonly rechts: string | null;
125 /**
126 * UEBER WELCHEN WEG das Paar gefunden wurde - nicht daraus geschlossen, ob
127 * die Kennungen gleich sind. Zwei Planfaelle desselben Projekts tragen
128 * dieselben Kennungen; faende die Zuordnung sie ueber den Namen, sagte ein
129 * Vergleich der Kennungen trotzdem "ueber die Kennung". Die Zeile behauptete
130 * dann eine Sicherheit, die die Zuordnung nicht hatte.
131 */
132 readonly art: 'kennung' | 'name';
133 }
134
135 function ordneGruppenZu(
136 links: SignalPlan,
137 rechts: SignalPlan,
138 ): { readonly paare: readonly Zuordnung[]; readonly nurRechts: readonly string[] } {
139 const rechtsNachKennung = new Map(rechts.groups.map((g) => [g.groupId, g]));
140 const namenRechts = new Map<string, number>();
141 for (const g of rechts.groups) namenRechts.set(g.name, (namenRechts.get(g.name) ?? 0) + 1);
142 const namenLinks = new Map<string, number>();
143 for (const g of links.groups) namenLinks.set(g.name, (namenLinks.get(g.name) ?? 0) + 1);
144
145 const paare: Zuordnung[] = [];
146 const belegt = new Set<string>();
147 for (const gruppe of links.groups) {
148 const ueberKennung = rechtsNachKennung.get(gruppe.groupId);
149 if (ueberKennung !== undefined) {
150 paare.push({ links: gruppe.groupId, rechts: ueberKennung.groupId, art: 'kennung' });
151 belegt.add(ueberKennung.groupId);
152 continue;
153 }
154 const eindeutig = namenRechts.get(gruppe.name) === 1 && namenLinks.get(gruppe.name) === 1;
155 const ueberNamen = eindeutig
156 ? rechts.groups.find((g) => g.name === gruppe.name && !belegt.has(g.groupId))
157 : undefined;
158 if (ueberNamen !== undefined) {
159 paare.push({ links: gruppe.groupId, rechts: ueberNamen.groupId, art: 'name' });
160 belegt.add(ueberNamen.groupId);
161 continue;
162 }
163 paare.push({ links: gruppe.groupId, rechts: null, art: 'kennung' });
164 }
165 return {
166 paare,
167 nurRechts: rechts.groups.filter((g) => !belegt.has(g.groupId)).map((g) => g.groupId),
168 };
169 }
170
171 /** Stellt zwei Planfaelle gegenueber. Rechnet nichts und aendert nichts. */
172 export function vergleicheVarianten(
173 links: Vergleichsseite,
174 rechts: Vergleichsseite,
175 ): Variantenvergleich {
176 const { paare, nurRechts } = ordneGruppenZu(links.plan, rechts.plan);
177 const linksNach = new Map(paare.map((z) => [z.links, z.rechts]));
178 const gruppenLinks = new Map(links.plan.groups.map((g) => [g.groupId, g]));
179 const gruppenRechts = new Map(rechts.plan.groups.map((g) => [g.groupId, g]));
180
181 const gruppen: Gruppenvergleich[] = [];
182 let ueberNamen = 0;
183 for (const zuordnung of paare) {
184 const l = gruppenLinks.get(zuordnung.links);
185 if (l === undefined) continue;
186 const r = zuordnung.rechts === null ? undefined : gruppenRechts.get(zuordnung.rechts);
187 const art: Zuordnungsart = r === undefined ? 'nur-links' : zuordnung.art;
188 if (art === 'name') ueberNamen += 1;
189 gruppen.push({
190 name: l.name,
191 art,
192 freigabezeit: zeit(l.totalGreen, r?.totalGreen ?? null),
193 mindestfreigabezeit: zeit(l.times.minGreen, r?.times.minGreen ?? null),
194 });
195 }
196 for (const id of nurRechts) {
197 const r = gruppenRechts.get(id);
198 if (r === undefined) continue;
199 gruppen.push({
200 name: r.name,
201 art: 'nur-rechts',
202 freigabezeit: zeit(null, r.totalGreen),
203 mindestfreigabezeit: zeit(null, r.times.minGreen),
204 });
205 }
206
207 const zwischenzeiten: Zwischenzeitvergleich[] = [];
208 let verkuerzt = 0;
209 const gesehenRechts = new Set<string>();
210 for (const eintrag of links.plan.intergreens.values()) {
211 const vonRechts = linksNach.get(eintrag.fromId) ?? null;
212 const nachRechts = linksNach.get(eintrag.toId) ?? null;
213 // Der Schluessel der Gegenseite entsteht nur, wo BEIDE Gruppen zugeordnet
214 // sind: Eine Beziehung, von der eine Haelfte fehlt, hat drueben keine.
215 const gegenschluessel =
216 vonRechts === null || nachRechts === null ? null : intergreenKey(vonRechts, nachRechts);
217 const gegenstueck =
218 gegenschluessel === null ? undefined : rechts.plan.intergreens.get(gegenschluessel);
219 if (gegenschluessel !== null && gegenstueck !== undefined) gesehenRechts.add(gegenschluessel);
220 const vergleich = zeit(eintrag.value, gegenstueck?.value ?? null);
221 if (vergleich.richtung === 'kuerzer') verkuerzt += 1;
222 zwischenzeiten.push({
223 von: gruppenLinks.get(eintrag.fromId)?.name ?? eintrag.fromId,
224 nach: gruppenLinks.get(eintrag.toId)?.name ?? eintrag.toId,
225 art: gegenstueck === undefined ? 'nur-links' : 'kennung',
226 zeit: vergleich,
227 });
228 }
229 for (const [schluessel, eintrag] of rechts.plan.intergreens) {
230 if (gesehenRechts.has(schluessel)) continue;
231 zwischenzeiten.push({
232 von: gruppenRechts.get(eintrag.fromId)?.name ?? eintrag.fromId,
233 nach: gruppenRechts.get(eintrag.toId)?.name ?? eintrag.toId,
234 art: 'nur-rechts',
235 zeit: zeit(null, eintrag.value),
236 });
237 }
238
239 const unpaarig =
240 gruppen.filter((g) => g.art === 'nur-links' || g.art === 'nur-rechts').length +
241 zwischenzeiten.filter((z) => z.art === 'nur-links' || z.art === 'nur-rechts').length;
242
243 return {
244 namen: { links: links.name, rechts: rechts.name },
245 programme: { links: links.project.program.name, rechts: rechts.project.program.name },
246 umlaufzeit: zeit(links.plan.cycleTime, rechts.plan.cycleTime),
247 gruppen,
248 zwischenzeiten,
249 verkuerzteZwischenzeiten: verkuerzt,
250 unpaarig,
251 vorbehalte: vorbehalte(links, rechts, ueberNamen, unpaarig),
252 };
253 }
254
255 /**
256 * Was diesem Vergleich an Aussagekraft fehlt - jeder Satz einzeln.
257 *
258 * Sie stehen in der Ansicht ueber den Tabellen und nicht darunter: Ein
259 * Unterschied, der von der Migration oder von einer geratenen Zuordnung kommt,
260 * sieht wie ein Planungsunterschied aus.
261 */
262 function vorbehalte(
263 links: Vergleichsseite,
264 rechts: Vergleichsseite,
265 ueberNamen: number,
266 unpaarig: number,
267 ): string[] {
268 const saetze: string[] = [];
269 for (const seite of [links, rechts]) {
270 if (seite.migriert === true) {
271 saetze.push(
272 `„${seite.name}" stammt aus einer älteren Programmfassung` +
273 (seite.herkunftsfassung === undefined ? '' : ` (Stand ${seite.herkunftsfassung})`) +
274 '. Beim Einlesen wurden die Zwischenzeiten NEU GERECHNET und nicht aus der Datei ' +
275 'übernommen. Ein Unterschied kann deshalb von der Umstellung kommen statt von der ' +
276 'Planung.',
277 );
278 }
279 }
280 for (const seite of [links, rechts]) {
281 if (programmanzahl(seite.project) > 1) {
282 saetze.push(
283 `„${seite.name}" führt ${String(programmanzahl(seite.project))} Tagesprogramme. ` +
284 `Verglichen wird nur das geöffnete („${seite.project.program.name}"); die übrigen ` +
285 'stehen in diesem Vergleich nicht.',
286 );
287 }
288 }
289 if (ueberNamen > 0) {
290 saetze.push(
291 `${String(ueberNamen)} Signalgruppen sind über ihren NAMEN einander zugeordnet, nicht über ` +
292 'ihre Kennung. Der Name sagt nicht, dass es derselbe Strom ist – prüfen Sie diese Zeilen ' +
293 'einzeln.',
294 );
295 }
296 if (unpaarig > 0) {
297 saetze.push(
298 `${String(unpaarig)} Zeilen kommen nur auf einer Seite vor. Sie stehen mit einem Strich ` +
299 'auf der anderen und gehen in keine Differenz ein.',
300 );
301 }
302 return saetze;
303 }