lsa-planer

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

/ tests domain massgebenderStromBericht.test.ts

10,9 KB Rohdatei
tests/domain/massgebenderStromBericht.test.ts — 264 Zeilen
1 import { describe, expect, it } from 'vitest';
2 import { createStandardIntersectionProject } from '@/domain/model/factory';
3 import type { Conflict, Project } from '@/domain/model/project';
4 import {
5 buildSignalPlan,
6 intergreenKey,
7 massgebendeRaeumbeziehung,
8 } from '@/domain/plan/signalPlan';
9 import { raeumbeziehungSatz } from '@/domain/rilsa/ansaetze';
10 import { RILSA_DEFAULTS } from '@/domain/rilsa/constants';
11 import { validateProject } from '@/domain/validation/engine';
12
13 /*
14 * Fassung 5.11.0 - der massgebende Strom im PRUEFBERICHT.
15 *
16 * Eine fruehere Behebung hat die Entscheidung, mit welcher Fahrbeziehung eine Beziehung
17 * raeumt, an genau eine Stelle gelegt: `massgebendeRaeumbeziehung(from,
18 * conflict)` in signalPlan.ts. Der Planaufbau liest sie. Der Pruefbericht
19 * bildete den Ausdruck bis zu diesem Befund weiterhin selbst und nahm
20 * `from.movement` - die Fahrbeziehung der SIGNALGRUPPE.
21 *
22 * Was der Anwender davon merkt: An einer Sammelsignalgruppe "geradeaus UND
23 * rechts" - dem Regelfall des gemeinsamen Fahrstreifens - raeumt der Plan die
24 * Beziehung als Rechtsabbieger und setzt das Merkmal "enger Innenradius" an;
25 * der Bericht derselben Rechnung meldet daneben, das Merkmal gehe "in keine
26 * Zwischenzeit ein", weil der raeumende Strom kein abbiegender
27 * Kraftfahrzeugstrom sei. Zwei Aussagen zu EINER Beziehung, und die des
28 * Berichts raet dazu, ein Merkmal zu entfernen, das gerechnet hat.
29 */
30
31 const DATUM = new Date('2026-01-01T00:00:00Z');
32
33 /**
34 * Standardknotenpunkt, an dem "K1" geradeaus UND rechts fuehrt: Die
35 * Signalgruppe bleibt bei 'geradeaus', die Beziehung K1 -> K3 traegt die
36 * Fahrbeziehung ihres massgebenden Stroms ('rechts') - genau das, was
37 * `uebernimmWege` aus einem Lageplan schreibt.
38 */
39 function sammelgruppe(zusatz: Partial<Conflict> = {}): Project {
40 const basis = createStandardIntersectionProject('Sammelgruppe', DATUM);
41 const k1 = basis.signalGroups.find((g) => g.name === 'K1')!;
42 const k3 = basis.signalGroups.find((g) => g.name === 'K3')!;
43 return {
44 ...basis,
45 conflicts: basis.conflicts.map((c) =>
46 c.fromId === k1.id && c.toId === k3.id
47 ? { ...c, raeumbeziehung: 'rechts' as const, ...zusatz }
48 : c,
49 ),
50 };
51 }
52
53 function beziehung(project: Project): Conflict {
54 const k1 = project.signalGroups.find((g) => g.name === 'K1')!;
55 const k3 = project.signalGroups.find((g) => g.name === 'K3')!;
56 return project.conflicts.find((c) => c.fromId === k1.id && c.toId === k3.id)!;
57 }
58
59 describe('N1/1: Der Bericht beurteilt den engen Innenradius am massgebenden Strom', () => {
60 it('meldet ihn als wirksam, wo der Plan mit ihm rechnet', () => {
61 const project = sammelgruppe({ engerRadius: true });
62 const konflikt = beziehung(project);
63 const plan = buildSignalPlan(project);
64 const gerechnet = plan.intergreens.get(intergreenKey(konflikt.fromId, konflikt.toId));
65
66 // Der Plan raeumt als Rechtsabbieger mit engem Innenradius: 5,0 m/s.
67 expect(gerechnet?.calculation?.clearingSpeed).toBe(
68 RILSA_DEFAULTS.clearingSpeed.kfzTurningEngerRadius,
69 );
70
71 const bericht = validateProject(project, plan, DATUM);
72 const zurBeziehung = bericht.findings.filter(
73 (f) => f.target?.kind === 'konflikt' && f.target.id === konflikt.id,
74 );
75
76 expect(zurBeziehung.map((f) => f.rule)).toContain('zwischenzeiten.enger-radius');
77 expect(zurBeziehung.map((f) => f.rule)).not.toContain(
78 'zwischenzeiten.enger-radius-ohne-wirkung',
79 );
80 });
81
82 it('nennt nicht "kein abbiegender Kraftfahrzeugstrom", wo der Plan abbiegend raeumt', () => {
83 const project = sammelgruppe({ engerRadius: true });
84 const konflikt = beziehung(project);
85 const bericht = validateProject(project, buildSignalPlan(project), DATUM);
86 const texte = bericht.findings
87 .filter((f) => f.target?.kind === 'konflikt' && f.target.id === konflikt.id)
88 .map((f) => f.message)
89 .join(' ');
90
91 expect(texte).not.toContain('kein abbiegender Kraftfahrzeugstrom');
92 });
93
94 it('bleibt bei der Gruppenbeziehung, wo die Beziehung keine eigene fuehrt', () => {
95 // Gegenprobe: ohne `raeumbeziehung` gilt die Fahrbeziehung der Gruppe
96 // ('geradeaus'). Dort wirkt das Merkmal wirklich nicht - der Bericht muss
97 // weiter warnen, sonst prueft der Fall oben nichts.
98 const basis = sammelgruppe({ engerRadius: true });
99 const project: Project = {
100 ...basis,
101 conflicts: basis.conflicts.map((c) => {
102 const { raeumbeziehung: _weg, ...ohne } = c;
103 return ohne;
104 }),
105 };
106 const konflikt = beziehung(project);
107 const plan = buildSignalPlan(project);
108
109 expect(
110 plan.intergreens.get(intergreenKey(konflikt.fromId, konflikt.toId))?.calculation
111 ?.clearingSpeed,
112 ).toBe(RILSA_DEFAULTS.clearingSpeed.kfzStraight);
113 expect(
114 validateProject(project, plan, DATUM)
115 .findings.filter((f) => f.target?.kind === 'konflikt' && f.target.id === konflikt.id)
116 .map((f) => f.rule),
117 ).toContain('zwischenzeiten.enger-radius-ohne-wirkung');
118 });
119
120 it('vergleicht die Handeingabe mit der Raeumgeschwindigkeit des massgebenden Stroms', () => {
121 /*
122 * Der dritte Zweig der Regel (Handeingabe hat Vorrang) nennt die
123 * Raeumgeschwindigkeit, die das Merkmal ergaebe. Mit der Fahrbeziehung der
124 * Gruppe waren das 10,0 m/s - der Geradeauswert -, und der Satz "Geraeumt
125 * wird mit 9 m/s, nicht mit 10 m/s" behauptete eine Verkuerzung, wo die
126 * Eingabe die Zwischenzeit in Wahrheit VERLAENGERT (5,0 m/s waeren
127 * langsamer).
128 */
129 const project = sammelgruppe({ engerRadius: true, clearingSpeedOverride: 9 });
130 const konflikt = beziehung(project);
131 const befund = validateProject(project, buildSignalPlan(project), DATUM).findings.find(
132 (f) =>
133 f.rule === 'zwischenzeiten.enger-radius-ohne-wirkung' &&
134 f.target?.kind === 'konflikt' &&
135 f.target.id === konflikt.id,
136 );
137
138 expect(befund?.message).toContain('nicht mit 5 m/s');
139 expect(befund?.message).toContain('KÜRZER');
140 });
141 });
142
143 /*
144 * NACHTRAG (Fassung 5.11.0): Der Bericht urteilt am massgebenden Strom, sagte
145 * als einzige der vier Auskunftsstellen aber nicht, dass er es tut. Im
146 * Spiegelfall widersprach er damit der Signalgruppentabelle: "der raeumende
147 * Strom "K1" ist kein abbiegender Kraftfahrzeugstrom" ueber einer Gruppe, die
148 * als rechts abbiegend erfasst ist.
149 */
150 describe('N1/7: Der Bericht sagt, dass er am massgebenden Strom urteilt', () => {
151 /** Meldungen zur Beziehung K1 -> K3. */
152 function meldungen(project: Project): { rule: string; message: string }[] {
153 const konflikt = beziehung(project);
154 return validateProject(project, buildSignalPlan(project), DATUM)
155 .findings.filter((f) => f.target?.kind === 'konflikt' && f.target.id === konflikt.id)
156 .map((f) => ({ rule: f.rule, message: f.message }));
157 }
158
159 it('nennt den Strom, wo das Merkmal an ihm wirkt', () => {
160 const project = sammelgruppe({ engerRadius: true });
161 const befund = meldungen(project).find((f) => f.rule === 'zwischenzeiten.enger-radius');
162
163 expect(befund?.message).toContain(
164 'Den Räumweg dieser Beziehung fährt ein Strom der Signalgruppe „K1“ mit der ' +
165 'Fahrbeziehung „rechts abbiegend“; geräumt wird mit ihr.',
166 );
167 });
168
169 it('nennt ihn im Spiegelfall, in dem die Gruppe abbiegt und der Strom geradeaus faehrt', () => {
170 /*
171 * K1 ist als rechts abbiegend erfasst, den Raeumweg dieser Beziehung faehrt
172 * aber der Geradeausstrom. Das Merkmal wirkt an ihm nicht - der Bericht
173 * warnt zu Recht und muss daneben sagen, warum er die Gruppe anders
174 * beurteilt, als die Signalgruppentabelle sie fuehrt.
175 */
176 const basis = sammelgruppe({ engerRadius: true });
177 const project: Project = {
178 ...basis,
179 signalGroups: basis.signalGroups.map((g) =>
180 g.name === 'K1' ? { ...g, movement: 'rechts' as const } : g,
181 ),
182 conflicts: basis.conflicts.map((c) =>
183 c.raeumbeziehung === undefined ? c : { ...c, raeumbeziehung: 'geradeaus' as const },
184 ),
185 };
186 const befund = meldungen(project).find(
187 (f) => f.rule === 'zwischenzeiten.enger-radius-ohne-wirkung',
188 );
189
190 expect(befund?.message).toContain('kein abbiegender Kraftfahrzeugstrom');
191 expect(befund?.message).toContain(
192 'Den Räumweg dieser Beziehung fährt ein Strom der Signalgruppe „K1“ mit der ' +
193 'Fahrbeziehung „geradeaus“; geräumt wird mit ihr.',
194 );
195 });
196
197 it('nennt ihn auch dort, wo eine Handeingabe dem Merkmal vorgeht', () => {
198 const project = sammelgruppe({ engerRadius: true, clearingSpeedOverride: 9 });
199 const befund = meldungen(project).find(
200 (f) => f.rule === 'zwischenzeiten.enger-radius-ohne-wirkung',
201 );
202
203 expect(befund?.message).toContain('Den Räumweg dieser Beziehung fährt ein Strom');
204 });
205
206 it('schweigt dazu, wo Strom und Signalgruppe dieselbe Fahrbeziehung haben', () => {
207 // Gegenprobe: ohne `raeumbeziehung` gibt es nichts zu erklaeren, und ein
208 // Satz darueber waere eine Behauptung.
209 const basis = sammelgruppe({ engerRadius: true });
210 const project: Project = {
211 ...basis,
212 conflicts: basis.conflicts.map((c) => {
213 const { raeumbeziehung: _weg, ...ohne } = c;
214 return ohne;
215 }),
216 };
217
218 expect(
219 meldungen(project)
220 .map((f) => f.message)
221 .join(' '),
222 ).not.toContain('Den Räumweg dieser Beziehung fährt');
223 });
224
225 it('formuliert den Satz einmal - Bericht und Konfliktfenster teilen ihn', () => {
226 // `raeumbeziehungSatz` ist die eine Quelle; das Konfliktfenster setzt
227 // denselben Satz in seinen Kopf (tests/ui/massgebenderStromKonfliktfenster.test.ts).
228 const project = sammelgruppe({ engerRadius: true });
229 const konflikt = beziehung(project);
230 const from = project.signalGroups.find((g) => g.id === konflikt.fromId)!;
231 const satz = raeumbeziehungSatz(from, konflikt);
232
233 expect(satz).not.toBe('');
234 expect(
235 meldungen(project).find((f) => f.rule === 'zwischenzeiten.enger-radius')?.message,
236 ).toContain(satz);
237 });
238 });
239
240 describe('N1/2: Plan und Bericht legen dieselbe Fahrbeziehung zugrunde', () => {
241 it('beurteilt jede Beziehung mit der Fahrbeziehung, die massgebendeRaeumbeziehung nennt', () => {
242 const project = sammelgruppe({ engerRadius: true });
243 const plan = buildSignalPlan(project);
244 const bericht = validateProject(project, plan, DATUM);
245
246 const abweichend = project.conflicts
247 .filter((c) => c.engerRadius === true)
248 .map((c) => {
249 const from = project.signalGroups.find((g) => g.id === c.fromId)!;
250 const massgebend = massgebendeRaeumbeziehung(from, c);
251 const abbiegend = massgebend === 'links' || massgebend === 'rechts';
252 const gemeldet = bericht.findings.some(
253 (f) =>
254 f.rule === 'zwischenzeiten.enger-radius' &&
255 f.target?.kind === 'konflikt' &&
256 f.target.id === c.id,
257 );
258 return { beziehung: `${c.fromId} -> ${c.toId}`, massgebend, abbiegend, gemeldet };
259 })
260 .filter((z) => z.abbiegend !== z.gemeldet);
261
262 expect(abweichend).toEqual([]);
263 });
264 });