lsa-planer

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

/ tests domain verkehrsstaerkeNull.test.ts

6,0 KB Rohdatei
tests/domain/verkehrsstaerkeNull.test.ts — 127 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 * as actions from '@/app/actions';
6 import type { Project } from '@/domain/model/project';
7
8 /*
9 * Befunde 12 und 18, behoben in Fassung 5.10.0 - ein Eintrag ohne
10 * Verkehrsstaerke galt als gezaehlt.
11 *
12 * `setDemand` und `setLastzugAnteil` legen bei fehlendem Datensatz einen mit
13 * `volume: 0` an - das geschieht schon, wenn in der Signalgruppentabelle nur
14 * die Spalte "SV-Anteil" oder "Lastzuege am SV" ausgefuellt wird. Der Fachkern
15 * entschied an zwei Stellen allein am VORHANDENSEIN des Datensatzes:
16 * - `phaseFlowRatio` setzte `erfasst = true` und lieferte 0 statt
17 * `undefined`; die Luecke 'saettigungsgrade-unvollstaendig' entstand nicht.
18 * - `computeCapacity` bekam die 0 als Nachfrage und lieferte einen gesetzten
19 * `degreeOfSaturation`; damit fiel auch
20 * 'leistungsfaehigkeit.keine-verkehrsstaerken' weg.
21 * Y blieb trotzdem zu klein, die Umlaufzeit fiel zu kurz aus - die gefaehrliche
22 * Richtung, und im teilweise erfassten Fall ohne jede Meldung.
23 *
24 * Massstab ist das Kriterium, das der Fachkern sonst durchgaengig fuehrt
25 * (rules.ts, simulation.ts, naechsterSchritt.ts): erfasst ist eine
26 * Verkehrsstaerke nur bei `Number.isFinite(volume) && volume > 0`.
27 *
28 * GEGEN DEN ALTSTAND schlagen die ersten fuenf Faelle fehl. Der sechste ist die
29 * Gegenprobe mit einer echten Verkehrsstaerke und besteht auch dort.
30 */
31
32 const DATUM = new Date('2026-01-01T00:00:00Z');
33 const LUECKE = 'saettigungsgrade-unvollstaendig';
34 const REGEL = 'leistungsfaehigkeit.keine-verkehrsstaerken';
35
36 /** Standardknotenpunkt, dem die Verkehrsstaerken der zweiten Phase fehlen. */
37 function ohneVerkehrsstaerkenInPhase2(): Project {
38 const roh = createStandardIntersectionProject('Teilweise erfasst', DATUM);
39 const k3 = roh.signalGroups.find((g) => g.name === 'K3')!;
40 const k4 = roh.signalGroups.find((g) => g.name === 'K4')!;
41 return {
42 ...roh,
43 demands: roh.demands.filter((d) => d.signalGroupId !== k3.id && d.signalGroupId !== k4.id),
44 };
45 }
46
47 /** Derselbe Knotenpunkt ohne jede Verkehrsstaerke. */
48 function ohneJedeVerkehrsstaerke(): Project {
49 return { ...createStandardIntersectionProject('Ohne Zaehlung', DATUM), demands: [] };
50 }
51
52 describe('Befunde 12/18 - ein Eintrag mit der Verkehrsstaerke 0 ist keine Zaehlung', () => {
53 it('meldet die Luecke weiter, wenn nur der Schwerverkehrsanteil eingetragen wird', () => {
54 const basis = ohneVerkehrsstaerkenInPhase2();
55 const k3 = basis.signalGroups.find((g) => g.name === 'K3')!;
56
57 // Genau der Bedienweg: Spalte "SV-Anteil" ausfuellen, Spalte
58 // "Verkehrsstaerke" leer lassen.
59 const mitAnteil = actions.setDemand(basis, k3.id, { heavyVehicleShare: 0.05 });
60 expect(mitAnteil.demands.some((d) => d.signalGroupId === k3.id && d.volume === 0)).toBe(true);
61
62 const notiz = buildSignalPlan(mitAnteil).notes.find((n) => n.code === LUECKE);
63 expect(notiz).toBeDefined();
64 expect(notiz?.message).toContain('Phase 2');
65 });
66
67 it('meldet die Luecke weiter, wenn nur der Lastzuganteil eingetragen wird', () => {
68 const basis = ohneVerkehrsstaerkenInPhase2();
69 const k3 = basis.signalGroups.find((g) => g.name === 'K3')!;
70
71 const mitAnteil = actions.setLastzugAnteil(basis, k3.id, 0.3);
72
73 expect(buildSignalPlan(mitAnteil).notes.some((n) => n.code === LUECKE)).toBe(true);
74 });
75
76 it('bildet ohne Verkehrsstaerke keinen Auslastungsgrad', () => {
77 const basis = ohneVerkehrsstaerkenInPhase2();
78 const k3 = basis.signalGroups.find((g) => g.name === 'K3')!;
79 const plan = buildSignalPlan(actions.setDemand(basis, k3.id, { heavyVehicleShare: 0.05 }));
80
81 const geplant = plan.groups.find((g) => g.groupId === k3.id)!;
82 // Ein aus dem Nichts gebildeter Auslastungsgrad 0,00 ist die unsichere
83 // Aussage: Er stellt die Zufahrt als voellig unbelastet dar.
84 expect(geplant.capacity?.degreeOfSaturation).toBeUndefined();
85 expect(geplant.flowRatio).toBeNull();
86 });
87
88 it('haelt die Warnung "Keine Verkehrsstaerken erfasst" aufrecht', () => {
89 let projekt = ohneJedeVerkehrsstaerke();
90 for (const gruppe of projekt.signalGroups.filter((g) => g.mode === 'kfz')) {
91 projekt = actions.setDemand(projekt, gruppe.id, { heavyVehicleShare: 0.05 });
92 }
93
94 const bericht = validateProject(projekt, buildSignalPlan(projekt));
95 expect(bericht.findings.some((f) => f.rule === REGEL)).toBe(true);
96 });
97
98 it('behandelt eine von Hand eingetragene 0 genauso', () => {
99 // Das Programm kann "gezaehlt und 0 Fz/h" von "nicht erfasst" nicht
100 // unterscheiden - beide Wege erzeugen denselben Datensatz. Eine
101 // massgebende Kfz-Verkehrsstaerke von 0 ist kein Bemessungsfall; im
102 // Zweifel nach der Zaehlung zu fragen ist die sichere Richtung.
103 const basis = ohneVerkehrsstaerkenInPhase2();
104 const k3 = basis.signalGroups.find((g) => g.name === 'K3')!;
105
106 const plan = buildSignalPlan(actions.setDemand(basis, k3.id, { volume: 0 }));
107
108 expect(plan.notes.some((n) => n.code === LUECKE)).toBe(true);
109 });
110
111 it('schweigt, sobald eine Verkehrsstaerke erfasst ist', () => {
112 // Gegenprobe: Mit einer echten Zahl entsteht weder die Luecke noch die
113 // Warnung, und der Auslastungsgrad steht.
114 const basis = ohneVerkehrsstaerkenInPhase2();
115 const k3 = basis.signalGroups.find((g) => g.name === 'K3')!;
116 const k4 = basis.signalGroups.find((g) => g.name === 'K4')!;
117 let projekt = actions.setDemand(basis, k3.id, { volume: 410, heavyVehicleShare: 0.03 });
118 projekt = actions.setDemand(projekt, k4.id, { volume: 390, heavyVehicleShare: 0.03 });
119
120 const plan = buildSignalPlan(projekt);
121 expect(plan.notes.some((n) => n.code === LUECKE)).toBe(false);
122 expect(
123 plan.groups.find((g) => g.groupId === k3.id)?.capacity?.degreeOfSaturation,
124 ).toBeGreaterThan(0);
125 expect(validateProject(projekt, plan).findings.some((f) => f.rule === REGEL)).toBe(false);
126 });
127 });