lsa-planer

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

/ tests export matrixVerfahrenssatz.test.ts

11,0 KB Rohdatei
tests/export/matrixVerfahrenssatz.test.ts — 262 Zeilen
1 import { inflateSync } from 'node:zlib';
2 import { describe, expect, it } from 'vitest';
3 import {
4 createConflict,
5 createEmptyProject,
6 createPhase,
7 createSignalGroup,
8 createStandardIntersectionProject,
9 } from '@/domain/model/factory';
10 import { buildSignalPlan } from '@/domain/plan/signalPlan';
11 import { RILSA_ENGSTELLE } from '@/domain/rilsa/constants';
12 import { validateProject } from '@/domain/validation';
13 import { toWinAnsi } from '@/render/pdfSurface';
14 import { buildProjectPdf, DEFAULT_PDF_OPTIONS } from '@/services/export/pdf';
15 import type { Project } from '@/domain/model/project';
16 import type { PdfOptions } from '@/services/export/pdf';
17
18 /**
19 * Fassung 5.43.0: Der Satz unter der Zwischenzeitenmatrix behauptete einen
20 * Rechenansatz, der nicht gerechnet hat.
21 *
22 * DER BEFUND. Unter der Matrix stand unbedingt "Ueberfahrzeit des
23 * Kraftfahrzeugverkehrs: Gerechnet wurde mit dem Ansatz ..."
24 * (`ueberfahrzeitAnsatzSatz` aus services/export/bewertung.ts) - also auch an
25 * einer EINSTREIFIGEN Verkehrsfuehrung. Dort geht der Ansatz in keine Zahl der
26 * Matrix ein: `buildSignalPlan` waehlt fuer jeden raeumenden Kfz-Strom das
27 * Verfahren der Engstellensignalisierung (`engstelle && from.mode === 'kfz'`),
28 * und dieses setzt die Ueberfahrzeit mit RILSA_ENGSTELLE.ueberfahrzeit fest
29 * (RiLSA 2015, Abschnitt 5.2.2). Die Unterlage nannte damit eine Rechnung, die
30 * nicht stattgefunden hat - und zwar neben den Zahlen, die der Pruefer
31 * nachrechnet.
32 *
33 * WARUM NICHT EINFACH WEGLASSEN: Der Satz steht aus einem Grund unter der Matrix
34 * und nicht nur im Rechenweg (Fassung 5.5.0, Frage E2) - der
35 * Rechenweg ist abwaehlbar, und eine Matrix ohne jede Angabe zur Ueberfahrzeit
36 * laesst sich nicht nachrechnen. An seine Stelle tritt deshalb das Verfahren,
37 * das wirklich gerechnet hat. Geprueft wird beides mit ABGEWAEHLTEM Rechenweg,
38 * damit die Fussnote dort den Befund nicht verdeckt.
39 */
40
41 const STICHTAG = new Date('2026-01-01T12:00:00Z');
42
43 /** Ausdruck ohne Rechenweg - die Lage, fuer die der Satz gedacht ist. */
44 const NUR_MATRIX: PdfOptions = { ...DEFAULT_PDF_OPTIONS, includeCalculationDetails: false };
45
46 /** Sichtbaren Text aus dem PDF lesen; die Inhaltsstroeme sind komprimiert. */
47 function pdfText(bytes: Uint8Array): string {
48 const roh = new TextDecoder('latin1').decode(bytes);
49 const teile: string[] = [];
50 const suche = /stream\r?\n/g;
51 let treffer: RegExpExecArray | null;
52
53 while ((treffer = suche.exec(roh)) !== null) {
54 const start = treffer.index + treffer[0].length;
55 const ende = roh.indexOf('endstream', start);
56 if (ende < 0) continue;
57 let inhalt: string;
58 try {
59 inhalt = inflateSync(Buffer.from(roh.slice(start, ende), 'latin1')).toString('latin1');
60 } catch {
61 continue;
62 }
63 for (const t of inhalt.matchAll(/\(((?:[^()\\]|\\.)*)\)\s*Tj/gu)) {
64 teile.push(t[1]!.replace(/\\([()\\])/gu, '$1'));
65 }
66 }
67 return teile.join(' ').replace(/\s+/gu, ' ');
68 }
69
70 const gedruckt = (text: string): string => toWinAnsi(text).replace(/\s+/gu, ' ');
71
72 function unterlage(projekt: Project, options: PdfOptions = NUR_MATRIX): string {
73 const plan = buildSignalPlan(projekt);
74 const bericht = validateProject(projekt, plan, STICHTAG);
75 return pdfText(buildProjectPdf(projekt, plan, bericht, options, STICHTAG));
76 }
77
78 /** Zwei Richtungen einer Baustellenampel, 200 m Engstelle, 50 km/h. */
79 function baustelle(): Project {
80 const basis = createEmptyProject('Baustelle', STICHTAG, 'einstreifig');
81 const a = createSignalGroup({ name: 'K1', mode: 'kfz', vZul: 50, index: 0 });
82 const b = createSignalGroup({ name: 'K2', mode: 'kfz', vZul: 50, index: 1 });
83 const p1 = createPhase('Seite A', [a.id]);
84 const p2 = createPhase('Seite B', [b.id]);
85 const beziehung = (von: string, nach: string) => ({
86 ...createConflict(von, nach),
87 clearingDistance: 200,
88 enteringDistance: 0,
89 });
90 return {
91 ...basis,
92 signalGroups: [a, b],
93 conflicts: [beziehung(a.id, b.id), beziehung(b.id, a.id)],
94 phases: [p1, p2],
95 program: { ...basis.program, phaseOrder: [p1.id, p2.id] },
96 };
97 }
98
99 /** Jede Beziehung mit raeumendem Kraftfahrzeug von Hand vorgegeben. */
100 function mitHandvorgaben(projekt: Project): Project {
101 return {
102 ...projekt,
103 conflicts: projekt.conflicts.map((c) =>
104 projekt.signalGroups.find((g) => g.id === c.fromId)?.mode === 'kfz'
105 ? { ...c, manualIntergreen: 8 }
106 : c,
107 ),
108 };
109 }
110
111 function mitFestemAnsatz(projekt: Project): Project {
112 return { ...projekt, settings: { ...projekt.settings, ueberfahrzeitAnsatz: 'fest' } };
113 }
114
115 describe('Die Matrix behauptet keinen Rechenansatz, der nicht gerechnet hat', () => {
116 it('Vorbedingung: an der Engstelle rechnet wirklich das zweite Verfahren', () => {
117 const plan = buildSignalPlan(baustelle());
118 expect(
119 [...plan.intergreens.values()].every((i) => i.calculation?.verfahren === 'engstelle'),
120 'jede Beziehung nach Abschnitt 5.2.2',
121 ).toBe(true);
122 // Und die feste Ueberfahrzeit stammt nicht aus dem Ansatz.
123 expect([...plan.intergreens.values()].every((i) => i.calculation?.crossingTime === 4)).toBe(
124 true,
125 );
126 });
127
128 it('nennt unter der Matrix nicht den Kfz-Ansatz, wo er in keine Zahl eingeht', () => {
129 const text = unterlage(baustelle());
130 expect(text, 'die Matrix steht in dieser Ausgabe').toContain(gedruckt('Angaben in Sekunden'));
131 expect(text, 'der Satz, der eine Rechnung behauptet').not.toContain(
132 gedruckt('Gerechnet wurde mit dem Ansatz'),
133 );
134 });
135
136 it('nennt dafuer das Verfahren, das gerechnet hat', () => {
137 const text = unterlage(baustelle());
138 expect(text).toContain(gedruckt('Verfahren der Engstellensignalisierung'));
139 expect(text).toContain(gedruckt('RiLSA 2015, Abschnitt 5.2.2'));
140 expect(text, 'die feste Ueberfahrzeit des Abschnitts').toContain(
141 gedruckt(`tü = ${RILSA_ENGSTELLE.ueberfahrzeit} s`),
142 );
143 });
144
145 it('laesst den Knotenpunkt unveraendert', () => {
146 // Die Gegenprobe: Dort geht der Ansatz in jede Zwischenzeit eines
147 // raeumenden Kraftfahrzeugs ein, und dort gehoert der Satz hin.
148 const text = unterlage(createStandardIntersectionProject('Knotenpunkt', STICHTAG));
149 expect(text).toContain(gedruckt('Überfahrzeit des Kraftfahrzeugverkehrs: Gerechnet wurde'));
150 expect(text, 'kein Verfahren, das dort nicht gilt').not.toContain(
151 gedruckt('Verfahren der Engstellensignalisierung'),
152 );
153 });
154
155 it('nennt keinen der beiden Saetze, wo jede Kfz-Zwischenzeit vorgegeben ist', () => {
156 /*
157 * DER DRITTE FALL, den keine Anlagenart hergibt: Am Knotenpunkt gilt die
158 * Formel des Abschnitts 2, aber sie ist an keiner Beziehung gelaufen -
159 * `plan.intergreens` fuehrt dort `calculation: null`. Bis 5.42.1 stand auch
160 * hier der Ansatzsatz.
161 */
162 const projekt = mitHandvorgaben(createStandardIntersectionProject('Vorgaben', STICHTAG));
163 const plan = buildSignalPlan(projekt);
164 expect(
165 [...plan.intergreens.values()].some(
166 (i) => i.source === 'vorgegeben' && i.calculation === null,
167 ),
168 'Vorbedingung: die Vorgabe ersetzt die Rechnung',
169 ).toBe(true);
170
171 const text = unterlage(projekt);
172 expect(text).not.toContain(gedruckt('Gerechnet wurde mit dem Ansatz'));
173 expect(text, 'und kein Verfahren, das hier nicht gilt').not.toContain(
174 gedruckt('Verfahren der Engstellensignalisierung'),
175 );
176 expect(text, 'die Sternfussnote sagt, woher die Zahlen kommen').toContain(
177 gedruckt('Mit Stern gekennzeichnete Werte sind von Hand vorgegeben'),
178 );
179 });
180
181 it('haelt beide Aussagen auch im vollen Ausdruck auseinander', () => {
182 // Mit Rechenweg steht die Fussnote zum Engstellenverfahren ohnehin da; der
183 // Kfz-Ansatz darf auch dann nirgends als gerechnet gelten.
184 const text = unterlage(baustelle(), DEFAULT_PDF_OPTIONS);
185 expect(text).not.toContain(gedruckt('Gerechnet wurde mit dem Ansatz'));
186 expect(text, 'die Fussnote des Rechenwegs bleibt').toContain(
187 gedruckt('gilt für den Kraftfahrzeugverkehr das eigene Verfahren'),
188 );
189 });
190 });
191
192 /**
193 * Dieselbe Frage wird an drei Stellen EINES Ausdrucks gestellt, und sie darf
194 * nicht dreierlei beantwortet werden.
195 *
196 * Die Eingrenzung des Matrixabsatzes widerlegte zwei Zusagen, die stehen
197 * blieben: die Deckblattzeile "Überfahrzeit Kfz (Rechenansatz)", die den Ansatz
198 * unbedingt nannte und fuer die Begruendung auf genau den Absatz verwies, den
199 * die Unterlage nun nicht mehr druckt, und die Tafel "Abweichungen von den
200 * Regelwerten", die fuer eine Wahl ohne Wirkung samt Richtungssatz eine
201 * Begruendung verlangte - waehrend ihr eigener Einleitungsabsatz seit Befund C20
202 * festhaelt, dass ein Kennwert ohne Wirkung keine zu begruendende Abweichung
203 * ist.
204 */
205 describe('Deckblatt und Abweichungstafel sagen dasselbe wie die Matrix', () => {
206 it('setzt an der Engstelle den Wirkungsvermerk neben die Deckblattzeile', () => {
207 // EIN Ausdruck, beide Stellen: Deckblatt und Matrixabschnitt duerfen nicht
208 // Verschiedenes sagen - das war der Befund.
209 const text = unterlage(baustelle(), DEFAULT_PDF_OPTIONS);
210 expect(text, 'die Zeile bleibt - die Einstellung steht ja da').toContain(
211 gedruckt('Überfahrzeit Kfz (Rechenansatz)'),
212 );
213 expect(text, 'und sagt, dass sie hier nichts bewirkt').toContain(
214 gedruckt('ohne Wirkung an dieser Anlage'),
215 );
216 expect(text, 'unter der Matrix steht dasselbe').toContain(
217 gedruckt('Verfahren der Engstellensignalisierung'),
218 );
219 expect(text, 'und nirgends eine Rechnung, die nicht stattfand').not.toContain(
220 gedruckt('Gerechnet wurde mit dem Ansatz'),
221 );
222 });
223
224 it('laesst die Deckblattzeile am Knotenpunkt unvermerkt', () => {
225 const text = unterlage(
226 createStandardIntersectionProject('Knotenpunkt', STICHTAG),
227 DEFAULT_PDF_OPTIONS,
228 );
229 expect(text).toContain(gedruckt('Überfahrzeit Kfz (Rechenansatz)'));
230 expect(text, 'dort wirkt der Ansatz in jede Kfz-Zwischenzeit').not.toContain(
231 gedruckt('ohne Wirkung an dieser Anlage'),
232 );
233 });
234
235 it('nennt die Handvorgabe als Grund, wo sie jede Kfz-Zwischenzeit ersetzt', () => {
236 const text = unterlage(
237 mitHandvorgaben(createStandardIntersectionProject('Vorgaben', STICHTAG)),
238 DEFAULT_PDF_OPTIONS,
239 );
240 expect(text).toContain(gedruckt('ohne Wirkung an dieser Anlage: Kfz-Zwischenzeiten von Hand'));
241 });
242
243 it('verlangt an der Engstelle keine Begruendung fuer den festen Ansatz', () => {
244 const text = unterlage(mitFestemAnsatz(baustelle()), DEFAULT_PDF_OPTIONS);
245 expect(text, 'die Tafel fuehrt die Wahl weiter auf').toContain(
246 gedruckt('Überfahrzeit des Kraftfahrzeugverkehrs (Rechenansatz, kein Kennwert)'),
247 );
248 expect(text).toContain(gedruckt('ohne Wirkung an dieser Anlage'));
249 expect(text, 'eine Fehlerrichtung ohne Rechnung gibt es nicht').not.toContain(
250 gedruckt('VERKÜRZT die Zwischenzeiten'),
251 );
252 });
253
254 it('nennt am Knotenpunkt weiter die Fehlerrichtung des festen Ansatzes', () => {
255 const text = unterlage(
256 mitFestemAnsatz(createStandardIntersectionProject('Knotenpunkt', STICHTAG)),
257 DEFAULT_PDF_OPTIONS,
258 );
259 expect(text).toContain(gedruckt('VERKÜRZT die Zwischenzeiten'));
260 expect(text).not.toContain(gedruckt('ohne Wirkung an dieser Anlage'));
261 });
262 });