lsa-planer

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

/ tests domain fahrbeziehungOhneNachweis.test.ts

11,2 KB Rohdatei
tests/domain/fahrbeziehungOhneNachweis.test.ts — 260 Zeilen
1 import { readFileSync } from 'node:fs';
2 import { describe, expect, it } from 'vitest';
3 import { leiteAb, uebernimmWege, wegSchluessel } from '@/domain/geometrie/ableitung';
4 import { createStandardIntersectionProject } from '@/domain/model/factory';
5 import { parseProject } from '@/domain/model/schema';
6 import type { Conflict, Project } from '@/domain/model/project';
7 import { buildSignalPlan, intergreenKey } from '@/domain/plan/signalPlan';
8 import { RILSA_DEFAULTS } from '@/domain/rilsa/constants';
9 import { validateProject } from '@/domain/validation/engine';
10 import { raeumbeziehungBefunde } from '@/domain/validation/rules';
11 import type { ValidationFinding } from '@/domain/validation/types';
12
13 /*
14 * Befund 4, behoben in Fassung 5.11.0 - der Widerspruch zwischen der
15 * Fahrbeziehung einer Signalgruppe und den Stroemen, die sie im Lageplan
16 * fuehrt, wurde von keiner Regel gemeldet.
17 *
18 * Er steht bis heute in der mitgelieferten `beispiele/vierarmiger-
19 * knotenpunkt.lsap`: "K Nord" ist als 'geradeaus' erfasst und fuehrt im
20 * Lageplan die Linien "Nord geradeaus" UND "Nord rechts". Die Beziehung
21 * sg-k-nord -> sg-k-ost traegt den Raeumweg des Rechtsabbiegers (19,52 m) mit
22 * dem Vermerk "(6 s)"; gerechnet wird sie mit der Fahrbeziehung der Gruppe und
23 * damit mit 10,0 m/s - 5 s. Zu kurz ist die gefaehrliche Richtung.
24 *
25 * Die Datei ist zugleich Prueffstueck fuer den Altdatei-Pfad und wird deshalb
26 * NICHT geaendert. Der Bericht muss den Zustand aber benennen.
27 */
28
29 const DATEI = 'beispiele/vierarmiger-knotenpunkt.lsap';
30 const DATUM = new Date('2026-01-01T00:00:00Z');
31 const REGEL = 'zwischenzeiten.raeumbeziehung-nicht-nachgewiesen';
32
33 /** Die mitgelieferte Altdatei, eingelesen wie beim Oeffnen. */
34 function beispiel(): Project {
35 const ergebnis = parseProject(JSON.parse(readFileSync(DATEI, 'utf8')), DATUM);
36 expect(ergebnis.issues.filter((i) => i.severity === 'fehler')).toEqual([]);
37 return ergebnis.project;
38 }
39
40 /** Dieselbe Datei nach "Ausgewählte übernehmen" ueber alle Beziehungen. */
41 function nachUebernahme(basis: Project = beispiel()): Project {
42 const ableitung = leiteAb(basis, basis.lageplan);
43 return uebernimmWege(basis, basis.lageplan, ableitung, ableitung.wege.map(wegSchluessel), DATUM)
44 .project;
45 }
46
47 function befunde(project: Project): ValidationFinding[] {
48 return validateProject(project, buildSignalPlan(project), DATUM).findings.filter(
49 (f) => f.rule === REGEL,
50 );
51 }
52
53 describe('N1/5: Der Bericht meldet die nicht nachgewiesene Fahrbeziehung', () => {
54 it('trifft die vier Sammelsignalgruppen der Beispieldatei', () => {
55 const gemeldet = befunde(beispiel());
56
57 expect(gemeldet.map((f) => (f.target?.kind === 'signalgruppe' ? f.target.id : ''))).toEqual([
58 'sg-k-nord',
59 'sg-k-ost',
60 'sg-k-sued',
61 'sg-k-west',
62 ]);
63 // Zu kurz gerechnet ist die gefaehrliche Richtung: Der Rechtsabbieger
64 // raeumt mit 7,0 m/s, gerechnet wird mit 10,0 m/s.
65 expect(gemeldet.every((f) => f.severity === 'warnung')).toBe(true);
66 expect(gemeldet.every((f) => f.category === 'zwischenzeiten')).toBe(true);
67 });
68
69 it('nennt beide Fahrbeziehungen, die Zahl der Beziehungen und die Richtung', () => {
70 const befund = befunde(beispiel())[0]!;
71
72 expect(befund.title).toContain('K Nord');
73 expect(befund.message).toContain('rechts abbiegend');
74 expect(befund.message).toContain('geradeaus');
75 // Alle sieben Beziehungen der Gruppe sind betroffen - keine traegt die
76 // Fahrbeziehung ihres massgebenden Stroms.
77 expect(befund.message).toContain('7 von 7');
78 expect(befund.message).toContain('zu kurz');
79 // Eine Regel traegt Ueberschrift und naechsten Schritt selbst; sie faellt
80 // nicht auf den Sammeltitel der Plan-Notizen zurueck.
81 expect(befund.title).not.toBe('Hinweis aus dem Planaufbau');
82 expect(befund.suggestion).not.toBe('Prüfen Sie die zugrunde liegenden Eingaben.');
83 expect(befund.suggestion).toContain('Lageplan');
84 expect(befund.reference).not.toBe('');
85 });
86
87 it('schweigt nach einer Uebernahme aus dem Lageplan', () => {
88 // Dort steht das Feld, wo es gebraucht wird - und wo es fehlt, ist sein
89 // Fehlen die Entscheidung derselben Uebernahme.
90 const project = nachUebernahme();
91 // Zwoelf seit 5.24.0; bis dahin acht - siehe massgebenderStrom.
92 expect(project.conflicts.filter((c) => 'raeumbeziehung' in c)).toHaveLength(12);
93 expect(befunde(project)).toEqual([]);
94 });
95
96 it('schweigt ohne Lageplan', () => {
97 // Der Standardknotenpunkt fuehrt keine Planlinien; ueber die Stroeme
98 // seiner Signalgruppen ist nichts bekannt, und eine Meldung waere eine
99 // Behauptung ins Blaue.
100 expect(befunde(createStandardIntersectionProject('Ohne Lageplan', DATUM))).toEqual([]);
101 });
102
103 it('bleibt ein Hinweis, wo die Zwischenzeit dadurch zu lang ausfaellt', () => {
104 /*
105 * Spiegelfall: Traegt die Sammelgruppe 'rechts', so raeumt der Plan ihre
106 * Beziehungen mit 7,0 m/s, waehrend der gebuendelte Geradeausstrom 10,0
107 * m/s faehrt. Die Zwischenzeit faellt dann zu LANG aus - zu klaeren ist
108 * das auch, gefaehrlich ist es nicht.
109 */
110 const basis = beispiel();
111 const gedreht: Project = {
112 ...basis,
113 signalGroups: basis.signalGroups.map((g) =>
114 g.id === 'sg-k-nord' ? { ...g, movement: 'rechts' as const } : g,
115 ),
116 };
117 const nord = befunde(gedreht).find(
118 (f) => f.target?.kind === 'signalgruppe' && f.target.id === 'sg-k-nord',
119 );
120
121 expect(nord?.severity).toBe('hinweis');
122 expect(nord?.message).toContain('zu lang');
123 // Die uebrigen drei bleiben bei der Warnung.
124 expect(befunde(gedreht).filter((f) => f.severity === 'warnung')).toHaveLength(3);
125 });
126
127 it('sagt den Rueckfall mit dem Wortlaut, den auch die Planunterlage fuehrt', () => {
128 // Beide Auskunftsstellen beschreiben denselben Sachverhalt; der
129 // Erlaeuterungsabsatz der Unterlage nennt ihn seit Fassung 5.11.0 ebenfalls.
130 expect(befunde(beispiel())[0]?.message).toContain('mit der Fahrbeziehung der Signalgruppe');
131 });
132
133 it('zaehlt eine von Hand vorgegebene Zwischenzeit nicht mit', () => {
134 // Wo die Zwischenzeit von Hand steht, geht keine Fahrbeziehung in eine
135 // Zahl ein - dort ist nichts zu klaeren.
136 const basis = beispiel();
137 const project: Project = {
138 ...basis,
139 conflicts: basis.conflicts.map((c) =>
140 c.fromId === 'sg-k-nord' && c.toId === 'sg-k-ost' ? { ...c, manualIntergreen: 9 } : c,
141 ),
142 };
143 const nord = befunde(project).find(
144 (f) => f.target?.kind === 'signalgruppe' && f.target.id === 'sg-k-nord',
145 );
146
147 expect(nord?.message).toContain('6 von 7');
148 });
149 });
150
151 /*
152 * NACHTRAG (Fassung 5.11.0): Die Regel loeste die Raeumgeschwindigkeit selbst
153 * auf (`clearingSpeedOverride ?? clearingSpeedFor`) und liess damit JEDE
154 * Handeingabe als massgeblich gelten - auch eine, die der Fachkern verwirft.
155 * Sie haengt nicht an der Fahrbeziehung; der Regelwert, auf den zurueckgefallen
156 * wird, haengt sehr wohl an ihr. Wo die Regel schwieg, raeumte der Plan weiter
157 * mit 10,0 m/s als Gruppe und mit 7,0 m/s als Abbieger. Schweigen ist die
158 * gefaehrliche Richtung.
159 */
160 describe('N1/6: Die Regel prueft die Handeingabe wie der Fachkern', () => {
161 /** Die Beispieldatei mit einem Zusatz an allen sieben Beziehungen von "K Nord". */
162 function mitZusatz(zusatz: Partial<Conflict>, basis: Project = beispiel()): Project {
163 return {
164 ...basis,
165 conflicts: basis.conflicts.map((c) => (c.fromId === 'sg-k-nord' ? { ...c, ...zusatz } : c)),
166 };
167 }
168
169 function nordBefund(project: Project): ValidationFinding | undefined {
170 return befunde(project).find(
171 (f) => f.target?.kind === 'signalgruppe' && f.target.id === 'sg-k-nord',
172 );
173 }
174
175 it('meldet weiter, wo der Fachkern die eingetragene Raeumgeschwindigkeit verwirft', () => {
176 const project = mitZusatz({ clearingSpeedOverride: 0 });
177
178 // Der Plan setzt die 0 nicht an: Er faellt auf den Regelwert der
179 // Fahrbeziehung zurueck und raeumt die Gruppe mit 10,0 m/s.
180 expect(
181 buildSignalPlan(project).intergreens.get(intergreenKey('sg-k-nord', 'sg-k-ost'))?.calculation
182 ?.clearingSpeed,
183 ).toBe(RILSA_DEFAULTS.clearingSpeed.kfzStraight);
184
185 const nord = nordBefund(project);
186 expect(nord?.severity).toBe('warnung');
187 expect(nord?.message).toContain('7 von 7');
188 });
189
190 it('schweigt, wo eine gueltige Handeingabe die Fahrbeziehung gegenstandslos macht', () => {
191 // Gegenprobe zum Fall darueber: 8,0 m/s setzt der Fachkern an, und zwar
192 // fuer jede Fahrbeziehung dieselben. Dann ist nichts zu klaeren.
193 expect(nordBefund(mitZusatz({ clearingSpeedOverride: 8 }))).toBeUndefined();
194 });
195
196 it('misst am Regelwert und damit an der harten Obergrenze der Verkehrsart', () => {
197 /*
198 * Ueber der Obergrenze von 10,0 m/s rechnet der Plan geradeaus wie
199 * abbiegend mit dem Deckel - die Fahrbeziehung faellt aus der Rechnung.
200 * `clearingSpeedFor` kennt ihn nicht und meldete hier 14,0 gegen 12,0 m/s:
201 * eine Warnung "zu kurz gerechnet" ueber zwei Zahlen, die im Plan
202 * dieselben sind.
203 */
204 const defaults = {
205 ...RILSA_DEFAULTS,
206 clearingSpeed: { ...RILSA_DEFAULTS.clearingSpeed, kfzStraight: 14, kfzTurning: 12 },
207 };
208
209 expect(raeumbeziehungBefunde(beispiel(), defaults)).toEqual([]);
210 // Ohne die Kappung waeren es die vier Sammelsignalgruppen - der Fall
211 // prueft nur etwas, solange die Regel ohne Kappung anschlagen wuerde.
212 expect(befunde(beispiel())).toHaveLength(4);
213 });
214
215 it('meldet weiter, wo der Fachkern die eingetragene Ueberfahrzeit verwirft', () => {
216 /*
217 * Beim Ansatz "feste Ueberfahrzeit" haengt auch die Ueberfahrzeit an der
218 * Fahrbeziehung (geradeaus laenger als abbiegend). Die eingetragene
219 * Raeumgeschwindigkeit nimmt vr aus dem Vergleich; bleibt eine
220 * Ueberfahrzeit von -1 s, die der Fachkern verwirft. Ungeprueft
221 * uebernommen sah die Regel zwei gleiche Werte und schwieg.
222 */
223 const basis = beispiel();
224 const festerAnsatz: Project = {
225 ...basis,
226 settings: { ...basis.settings, ueberfahrzeitAnsatz: 'fest' },
227 };
228 const project = mitZusatz({ clearingSpeedOverride: 8, crossingTimeOverride: -1 }, festerAnsatz);
229
230 const nord = nordBefund(project);
231 expect(nord?.message).toContain('7 von 7');
232 // Der abbiegende Strom ueberfaehrt kuerzer: gerechnet wird zu LANG.
233 expect(nord?.severity).toBe('hinweis');
234 });
235
236 it('behaelt den Regelwert als Untergrenze der Kfz-Ueberfahrzeit', () => {
237 /*
238 * 2,5 s liegen zwischen den beiden festen Werten. Der Fachkern setzt sie
239 * beim Abbieger an (2 s Regelwert) und verwirft sie geradeaus (3 s
240 * Regelwert) - die Fahrbeziehung entscheidet also weiterhin, obwohl an der
241 * Beziehung eine gueltige Ueberfahrzeit steht.
242 */
243 const basis = beispiel();
244 const festerAnsatz: Project = {
245 ...basis,
246 settings: { ...basis.settings, ueberfahrzeitAnsatz: 'fest' },
247 };
248 const project = mitZusatz(
249 { clearingSpeedOverride: 8, crossingTimeOverride: 2.5 },
250 festerAnsatz,
251 );
252
253 // Was der Plan an der Gruppe rechnet: die 3 s des Geradeausstroms.
254 expect(
255 buildSignalPlan(project).intergreens.get(intergreenKey('sg-k-nord', 'sg-k-ost'))?.calculation
256 ?.crossingTime,
257 ).toBe(RILSA_DEFAULTS.crossingTime.kfzGeradeaus);
258 expect(nordBefund(project)?.message).toContain('7 von 7');
259 });
260 });