lsa-planer

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

/ tests domain signalgruppeOhneKonflikt.test.ts

7,9 KB Rohdatei
tests/domain/signalgruppeOhneKonflikt.test.ts — 190 Zeilen
1 import { describe, expect, it } from 'vitest';
2 import {
3 createConflict,
4 createEmptyProject,
5 createPhase,
6 createSignalGroup,
7 createStandardIntersectionProject,
8 } from '@/domain/model/factory';
9 import type { Anlagenart, Conflict, Project, SignalGroup } from '@/domain/model/project';
10 import { buildSignalPlan } from '@/domain/plan/signalPlan';
11 import { validateProject } from '@/domain/validation/engine';
12 import type { ValidationFinding } from '@/domain/validation/types';
13
14 /*
15 * Fassung 5.12.0: Es fehlte die Regel "Signalgruppe ohne Konfliktbeziehung".
16 *
17 * Feindlich ist im Modell nur, was erfasst ist; alles andere gilt als
18 * vertraeglich. Eine Signalgruppe, die in keiner Beziehung steht, bekommt
19 * deshalb keine einzige Zwischenzeit - der Plan darf sie zu jedem Zeitpunkt
20 * freigeben.
21 *
22 * DIE LUECKE, DIE DIE REGEL SCHLIESST, steht im zweiten Fall: Phase 1 fuehrt
23 * den Fahrzeugstrom K1 und die Furt F1, Phase 2 den querenden K2. K1 und K2
24 * sind feindlich erfasst, F1 mit niemandem. Der Uebergang ist ueber K1/K2
25 * bemessen, `zwischenzeiten.uebergang-ungesichert` schweigt deshalb - und die
26 * Furt bekommt ihre Freigabe ohne jeden Abstand zu dem Strom, der sie quert.
27 *
28 * DIE SCHWERE HAENGT AN DER ANLAGENART: An Fussgaengerschutzanlage und
29 * einstreifiger Verkehrsfuehrung ist ein Strom ohne jeden Konflikt nicht
30 * moeglich - Fehler. Am Knotenpunkt ist er es (Rechtsabbieger auf eigenem
31 * Fahrstreifen ohne Furt), und der Anwender haette keinen Weg, einen Fehler
32 * auszuraeumen ausser die Signalgruppe zu loeschen - Warnung.
33 *
34 * GEGEN DEN ALTSTAND schlagen alle Faelle bis auf die drei letzten fehl; jene
35 * bewachen, dass die Regel schweigt, wo sie schweigen muss, und bestehen dort
36 * ebenso.
37 */
38
39 const DATUM = new Date('2026-01-01T00:00:00Z');
40 const REGEL = 'signalgruppen.ohne-konflikt';
41
42 function beziehung(fromId: string, toId: string): Conflict {
43 return { ...createConflict(fromId, toId), clearingDistance: 22, enteringDistance: 8 };
44 }
45
46 /**
47 * Zwei Fahrzeugstroeme in zwei Phasen, feindlich erfasst - dazu ein dritter
48 * Strom in Phase 1, der in keiner Beziehung steht.
49 */
50 function mitStrommOhneBeziehung(
51 anlagenart: Anlagenart,
52 dritter: Partial<Pick<SignalGroup, 'mode'>> = {},
53 ): Project {
54 const basis = createEmptyProject('Prueffall', DATUM, anlagenart);
55 const k1 = createSignalGroup({ name: 'K1', mode: 'kfz', armId: null, index: 0 });
56 const k2 = createSignalGroup({ name: 'K2', mode: 'kfz', armId: null, index: 1 });
57 const f1 = createSignalGroup({ name: 'F1', mode: dritter.mode ?? 'fuss', armId: null, index: 2 });
58 const p1 = createPhase('Phase 1', [k1.id, f1.id]);
59 const p2 = createPhase('Phase 2', [k2.id]);
60 return {
61 ...basis,
62 signalGroups: [k1, k2, f1],
63 phases: [p1, p2],
64 conflicts: [beziehung(k1.id, k2.id), beziehung(k2.id, k1.id)],
65 program: { ...basis.program, phaseOrder: [p1.id, p2.id] },
66 };
67 }
68
69 function befunde(project: Project): readonly ValidationFinding[] {
70 return validateProject(project, buildSignalPlan(project), DATUM).findings;
71 }
72
73 function ohneKonflikt(project: Project): readonly ValidationFinding[] {
74 return befunde(project).filter((f) => f.rule === REGEL);
75 }
76
77 describe('Signalgruppe ohne jede Konfliktbeziehung', () => {
78 it('meldet die Signalgruppe am Knotenpunkt als Warnung', () => {
79 const project = mitStrommOhneBeziehung('knotenpunkt');
80 const treffer = ohneKonflikt(project);
81
82 expect(treffer).toHaveLength(1);
83 expect(treffer[0]!.severity).toBe('warnung');
84 expect(treffer[0]!.title).toBe('"F1" steht in keiner Konfliktbeziehung');
85 expect(treffer[0]!.category).toBe('signalgruppen');
86 // Das Sprungziel zeigt auf die Signalgruppe selbst.
87 expect(treffer[0]!.target).toEqual({
88 kind: 'signalgruppe',
89 id: project.signalGroups.find((g) => g.name === 'F1')!.id,
90 label: 'F1',
91 });
92 // Die Warnung sperrt nicht - am Knotenpunkt ist ein vertraeglicher Strom
93 // moeglich, und ausraeumen liesse sich ein Fehler hier nicht.
94 expect(treffer[0]!.message).toContain('Rechtsabbieger auf eigenem Fahrstreifen');
95 });
96
97 it('schliesst die Luecke, die keine andere Regel faengt', () => {
98 const project = mitStrommOhneBeziehung('knotenpunkt');
99 const alle = befunde(project);
100 const f1 = project.signalGroups.find((g) => g.name === 'F1')!;
101
102 // Der Phasenuebergang ist ueber K1/K2 bemessen; die Regel zum ungesicherten
103 // Uebergang sieht die Furt deshalb nicht.
104 expect(alle.some((f) => f.rule === 'zwischenzeiten.uebergang-ungesichert')).toBe(false);
105 // Und es gibt zu F1 keine Beziehung, deren Gegenrichtung fehlen koennte.
106 expect(alle.some((f) => f.rule === 'konflikte.gegenrichtung-fehlt')).toBe(false);
107 // Ohne die neue Regel stand im Bericht nichts ueber die fehlende
108 // Absicherung von F1. Was sonst zu ihr steht, ist der Hinweis auf die
109 // unbekannte Furtlaenge - er sagt ueber Zwischenzeiten nichts.
110 const zuF1 = alle.filter((f) => f.target?.kind === 'signalgruppe' && f.target.id === f1.id);
111 expect([...zuF1.map((f) => f.rule)].sort()).toEqual([
112 REGEL,
113 'signalplan.mindestfreigabezeit-ohne-furtlaenge',
114 ]);
115 expect(alle.some((f) => f.category === 'zwischenzeiten' || f.category === 'konflikte')).toBe(
116 false,
117 );
118 });
119
120 it('meldet sie an der Fussgaengerschutzanlage als Fehler und sperrt die Ausgabe', () => {
121 const project = mitStrommOhneBeziehung('fussgaengerschutzanlage');
122 const bericht = validateProject(project, buildSignalPlan(project), DATUM);
123 const treffer = bericht.findings.filter((f) => f.rule === REGEL);
124
125 expect(treffer).toHaveLength(1);
126 expect(treffer[0]!.severity).toBe('fehler');
127 expect(treffer[0]!.message).toContain('Fußgängerschutzanlage');
128 expect(bericht.exportBlocked).toBe(true);
129 });
130
131 it('meldet sie an der einstreifigen Verkehrsfuehrung als Fehler', () => {
132 const treffer = ohneKonflikt(mitStrommOhneBeziehung('einstreifig'));
133
134 expect(treffer).toHaveLength(1);
135 expect(treffer[0]!.severity).toBe('fehler');
136 expect(treffer[0]!.message).toContain('denselben Fahrstreifen');
137 });
138
139 it('nennt jede betroffene Signalgruppe einmal, auch mehrere', () => {
140 const basis = mitStrommOhneBeziehung('knotenpunkt');
141 const r1 = createSignalGroup({ name: 'R1', mode: 'rad', armId: null, index: 3 });
142 const project: Project = {
143 ...basis,
144 signalGroups: [...basis.signalGroups, r1],
145 phases: [
146 basis.phases[0]!,
147 { ...basis.phases[1]!, signalGroupIds: [...basis.phases[1]!.signalGroupIds, r1.id] },
148 ],
149 };
150
151 expect(ohneKonflikt(project).map((f) => f.title)).toEqual([
152 '"F1" steht in keiner Konfliktbeziehung',
153 '"R1" steht in keiner Konfliktbeziehung',
154 ]);
155 });
156
157 it('schweigt, wo jede Signalgruppe in einer Beziehung steht', () => {
158 // Der Regelknotenpunkt der Vorlage: alle sechs Gruppen sind erfasst.
159 expect(ohneKonflikt(createStandardIntersectionProject('Vorlage', DATUM))).toHaveLength(0);
160 });
161
162 it('schweigt bei einer Signalgruppe ohne Phase - dort steht der schwerere Befund', () => {
163 const basis = mitStrommOhneBeziehung('knotenpunkt');
164 const f1 = basis.signalGroups.find((g) => g.name === 'F1')!;
165 const ohnePhase: Project = {
166 ...basis,
167 phases: basis.phases.map((p) => ({
168 ...p,
169 signalGroupIds: p.signalGroupIds.filter((id) => id !== f1.id),
170 })),
171 };
172
173 expect(ohneKonflikt(ohnePhase)).toHaveLength(0);
174 expect(befunde(ohnePhase).some((f) => f.rule === 'signalgruppen.ohne-phase')).toBe(true);
175 });
176
177 it('schweigt bei einer einzigen Signalgruppe - es gibt kein Paar', () => {
178 const basis = createEmptyProject('Einzelgruppe', DATUM, 'fussgaengerschutzanlage');
179 const k1 = createSignalGroup({ name: 'K1', mode: 'kfz', armId: null, index: 0 });
180 const p1 = createPhase('Phase 1', [k1.id]);
181 const project: Project = {
182 ...basis,
183 signalGroups: [k1],
184 phases: [p1],
185 program: { ...basis.program, phaseOrder: [p1.id] },
186 };
187
188 expect(ohneKonflikt(project)).toHaveLength(0);
189 });
190 });