lsa-planer

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

/ tests export verkehrsstaerkeNull.test.ts

8,8 KB Rohdatei
tests/export/verkehrsstaerkeNull.test.ts — 204 Zeilen
1 import { inflateSync } from 'node:zlib';
2 import { describe, expect, it } from 'vitest';
3 import { buildProjectPdf, DEFAULT_PDF_OPTIONS } from '@/services/export/pdf';
4 import { signalGroupsCsv } from '@/services/export/csv';
5 import { buildSignalPlan } from '@/domain/plan/signalPlan';
6 import { validateProject } from '@/domain/validation';
7 import { createStandardIntersectionProject } from '@/domain/model/factory';
8 import { toWinAnsi } from '@/render/pdfSurface';
9 import type { Project, TrafficDemand } from '@/domain/model/project';
10
11 /*
12 * Fassung 5.9.0 - die Verkehrsstaerke 0 im Ausdruck und in der Tabellenausgabe.
13 *
14 * Seit den Befunden 12, 18 und 58 gilt im Fachkern durchgaengig: Ein DATENSATZ
15 * ist noch keine Zaehlung. `actions.setDemand` und `actions.setLastzugAnteil`
16 * legen einen Datensatz mit `volume: 0` an, sobald in der
17 * Signalgruppentabelle nur die Spalte "SV-Anteil" ausgefuellt wird;
18 * `erfassteVerkehrsstaerke` (signalPlan.ts) liest `volume <= 0` deshalb als
19 * "keine Angabe", der Bildschirm zeichnet das Feld leer, und der
20 * Auslastungsgrad steht als "-" da.
21 *
22 * Der Ausdruck zog nicht mit: Die Spalte "Verkehrsstärke" prueft nur
23 * `demand === undefined` und druckte "0 Fz/h" - waehrend die
24 * Schwerverkehrsspalte derselben Zeile (schwerverkehrZelle, `demand.volume
25 * <= 0`) einen Gedankenstrich setzt. In derselben Zeile stand damit eine
26 * gezaehlte 0 neben zwei Strichen, und Bildschirm und Unterlage sagten
27 * sichtbar Verschiedenes. Die Lage ist aelter als Befund 58: Sie entsteht
28 * schon, wenn nur der Schwerverkehrsanteil erfasst ist.
29 *
30 * UND DIE TABELLENAUSGABE ZOG NACH (Fassung 5.10.0): Sie schrieb weiter "0",
31 * waehrend der Ausdruck an allen drei Stellen einen Gedankenstrich setzte.
32 * Vorher sagten beide Ausgaben dasselbe (beide falsch), danach widersprachen
33 * sie einander - und nebeneinandergelegt werden sie, um verglichen zu werden.
34 * In der Tabellenausgabe bleibt die Zelle LEER: dieselbe Schreibweise wie bei
35 * jeder Groesse, die sie fuer eine Zeile nicht bildet.
36 *
37 * MUTATIONSNACHWEIS, nachgemessen an zwei zurueckgesetzten Staenden:
38 *
39 * - Beide Bedingungen des Ausdrucks auf `demand === undefined`
40 * zurueckgesetzt: 2 von 5 Faellen fallen - der erste und der zweite.
41 * - Die Zelle der Tabellenausgabe auf `nachfrage ? ... : ''` zurueckgesetzt:
42 * 1 von 5 faellt - der vierte.
43 *
44 * Der dritte ("lässt eine erfasste Verkehrsstärke unverändert stehen") und der
45 * fuenfte (die Rechenwegtabelle KANN nicht betroffen sein) sind in allen
46 * Staenden gruen; sie bewachen die Gegenrichtung.
47 *
48 * BERICHTIGT (Fassung 5.10.0): Hier stand "gegen den Altstand
49 * fallen die drei ersten Faelle" - gemessen fallen zwei; der dritte ist in
50 * beiden Staenden gruen.
51 */
52
53 const STICHTAG = new Date('2026-01-01T12:00:00Z');
54 const OHNE_ZAEHLUNG = 'K-NULL';
55
56 /** Sichtbaren Text aus dem PDF, Stueck fuer Stueck (eine Tabellenzelle je Stueck). */
57 function pdfStuecke(bytes: Uint8Array): string[] {
58 const roh = new TextDecoder('latin1').decode(bytes);
59 const teile: string[] = [];
60 const suche = /stream\r?\n/g;
61 let treffer: RegExpExecArray | null;
62
63 while ((treffer = suche.exec(roh)) !== null) {
64 const start = treffer.index + treffer[0].length;
65 const ende = roh.indexOf('endstream', start);
66 if (ende < 0) continue;
67 let inhalt: string;
68 try {
69 inhalt = inflateSync(Buffer.from(roh.slice(start, ende), 'latin1')).toString('latin1');
70 } catch {
71 continue;
72 }
73 for (const t of inhalt.matchAll(/\(((?:[^()\\]|\\.)*)\)\s*Tj/g)) {
74 teile.push(t[1]!.replace(/\\([()\\])/g, '$1'));
75 }
76 }
77 return teile;
78 }
79
80 function ausdruck(project: Project): string[] {
81 const plan = buildSignalPlan(project);
82 return pdfStuecke(
83 buildProjectPdf(
84 project,
85 plan,
86 validateProject(project, plan, STICHTAG),
87 DEFAULT_PDF_OPTIONS,
88 STICHTAG,
89 ),
90 );
91 }
92
93 /**
94 * Knotenpunkt, an dessen erster Kfz-Gruppe NUR der Schwerverkehrsanteil erfasst
95 * ist - der Datensatz traegt `volume: 0`. Genau das legt die Oberflaeche an,
96 * wenn in der Signalgruppentabelle allein die Spalte "SV-Anteil" ausgefuellt
97 * wird.
98 */
99 function nurSchwerverkehrsanteil(): Project {
100 const basis = createStandardIntersectionProject('Verkehrsstärke', STICHTAG);
101 return {
102 ...basis,
103 signalGroups: basis.signalGroups.map((g, index) =>
104 index === 0 ? { ...g, name: OHNE_ZAEHLUNG } : g,
105 ),
106 demands: basis.demands.map((d, index): TrafficDemand =>
107 index === 0 ? { ...d, volume: 0, heavyVehicleShare: 0.12 } : d,
108 ),
109 };
110 }
111
112 describe('Eine Verkehrsstärke von 0 gilt in Ausdruck und Tabellenausgabe nicht als Zählung', () => {
113 it('setzt in der Leistungstabelle einen Gedankenstrich statt "0 Fz/h"', () => {
114 const projekt = nurSchwerverkehrsanteil();
115 expect(projekt.demands[0]!.volume, 'Vorbedingung: der Datensatz trägt 0').toBe(0);
116 const stuecke = ausdruck(projekt);
117
118 // Die Zeile der Leistungstabelle: Name, Saettigungsverkehrsstaerke,
119 // Abflusszeit, Kapazitaet, Verkehrsstaerke.
120 const zeile = stuecke.findIndex(
121 (s, i) => s === OHNE_ZAEHLUNG && (stuecke[i + 1] ?? '').endsWith('Fz/h'),
122 );
123 expect(zeile, 'Vorbedingung: die Gruppe steht in der Leistungstabelle').toBeGreaterThan(0);
124 // Der Gedankenstrich, wie ihn der Zeichenvorrat des PDF traegt.
125 expect(stuecke[zeile + 4]).toBe(toWinAnsi('–'));
126 });
127
128 it('druckt nirgends in der Unterlage "0 Fz/h"', () => {
129 const stuecke = ausdruck(nurSchwerverkehrsanteil());
130
131 // Auch die Signalgruppentabelle fuehrt eine Spalte "Verkehrsstärke"; sie
132 // ist dieselbe Aussage und derselbe Befund.
133 expect(stuecke).not.toContain('0 Fz/h');
134 });
135
136 it('lässt eine erfasste Verkehrsstärke unverändert stehen', () => {
137 const projekt = nurSchwerverkehrsanteil();
138 const stuecke = ausdruck(projekt);
139
140 // Kein pauschales Ausblenden: Die uebrigen Stroeme behalten ihre Zahl.
141 const uebrige = projekt.demands.slice(1).filter((d) => d.volume > 0);
142 expect(uebrige.length, 'Vorbedingung: es gibt gezählte Ströme').toBeGreaterThan(0);
143 for (const d of uebrige) {
144 expect(stuecke, String(d.volume)).toContain(
145 `${new Intl.NumberFormat('de-DE').format(d.volume)} Fz/h`,
146 );
147 }
148 });
149
150 it('lässt die Zelle auch in der Tabellenausgabe leer', () => {
151 /*
152 * Nachgezogen (Fassung 5.10.0): Der Ausdruck setzte seit der Behebung an
153 * allen drei Stellen einen Gedankenstrich, die Tabellenausgabe schrieb
154 * weiter "0". Vorher sagten beide dasselbe (beide falsch), danach
155 * widersprachen sie einander - und wer beide nebeneinanderlegt, tut das, um
156 * sie zu vergleichen.
157 *
158 * LEER UND NICHT "–": Dieselbe Schreibweise wie in der Nachbarspalte
159 * "Lastzuganteil ... (leer = nicht erfasst)" und wie bei jeder Groesse,
160 * die diese Ausgabe fuer eine Zeile nicht bildet. Ein Gedankenstrich waere
161 * in einer Zahlenspalte Text.
162 */
163 const projekt = nurSchwerverkehrsanteil();
164 const zeilen = signalGroupsCsv(projekt, buildSignalPlan(projekt))
165 .replace(/^/, '')
166 .split('\r\n')
167 .filter((z) => z.length > 0)
168 .map((z) => (z.match(/("([^"]|"")*"|[^;]*)(;|$)/g) ?? []).map((f) => f.replace(/;$/, '')));
169 const spalte = zeilen[0]!.indexOf('Verkehrsstärke [Fz/h]');
170
171 expect(spalte, 'Vorbedingung: die Spalte steht in der Ausgabe').toBeGreaterThan(0);
172 expect(zeilen.slice(1).find((z) => z[0] === OHNE_ZAEHLUNG)![spalte]).toBe('');
173 // Kein pauschales Leeren: Die gezaehlten Stroeme behalten ihre Zahl.
174 const gezaehlt = projekt.demands.filter((d) => d.volume > 0);
175 expect(gezaehlt.length, 'Vorbedingung: es gibt gezählte Ströme').toBeGreaterThan(0);
176 for (const d of gezaehlt) {
177 const name = projekt.signalGroups.find((g) => g.id === d.signalGroupId)!.name;
178 const zeile = zeilen.slice(1).find((z) => z[0] === name);
179 if (zeile === undefined) continue;
180 expect(zeile[spalte], name).not.toBe('');
181 }
182 });
183
184 it('führt in der Rechenwegtabelle nur Gruppen mit erfasster Verkehrsstärke', () => {
185 /*
186 * Zusammenhangspruefung zur zweiten Stelle: `drawDelayDetails` zeigt nur
187 * Gruppen mit `delay !== null`, und `delay` entsteht nur aus einer
188 * erfassten Verkehrsstaerke (signalPlan.ts, `verkehrsstaerke !== undefined
189 * && capacity !== null`). Die 0 kann dort also gar nicht ankommen - die
190 * Bedingung steht dort trotzdem, damit beide Tabellen dieselbe Aussage
191 * machen.
192 */
193 const projekt = nurSchwerverkehrsanteil();
194 const plan = buildSignalPlan(projekt);
195 const ohne = plan.groups.find((g) => g.name === OHNE_ZAEHLUNG)!;
196
197 expect(ohne.delay).toBeNull();
198 for (const gruppe of plan.groups.filter((g) => g.delay !== null)) {
199 const demand = projekt.demands.find((d) => d.signalGroupId === gruppe.groupId);
200 if (demand === undefined) continue;
201 expect(demand.volume, gruppe.name).toBeGreaterThan(0);
202 }
203 });
204 });