lsa-planer

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

/ tests domain mehrdeutigeKennungen.test.ts

9,9 KB Rohdatei
tests/domain/mehrdeutigeKennungen.test.ts — 237 Zeilen
1 import { describe, expect, it } from 'vitest';
2 import { parseProject } from '@/domain/model/schema';
3 import { buildSignalPlan } from '@/domain/plan/signalPlan';
4 import { validateProject } from '@/domain/validation/engine';
5
6 /*
7 * Befunde 17 und 42, behoben in Fassung 5.11.0.
8 *
9 * MEHRDEUTIGE KENNUNGEN WERDEN AUFGELOEST, NICHT GERATEN.
10 *
11 * Kommt eine Signalgruppen- oder Zufahrtskennung zweimal vor, bekam bisher die
12 * ZWEITE eine neue Kennung, die erste behielt die alte - und alles, was auf die
13 * alte verwies, fiel damit der ERSTEN zu: Konfliktbeziehungen, Phasen,
14 * Verkehrsstaerken und Fahrlinien bei den Signalgruppen, `armId` von
15 * Signalgruppen und Haltlinien bei den Zufahrten. Welche der beiden gemeint
16 * war, steht in der Datei nirgends; der Verweis ist nicht falsch, sondern
17 * MEHRDEUTIG.
18 *
19 * Genau dieselbe Bauart hat das Modul fuer Haltlinien (Befunde 34 und 47) und
20 * fuer Konfliktkennungen (Befund 2) bereits als Fehler abgestellt; der Grundsatz
21 * steht woertlich in schema.ts bei `haltlinienMehrdeutig`.
22 *
23 * WAS DER ALTSTAND ANRICHTETE - beides in der gefaehrlichen Richtung:
24 *
25 * (a) Signalgruppen. Die zweite Beziehung derselben Richtung fiel in die
26 * Paardoppelung von `parseConflicts` und wurde mit "Die Konfliktbeziehung
27 * ist doppelt erfasst" verworfen - eine Aussage, die nicht stimmte: Es waren
28 * zwei verschiedene Beziehungen zweier verschiedener Stroeme. Welche
29 * Vermassung ueberlebte, entschied allein die Reihenfolge in der Datei;
30 * stand der kuerzere Raeumweg vorn, blieb der kuerzere stehen. Die zweite
31 * Gruppe stand danach ohne jede Konfliktbeziehung da - `intergreenBetween`
32 * liefert dann 0, und `pruefeFeindlichInPhase` schweigt: Kraftfahrzeugstrom
33 * und gequerte Furt koennen gleichzeitig Freigabe bekommen, ohne einen
34 * einzigen Befund.
35 *
36 * (b) Zufahrten. Beide Stroeme hingen scheinbar an derselben Zufahrt. Damit
37 * entfiel der Hinweis `leistungsfaehigkeit.abbiegerfaktor-pauschal` - die
38 * Regel schliesst Partner derselben, ERFASSTEN Zufahrt aus (rules.ts) -,
39 * und die pauschale Abminderung 0,9 fuer einen bedingt vertraeglichen
40 * Linksabbieger blieb unkommentiert.
41 *
42 * GEGEN DEN ALTSTAND schlagen die Faelle mit doppelter Kennung fehl; die beiden
43 * Gegenproben mit verschiedenen Kennungen bestehen dort ebenso.
44 */
45
46 const DATUM = new Date('2026-01-01T00:00:00Z');
47
48 /** Ein Lageplan mit genau einer Haltlinie und einer Fahrlinie, beide verwiesen. */
49 function lageplan(armId: string, signalGroupId: string): Record<string, unknown> {
50 return {
51 bild: null,
52 kalibrierung: {
53 von: { x: 0, y: 0 },
54 bis: { x: 100, y: 0 },
55 laengeMeter: 100,
56 herkunft: 'gemessen',
57 },
58 haltlinien: [
59 {
60 id: 'hl-1',
61 name: 'HL Nord',
62 art: 'zufahrt',
63 armId,
64 von: { x: 0, y: 0 },
65 bis: { x: 7, y: 0 },
66 },
67 ],
68 linien: [
69 {
70 id: 'pl-1',
71 name: 'Nord geradeaus',
72 mode: 'kfz',
73 movement: 'geradeaus',
74 breiteMeter: 3.5,
75 punkte: [
76 { x: 3.5, y: 0 },
77 { x: 3.5, y: 60 },
78 ],
79 haltlinieId: 'hl-1',
80 startT: 0,
81 signalGroupId,
82 },
83 ],
84 };
85 }
86
87 /** Der Ausloeser aus Befund 17, woertlich. */
88 function dateiMitDoppelterGruppenkennung(): Record<string, unknown> {
89 return {
90 signalGroups: [
91 { id: 'X', name: 'K1', mode: 'kfz', movement: 'geradeaus' },
92 { id: 'X', name: 'K2', mode: 'kfz', movement: 'links' },
93 { id: 'F', name: 'Furt', mode: 'fuss', movement: 'querung' },
94 ],
95 conflicts: [
96 { id: 'cf-1', fromId: 'X', toId: 'F', clearingDistance: 20, enteringDistance: 8 },
97 { id: 'cf-2', fromId: 'X', toId: 'F', clearingDistance: 35, enteringDistance: 8 },
98 { id: 'cf-3', fromId: 'F', toId: 'X', clearingDistance: 12, enteringDistance: 6 },
99 ],
100 phases: [{ id: 'ph-1', name: 'Phase 1', signalGroupIds: ['X', 'F'] }],
101 demands: [
102 { signalGroupId: 'X', volume: 400, heavyVehicleShare: 0.05 },
103 { signalGroupId: 'X', volume: 900, heavyVehicleShare: 0.05 },
104 ],
105 lageplan: lageplan('arm-nord', 'X'),
106 };
107 }
108
109 /** Der Ausloeser aus Befund 42: zwei Zufahrten der Kennung "A". */
110 function dateiMitZufahrtskennungen(zweite: string): Record<string, unknown> {
111 return {
112 intersection: {
113 name: 'Musterkreuzung',
114 arms: [
115 { id: 'A', name: 'Nord', lanes: 2 },
116 { id: zweite, name: 'Süd', lanes: 2 },
117 ],
118 },
119 signalGroups: [
120 { id: 'sg-k1', name: 'K1', mode: 'kfz', movement: 'links', armId: 'A' },
121 { id: 'sg-k2', name: 'K2', mode: 'kfz', movement: 'geradeaus', armId: zweite },
122 ],
123 phases: [{ id: 'ph-1', name: 'Phase 1', signalGroupIds: ['sg-k1', 'sg-k2'] }],
124 demands: [
125 { signalGroupId: 'sg-k1', volume: 300, heavyVehicleShare: 0.05 },
126 { signalGroupId: 'sg-k2', volume: 500, heavyVehicleShare: 0.05 },
127 ],
128 lageplan: lageplan(zweite, 'sg-k2'),
129 };
130 }
131
132 describe('Befund 17 - doppelte Signalgruppenkennung', () => {
133 it('vergibt die zweite Kennung neu und nennt die Folge fuer die Verweise', () => {
134 const { project, issues } = parseProject(dateiMitDoppelterGruppenkennung(), DATUM);
135
136 // Vorbedingung: Beide Gruppen bleiben erhalten und sind unterscheidbar.
137 expect(project.signalGroups).toHaveLength(3);
138 expect(project.signalGroups[0]!.id).not.toBe(project.signalGroups[1]!.id);
139
140 const meldung = issues.find((i) => i.message.includes('Signalgruppenkennung "X"'));
141 expect(meldung?.severity).toBe('fehler');
142 // Wie bei der Haltlinienkennung: Die Meldung sagt, was mit den Verweisen
143 // geschieht - "neu vergeben" allein verschweigt den eigentlichen Verlust.
144 expect(meldung?.message).toContain('steht in der Datei nicht');
145 });
146
147 it('loest die Konfliktbeziehungen auf, statt eine als "doppelt erfasst" zu verwerfen', () => {
148 const { project, issues } = parseProject(dateiMitDoppelterGruppenkennung(), DATUM);
149
150 // Keine der drei Beziehungen laesst sich einem Strom zuordnen: Alle drei
151 // nennen "X". Geraten wird nichts.
152 expect(project.conflicts).toHaveLength(0);
153
154 // Die falsche Aussage des Altstands. Es waren zwei verschiedene
155 // Beziehungen zweier verschiedener Stroeme, keine Doppelerfassung.
156 expect(issues.map((i) => i.message).join(' | ')).not.toContain('doppelt erfasst');
157
158 const konfliktmeldungen = issues.filter((i) => i.path.startsWith('conflicts'));
159 expect(konfliktmeldungen).toHaveLength(3);
160 for (const m of konfliktmeldungen) {
161 expect(m.message).toContain('mehrfach vergebene Signalgruppenkennung');
162 }
163 });
164
165 it('loest Phasenzuordnung, Verkehrsstaerke und Fahrlinie auf', () => {
166 const { project, issues } = parseProject(dateiMitDoppelterGruppenkennung(), DATUM);
167
168 // Die Phase behaelt die eindeutige Furt und verliert die mehrdeutige "X".
169 expect(project.phases[0]!.signalGroupIds).toEqual(['F']);
170 expect(project.demands).toHaveLength(0);
171 expect(project.lageplan.linien[0]!.signalGroupId).toBeNull();
172
173 const pfade = issues.filter((i) => i.message.includes('mehrfach vergebene')).map((i) => i.path);
174 expect(pfade).toContain('phases[0]');
175 expect(pfade).toContain('demands[0]');
176 expect(pfade).toContain('lageplan.linien[0].signalGroupId');
177 });
178
179 it('laesst eindeutige Kennungen unangetastet (Gegenprobe)', () => {
180 const roh = dateiMitDoppelterGruppenkennung();
181 (roh['signalGroups'] as Record<string, unknown>[])[1]!['id'] = 'Y';
182 (roh['conflicts'] as Record<string, unknown>[])[1]!['fromId'] = 'Y';
183 (roh['phases'] as Record<string, unknown>[])[0]!['signalGroupIds'] = ['X', 'Y', 'F'];
184 (roh['demands'] as Record<string, unknown>[])[1]!['signalGroupId'] = 'Y';
185
186 const { project, issues } = parseProject(roh, DATUM);
187
188 expect(project.conflicts).toHaveLength(3);
189 expect(project.phases[0]!.signalGroupIds).toEqual(['X', 'Y', 'F']);
190 expect(project.demands).toHaveLength(2);
191 expect(project.lageplan.linien[0]!.signalGroupId).toBe('X');
192 expect(issues.filter((i) => i.message.includes('mehrfach vergebene'))).toHaveLength(0);
193 });
194 });
195
196 describe('Befund 42 - doppelte Zufahrtskennung', () => {
197 it('hebt die Zuordnung der Signalgruppen und Haltlinien auf', () => {
198 const { project, issues } = parseProject(dateiMitZufahrtskennungen('A'), DATUM);
199
200 // Vorbedingung: Beide Zufahrten bleiben erhalten und sind unterscheidbar.
201 expect(project.intersection.arms).toHaveLength(2);
202 expect(project.intersection.arms[0]!.id).not.toBe(project.intersection.arms[1]!.id);
203
204 // Kein Strom haengt danach an einer geratenen Zufahrt.
205 expect(project.signalGroups.map((g) => g.armId)).toEqual([null, null]);
206 expect(project.lageplan.haltlinien[0]!.armId).toBeNull();
207
208 const meldung = issues.find((i) => i.message.includes('Zufahrtskennung "A"'));
209 expect(meldung?.message).toContain('steht in der Datei nicht');
210 const pfade = issues.filter((i) => i.message.includes('mehrfach vergebene')).map((i) => i.path);
211 expect(pfade).toContain('signalGroups[0].armId');
212 expect(pfade).toContain('signalGroups[1].armId');
213 expect(pfade).toContain('lageplan.haltlinien[0].armId');
214 });
215
216 it('bringt den Hinweis zum pauschalen Abbiegerfaktor zurueck', () => {
217 const { project } = parseProject(dateiMitZufahrtskennungen('A'), DATUM);
218 const bericht = validateProject(project, buildSignalPlan(project), DATUM);
219
220 expect(bericht.findings.map((f) => f.rule)).toContain(
221 'leistungsfaehigkeit.abbiegerfaktor-pauschal',
222 );
223 });
224
225 it('haelt zwei verschiedene Zufahrtskennungen auseinander (Gegenprobe)', () => {
226 const { project, issues } = parseProject(dateiMitZufahrtskennungen('B'), DATUM);
227
228 expect(project.signalGroups.map((g) => g.armId)).toEqual(['A', 'B']);
229 expect(project.lageplan.haltlinien[0]!.armId).toBe('B');
230 expect(issues.filter((i) => i.message.includes('Zufahrtskennung'))).toHaveLength(0);
231
232 const bericht = validateProject(project, buildSignalPlan(project), DATUM);
233 expect(bericht.findings.map((f) => f.rule)).toContain(
234 'leistungsfaehigkeit.abbiegerfaktor-pauschal',
235 );
236 });
237 });