lsa-planer

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

/ tests domain hoechstfreigabezeitGruppe.test.ts

8,0 KB Rohdatei
tests/domain/hoechstfreigabezeitGruppe.test.ts — 181 Zeilen
1 import { describe, expect, it } from 'vitest';
2 import { buildSignalPlan } from '@/domain/plan/signalPlan';
3 import { validateProject } from '@/domain/validation/engine';
4 import { createStandardIntersectionProject } from '@/domain/model/factory';
5 import { parseProject } from '@/domain/model/schema';
6 import type { Project } from '@/domain/model/project';
7
8 /*
9 * Befunde 36 und 42, behoben in Fassung 5.10.0 - die Hoechstfreigabezeit je
10 * Signalgruppe wurde eingelesen, gemeldet und gespeichert, aber von keiner
11 * Rechnung gelesen.
12 *
13 * `SignalGroup.maxGreenOverride` kam aus der Projektdatei und aus dem
14 * 4.x-Aufstieg (Feld "maxGruen", uebernommen "damit bewusst gesetzte Werte
15 * nicht verloren gehen"), ueberlebte jedes Speichern - und die
16 * Freigabezeitverteilung setzte fuer jede Phase ohne Handvorgabe unbedingt
17 * `defaults.maxGreen`. Damit war die Behandlung genau verkehrt herum: Ein
18 * UNLESBARER Eintrag wurde gemeldet ("Gerechnet wird ohne diese obere
19 * Schranke"), ein LESBARER stumm verworfen. Die Meldung setzt voraus, dass ein
20 * lesbarer Wert als obere Schranke wirkt.
21 *
22 * Massgebend fuer die Phase ist der KLEINSTE Wert ihrer Signalgruppen: Alle
23 * Gruppen einer Phase erhalten dieselbe Freigabezeit, eine Obergrenze ist
24 * nur eingehalten, wenn die kleinste eingehalten ist. Liegt sie unter der
25 * Mindestfreigabezeit, bleibt diese massgebend - sie ist die harte Schranke -,
26 * und die Vorgabe wird als wirkungslos gemeldet statt still uebergangen.
27 *
28 * TRAEGT DIE PHASE EINE FESTE FREIGABEZEIT, bestimmt sie den Wert - die
29 * Schranke der Signalgruppen kommt dann nicht zur Wirkung. Gemeldet wird das
30 * mit dem tatsaechlich massgebenden Wert (Nachtrag zu denselben
31 * Befunden): Der Satz zur Mindestfreigabezeit waere hier falsch, und stumm
32 * verfallen darf der lesbare Eintrag ebenso wenig.
33 *
34 * GEGEN DEN ALTSTAND (5.9.0) schlagen fuenf der acht Faelle fehl: die drei
35 * zur wirkenden Schranke und die beiden zur festen Freigabezeit. Die drei
36 * uebrigen sind Gegenproben (Regelwert ohne Vorgabe, eingehaltene Vorgabe,
37 * unlesbarer Eintrag) und bestehen dort ebenso.
38 */
39
40 const DATUM = new Date('2026-01-01T00:00:00Z');
41
42 /** Standardknotenpunkt mit fester Umlaufzeit und Vorgaben je Signalgruppe. */
43 function mitHoechstfreigabezeit(
44 werte: Readonly<Record<string, number | null>>,
45 festeFreigabezeitPhase1: number | null = null,
46 ): Project {
47 const roh = createStandardIntersectionProject('Höchstfreigabezeit', DATUM);
48 return {
49 ...roh,
50 signalGroups: roh.signalGroups.map((g) =>
51 werte[g.name] === undefined ? g : { ...g, maxGreenOverride: werte[g.name] ?? null },
52 ),
53 phases: roh.phases.map((p, index) =>
54 index === 0 ? { ...p, manualGreen: festeFreigabezeitPhase1 } : p,
55 ),
56 program: { ...roh.program, method: 'manuell', manualCycleTime: 120 },
57 };
58 }
59
60 describe('Befunde 36/42 - Hoechstfreigabezeit je Signalgruppe', () => {
61 it('deckelt die Freigabezeit der Phase mit der Vorgabe der Signalgruppe', () => {
62 const plan = buildSignalPlan(mitHoechstfreigabezeit({ K1: 30 }));
63
64 expect(plan.phases[0]?.duration).toBeLessThanOrEqual(30);
65 // Die frei gewordene Zeit geht an die zweite Phase; die Summe bleibt der
66 // Umlauf.
67 expect(plan.phases[1]?.duration ?? 0).toBeGreaterThan(39);
68 expect(plan.groups.find((g) => g.name === 'K1')?.totalGreen ?? 0).toBeLessThanOrEqual(30);
69 });
70
71 it('nimmt den kleinsten Wert der Signalgruppen einer Phase', () => {
72 // K1 und K2 laufen in derselben Phase. Eine Obergrenze ist nur
73 // eingehalten, wenn die kleinste eingehalten ist.
74 const plan = buildSignalPlan(mitHoechstfreigabezeit({ K1: 40, K2: 25 }));
75
76 expect(plan.phases[0]?.duration).toBeLessThanOrEqual(25);
77 });
78
79 it('meldet eine Vorgabe unter der Mindestfreigabezeit, statt sie still zu uebergehen', () => {
80 const projekt = mitHoechstfreigabezeit({ K1: 3 });
81 const plan = buildSignalPlan(projekt);
82
83 const notiz = plan.notes.find(
84 (n) => n.code === 'hoechstfreigabezeit-unter-mindestfreigabezeit',
85 );
86 expect(notiz).toBeDefined();
87 expect(notiz?.severity).toBe('warnung');
88 expect(notiz?.message).toContain('K1');
89 // Die Mindestfreigabezeit bleibt die harte Schranke.
90 const k1 = plan.groups.find((g) => g.name === 'K1')!;
91 expect(plan.phases[0]?.duration).toBe(k1.mindestfreigabezeit.wert);
92
93 // Und im Bericht steht die Meldung mit eigener Ueberschrift statt unter
94 // dem Sammeltitel.
95 const befund = validateProject(projekt, plan).findings.find(
96 (f) => f.rule === 'signalplan.hoechstfreigabezeit-unter-mindestfreigabezeit',
97 );
98 expect(befund?.title).not.toBe('Hinweis aus dem Planaufbau');
99 expect(befund?.category).toBe('signalgruppen');
100 });
101
102 it('laesst ohne Vorgabe den Regelwert gelten', () => {
103 const plan = buildSignalPlan(mitHoechstfreigabezeit({}));
104
105 expect(plan.phases.map((p) => p.duration)).toEqual([61, 39]);
106 expect(plan.notes.some((n) => n.code === 'hoechstfreigabezeit-unter-mindestfreigabezeit')).toBe(
107 false,
108 );
109 });
110
111 it('nennt bei einer festen Freigabezeit der Phase diese als massgebend', () => {
112 // Nachtrag: Die Phase traegt 60 s von Hand, die Signalgruppe eine
113 // Hoechstfreigabezeit von 3 s. Massgebend sind die 60 s - der Satz
114 // "Massgebend bleibt die Mindestfreigabezeit" (5 s) war hier falsch und
115 // lief ueber den Bericht in die gedruckte Planunterlage.
116 const projekt = mitHoechstfreigabezeit({ K1: 3 }, 60);
117 const plan = buildSignalPlan(projekt);
118
119 expect(plan.phases[0]?.duration).toBe(60);
120 expect(plan.notes.some((n) => n.code === 'hoechstfreigabezeit-unter-mindestfreigabezeit')).toBe(
121 false,
122 );
123 const notiz = plan.notes.find(
124 (n) => n.code === 'hoechstfreigabezeit-unter-fester-freigabezeit',
125 );
126 expect(notiz?.severity).toBe('warnung');
127 expect(notiz?.message).toContain('K1');
128 expect(notiz?.message).toContain('60 s');
129 expect(notiz?.message).not.toContain('Mindestfreigabezeit');
130
131 const befund = validateProject(projekt, plan).findings.find(
132 (f) => f.rule === 'signalplan.hoechstfreigabezeit-unter-fester-freigabezeit',
133 );
134 expect(befund?.title).not.toBe('Hinweis aus dem Planaufbau');
135 expect(befund?.category).toBe('signalgruppen');
136 });
137
138 it('meldet die Vorgabe auch dann, wenn sie zwischen Mindest- und fester Freigabezeit liegt', () => {
139 // 20 s liegen ueber der Mindestfreigabezeit (5 s) und unter der festen
140 // Freigabezeit (60 s). Vor dem Nachtrag verfiel der lesbare Eintrag
141 // hier vollstaendig stumm - genau der Zustand, den die Befunde 36 und 42
142 // beanstanden.
143 const plan = buildSignalPlan(mitHoechstfreigabezeit({ K1: 20 }, 60));
144
145 expect(plan.phases[0]?.duration).toBe(60);
146 const notiz = plan.notes.find(
147 (n) => n.code === 'hoechstfreigabezeit-unter-fester-freigabezeit',
148 );
149 expect(notiz).toBeDefined();
150 expect(notiz?.message).toContain('20 s');
151 });
152
153 it('schweigt, wo die feste Freigabezeit die Vorgabe einhaelt', () => {
154 // Gegenprobe: 90 s Hoechstfreigabezeit, 60 s fest vorgegeben - die
155 // Schranke ist eingehalten, es gibt nichts zu melden.
156 const plan = buildSignalPlan(mitHoechstfreigabezeit({ K1: 90 }, 60));
157
158 expect(plan.phases[0]?.duration).toBe(60);
159 expect(plan.notes.some((n) => n.code.startsWith('hoechstfreigabezeit'))).toBe(false);
160 });
161
162 it('rechnet nach einem unlesbaren Eintrag wirklich ohne die obere Schranke', () => {
163 // Die Einlesemeldung sagt "Gerechnet wird ohne diese obere Schranke" -
164 // diese Zusage haelt jetzt, weil ein lesbarer Wert eine ist.
165 const { project, issues } = parseProject({
166 signalGroups: [
167 { id: 'k1', name: 'K1', mode: 'kfz', maxGreenOverride: 'lang' },
168 { id: 'k2', name: 'K2', mode: 'kfz' },
169 ],
170 phases: [{ id: 'p1', name: 'Phase 1', signalGroupIds: ['k1'] }],
171 });
172
173 expect(project.signalGroups[0]!.maxGreenOverride).toBeNull();
174 expect(
175 issues.some((i) => i.path === 'signalGroups[0].maxGreenOverride' && i.severity === 'warnung'),
176 ).toBe(true);
177 expect(
178 buildSignalPlan(project).notes.some((n) => n.code.startsWith('hoechstfreigabezeit')),
179 ).toBe(false);
180 });
181 });