lsa-planer

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

/ tests export pdfPruefstaende.test.ts

9,7 KB Rohdatei
tests/export/pdfPruefstaende.test.ts — 232 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 { buildSignalPlan } from '@/domain/plan/signalPlan';
5 import { validateProject } from '@/domain/validation';
6 import { createStandardIntersectionProject } from '@/domain/model/factory';
7 import { ANLAGENART_GRENZEN } from '@/domain/rilsa/constants';
8 import { settingFieldsByQuelle } from '@/domain/rilsa/settings';
9 import {
10 PRUEFSTAND_ERLAEUTERUNG,
11 PRUEFSTAND_LABELS,
12 PRUEFSTAND_REIHENFOLGE,
13 QUELLEN,
14 } from '@/domain/rilsa/quellen';
15 import { toWinAnsi } from '@/render/pdfSurface';
16 import type { Anlagenart, Project } from '@/domain/model/project';
17 import * as fmt from '@/ui/format';
18
19 /*
20 * Befunde B-1 und B-2: EIN Ausdruck, EINE Aussage ueber denselben Zahlenwert.
21 *
22 * B-1: Das Fundstellenverzeichnis fuehrte alle vier Umlaufzeitfelder unter der
23 * Quelle 'rilsa-umlaufzeit' mit dem Pruefstand "gesichert" und ohne Hinweis.
24 * Gedruckt stand damit "Regelbereich Umlaufzeit von 60 s ... gesichert",
25 * waehrend die Tabelle "Schranken der Anlagenart" zwei Abschnitte vorher im
26 * SELBEN Dokument "abzugleichen" auswies - an der Fussgaengerschutzanlage sogar
27 * fuer einen ganz anderen Bereich (40 bis 70 s). Genau die 60 s, um die die
28 * Frage E4 (Fassung 5.4.0) geht, standen weiter als gesicherter RiLSA-Wert in der
29 * Unterlage.
30 *
31 * B-2: Die gedruckte Legende erklaerte "abzugleichen" allein als "am Original
32 * zu pruefen" - fuer eine Schwelle, die dieses Programm selbst setzt, gibt es
33 * kein Original.
34 *
35 * Dieser Test haelt die Pruefstaende BEIDER Tabellen desselben PDF
36 * gegeneinander; ein Test dieser Art fehlte.
37 */
38
39 const STICHTAG = new Date('2026-01-01T12:00:00Z');
40
41 const ANLAGENARTEN: readonly Anlagenart[] = [
42 'knotenpunkt',
43 'fussgaengerschutzanlage',
44 'einstreifig',
45 ];
46
47 /** Sichtbaren Text aus dem PDF lesen; die Inhaltsstroeme sind komprimiert. */
48 function pdfText(bytes: Uint8Array): string {
49 const roh = new TextDecoder('latin1').decode(bytes);
50 const teile: string[] = [];
51 const suche = /stream\r?\n/g;
52 let treffer: RegExpExecArray | null;
53
54 while ((treffer = suche.exec(roh)) !== null) {
55 const start = treffer.index + treffer[0].length;
56 const ende = roh.indexOf('endstream', start);
57 if (ende < 0) continue;
58 let inhalt: string;
59 try {
60 inhalt = inflateSync(Buffer.from(roh.slice(start, ende), 'latin1')).toString('latin1');
61 } catch {
62 continue;
63 }
64 for (const t of inhalt.matchAll(/\(((?:[^()\\]|\\.)*)\)\s*Tj/g)) {
65 teile.push(t[1]!.replace(/\\([()\\])/g, '$1'));
66 }
67 }
68 return teile.join(' ').replace(/\s+/g, ' ');
69 }
70
71 function projekt(anlagenart: Anlagenart): Project {
72 const basis = createStandardIntersectionProject('Hauptstrasse / Bahnhofstrasse', STICHTAG);
73 return { ...basis, anlagenart };
74 }
75
76 function ausdruck(project: Project): string {
77 const plan = buildSignalPlan(project);
78 const bericht = validateProject(project, plan, STICHTAG);
79 return pdfText(buildProjectPdf(project, plan, bericht, DEFAULT_PDF_OPTIONS, STICHTAG));
80 }
81
82 const gedruckt = (text: string): string => toWinAnsi(text);
83
84 /**
85 * Die erste Zeile einer Feldgruppe des Fundstellenverzeichnisses, so wie sie
86 * gedruckt wird: Regelwerk, Kennwert, Regelwert, Sachgebiet, eigene Fundstelle
87 * und Pruefstand. Nur diese erste Zeile traegt den Pruefstand der Gruppe.
88 *
89 * NACHGEZOGEN (Fassung 5.12.0): Die Spalte "Regelwert" verglich
90 * hier gegen `feld.standard`, also gegen den Knotenpunktwert aus
91 * RILSA_DEFAULTS. Das Verzeichnis liest sie seither ueber
92 * `fundstellenkennwerte` aus derselben Quelle wie die Aufstellung
93 * "Abweichungen von den Regelwerten" und druckt an einer
94 * Fussgaengerschutzanlage 40 s. Der erwartete Regelwert kommt deshalb von der
95 * Aufrufstelle - dort aus `ANLAGENART_GRENZEN` und damit aus einer von der
96 * Umsetzung unabhaengigen Zahl. Wer den Regelwert kuenftig woanders herholt,
97 * merkt es an dieser Zeile.
98 */
99 function verzeichniszeile(project: Project, schluessel: string, regelwert?: number): string {
100 const plan = buildSignalPlan(project);
101 const gruppe = settingFieldsByQuelle(plan.defaults, plan.anlagenart).find(
102 (g) => g.schluessel === schluessel,
103 );
104 expect(gruppe, schluessel).toBeDefined();
105 const feld = gruppe!.felder[0]!;
106 /*
107 * Die Fundstellenspalte: seit 5.27.0 die im Programm hinterlegte
108 * Abschnittsnummer, wo es eine gibt, sonst weiterhin "nicht eingetragen".
109 * Diese Aufstellung traegt keine eigene Fundstelle des Anwenders.
110 */
111 const fundstelle = gruppe!.quelle.fundstelle ?? 'nicht eingetragen';
112 return (
113 `${gruppe!.quelle.kurz} ${feld.label} ${fmt.numShort(regelwert ?? feld.standard, 3)} ${feld.unit} ` +
114 `${gruppe!.quelle.sachgebiet} ${fundstelle} ${PRUEFSTAND_LABELS[gruppe!.quelle.pruefstand]}`
115 );
116 }
117
118 describe('B-1 - Fundstellenverzeichnis und Schrankentabelle sagen dasselbe', () => {
119 it('fuehrt den Regelbereich in beiden Tabellen mit demselben Pruefstand', () => {
120 for (const art of ANLAGENARTEN) {
121 const grenzen = ANLAGENART_GRENZEN[art];
122 const text = ausdruck(projekt(art));
123
124 // Schrankentabelle: Angesetzt - Grundlage - Pruefstand.
125 const schranke =
126 `${fmt.seconds(grenzen.cycleTime.recommendedMin)} bis ` +
127 `${fmt.seconds(grenzen.cycleTime.recommendedMax)} ${grenzen.herkunft.regelbereich.text} ` +
128 `${PRUEFSTAND_LABELS[grenzen.herkunft.regelbereich.pruefstand]}`;
129 expect(text, art).toContain(gedruckt(schranke));
130
131 /*
132 * Fundstellenverzeichnis: NIE zuversichtlicher als die Zeile der
133 * Anlagenart.
134 *
135 * Bis 5.26.0 stand hier "derselbe Pruefstand" - richtig, solange alle
136 * drei Anlagenarten denselben trugen. Seit 5.27.0 ist der Regelbereich
137 * am Knotenpunkt belegt (RiLSA 2015, Abschnitt 2.6), an der
138 * Fussgaengerschutzanlage und bei einstreifiger Fuehrung nach wie vor
139 * nicht. EINE Quelle kann das nicht in drei Staenden ausdruecken; sie
140 * traegt deshalb den schwaechsten. Was B-1 verhindern sollte, bleibt
141 * verhindert: dass das Verzeichnis MEHR behauptet als die
142 * Schrankentabelle.
143 */
144 const quelle = PRUEFSTAND_REIHENFOLGE.indexOf(
145 QUELLEN['rilsa-umlaufzeit-regelbereich'].pruefstand,
146 );
147 const zeile = PRUEFSTAND_REIHENFOLGE.indexOf(grenzen.herkunft.regelbereich.pruefstand);
148 expect(
149 quelle,
150 `${art}: das Verzeichnis behauptet mehr als die Schrankentabelle`,
151 ).toBeGreaterThanOrEqual(zeile);
152 expect(text, art).toContain(
153 gedruckt(
154 verzeichniszeile(
155 projekt(art),
156 'rilsa-umlaufzeit-regelbereich',
157 grenzen.cycleTime.recommendedMin,
158 ),
159 ),
160 );
161 }
162 });
163
164 it('nennt den Regelbereich in keiner Anlagenart mehr gesichert', () => {
165 for (const art of ANLAGENARTEN) {
166 const text = ausdruck(projekt(art));
167 expect(text, art).not.toContain(
168 gedruckt(
169 'Regelbereich Umlaufzeit von 60 s Umlaufzeit und Freigabezeitverteilung nicht eingetragen gesichert',
170 ),
171 );
172 // Auch nicht ueber das neue Sachgebiet.
173 expect(text, art).not.toContain(
174 gedruckt('Umlaufzeit: empfohlener Bereich nicht eingetragen gesichert'),
175 );
176 }
177 });
178
179 it('sagt beim zulaessigen Bereich, was belegt ist und was fuer DIESEN Plan gilt', () => {
180 // Die 30 bis 120 s sind der Regelwert der Vorgabenfelder - die
181 // Umlaufzeitzeile der RiLSA fuer den Knotenpunkt. Bei einer einstreifigen
182 // Fuehrung gilt fuer den Plan etwas anderes, und das muss dabeistehen,
183 // sonst stehen 120 s und 300 s unverbunden nebeneinander.
184 const hinweis = QUELLEN['rilsa-umlaufzeit'].hinweis;
185 expect(hinweis).toBeDefined();
186 for (const art of ANLAGENARTEN) {
187 const text = ausdruck(projekt(art));
188 expect(text, art).toContain(gedruckt(`Zu beachten: ${hinweis!}`));
189 }
190 expect(hinweis!).toContain('30 bis 120 s am Knotenpunkt');
191 expect(hinweis!).toContain('Schranken der Anlagenart');
192 });
193
194 it('nennt beim Regelbereich, was belegt ist und was dieses Programm setzt', () => {
195 /*
196 * NACHGEZOGEN (5.27.0): Bis dahin hiess dieser Fall "die halbe Beleglage
197 * der Frage E4" (Fassung 5.4.0) und verlangte die Saetze "Obergrenze von
198 * 90 s am Knotenpunkt" und "60 s als Untergrenze". Am Knotenpunkt sind seit
199 * dem Abgleich beide Grenzen belegt; offen bleiben die Bereiche der beiden
200 * anderen Anlagenarten, und genau das muss der Hinweis sagen.
201 */
202 const hinweis = QUELLEN['rilsa-umlaufzeit-regelbereich'].hinweis;
203 expect(hinweis).toBeDefined();
204 expect(hinweis!).toContain('minimal 30 s und maximal 90 (120) s');
205 expect(hinweis!).toContain('setzt dieses Programm den Bereich weiterhin selbst');
206 for (const art of ANLAGENARTEN) {
207 expect(ausdruck(projekt(art)), art).toContain(gedruckt(`Zu beachten: ${hinweis!}`));
208 }
209 });
210 });
211
212 describe('B-2 - die Legende des Pruefstands kennt beide Faelle', () => {
213 it('druckt sie aus derselben Erlaeuterung, die der Bildschirm zeigt', () => {
214 const text = ausdruck(projekt('knotenpunkt'));
215 for (const stand of PRUEFSTAND_REIHENFOLGE) {
216 expect(text, stand).toContain(
217 gedruckt(`Prüfstand "${PRUEFSTAND_LABELS[stand]}": ${PRUEFSTAND_ERLAEUTERUNG[stand]}`),
218 );
219 }
220 });
221
222 it('schickt den Pruefer bei selbst gesetzten Schwellen nicht mehr an ein Regelwerk', () => {
223 expect(PRUEFSTAND_ERLAEUTERUNG.abzugleichen).toContain('Straßenverkehrsbehörde');
224 expect(PRUEFSTAND_ERLAEUTERUNG.abzugleichen).toContain('Schwelle dieses Programms');
225 const text = ausdruck(projekt('einstreifig'));
226 expect(text).toContain(gedruckt('gibt es kein Original'));
227 // Der alte Wortlaut sagte nur die eine Haelfte.
228 expect(text).not.toContain(
229 gedruckt('"abzugleichen": praxisüblich, aber von Ausgabe oder Anwendungsfall abhängig'),
230 );
231 });
232 });