lsa-planer

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

/ tests export pdfFreigabefenster.test.ts

23,9 KB Rohdatei
tests/export/pdfFreigabefenster.test.ts — 557 Zeilen
1 import { inflateSync } from 'node:zlib';
2 import { describe, expect, it } from 'vitest';
3 import {
4 buildProjectPdf,
5 DEFAULT_PDF_OPTIONS,
6 MITTELINSEL_VERMERK,
7 furtregelAbsatz,
8 } from '@/services/export/pdf';
9 import {
10 FREIGABEFENSTER_FUSSNOTE,
11 freigabefensterZelle,
12 reportCsv,
13 signalGroupsCsv,
14 } from '@/services/export/csv';
15 import { quellenOhneFeld } from '@/services/export/bewertung';
16 import { ANFORDERUNGSBETRIEB_VORBEHALT } from '@/domain/validation/rules';
17 import { ANLAGENART_GRENZEN, RILSA_DEFAULTS, type RilsaDefaults } from '@/domain/rilsa/constants';
18 import { engstellenFolgen, umlaufBeiFreigabe } from '@/domain/rilsa/engstelle';
19 import { resolveRilsaSettings } from '@/domain/rilsa/settings';
20 import { buildSignalPlan } from '@/domain/plan/signalPlan';
21 import { validateProject } from '@/domain/validation';
22 import { createPhase, createStandardIntersectionProject } from '@/domain/model/factory';
23 import { toWinAnsi } from '@/render/pdfSurface';
24 import type { Project } from '@/domain/model/project';
25
26 /**
27 * Freigabefenster in Ausdruck und Tabellenausgabe (Befund C22), der
28 * Mittelinsel-Vermerk (Befund D2) und der Vorbehalt zum Anforderungsbetrieb
29 * (Befund D3) - alle drei in den Ausgaben geprueft, weil dort steht, was der
30 * Anwender aus der Hand gibt.
31 *
32 * C22: Die Signalzeitentabelle des Ausdrucks fuellte "Freigabe von" und
33 * "Freigabe bis" aus greens[0] - dem ersten Fenster -, waehrend "Freigabezeit"
34 * die Summe aller Fenster nannte. Eine Signalgruppe in zwei getrennten Phasen
35 * verlor damit ihr zweites Fenster, und die beiden Spalten widersprachen
36 * einander. Gegen den Altstand schlagen die Faelle fehl: Dort gab es die Spalte
37 * "Freigabefenster" weder im Ausdruck noch in der Tabellenausgabe, die
38 * Summenspalte hiess nicht Summe, und die Fussnote fehlte.
39 *
40 * D2: Der Furtregel-Absatz sagte nichts darueber, dass eine Furt mit
41 * Mittelinsel als durchgehende Furt gerechnet wird.
42 *
43 * D3: Das Programm empfahl, die Furt "auf Anforderung" freizugeben - einen
44 * Betrieb, den es selbst nicht abbildet und nicht prueft.
45 */
46
47 const FIXED_DATE = new Date('2026-01-01T12:00:00Z');
48
49 /** Sichtbaren Text aus dem PDF lesen; die Inhaltsstroeme sind komprimiert. */
50 function pdfText(bytes: Uint8Array): string {
51 const roh = new TextDecoder('latin1').decode(bytes);
52 const teile: string[] = [];
53 const suche = /stream\r?\n/g;
54 let treffer: RegExpExecArray | null;
55
56 while ((treffer = suche.exec(roh)) !== null) {
57 const start = treffer.index + treffer[0].length;
58 const ende = roh.indexOf('endstream', start);
59 if (ende < 0) continue;
60 let inhalt: string;
61 try {
62 inhalt = inflateSync(Buffer.from(roh.slice(start, ende), 'latin1')).toString('latin1');
63 } catch {
64 continue;
65 }
66 for (const stueck of inhalt.matchAll(/\((?:\\.|[^\\()])*\)\s*Tj/g)) {
67 teile.push(stueck[0].slice(1, stueck[0].lastIndexOf(')')));
68 }
69 }
70 return teile
71 .join(' ')
72 .replace(/\\([()])/g, '$1')
73 .replace(/\s+/g, ' ');
74 }
75
76 function ausdruck(project: Project): string {
77 const plan = buildSignalPlan(project);
78 const report = validateProject(project, plan, FIXED_DATE);
79 return pdfText(buildProjectPdf(project, plan, report, DEFAULT_PDF_OPTIONS, FIXED_DATE));
80 }
81
82 /**
83 * Semikolongetrennte Tabellenausgabe in Zeilen und Zellen zerlegen.
84 *
85 * Die Trennung achtet auf Anfuehrungszeichen: Die Fensterzelle enthaelt selbst
86 * ein Semikolon ("12–40; 60–72") und wird von der Ausgabe deshalb umschlossen.
87 * Ein naives split(';') zerrisse genau die Zelle, um die es hier geht.
88 */
89 function tabelle(csv: string): string[][] {
90 return csv
91 .replace(/^/, '')
92 .split('\r\n')
93 .map((zeile) => {
94 const zellen: string[] = [];
95 let aktuell = '';
96 let inAnfuehrung = false;
97 for (let i = 0; i < zeile.length; i += 1) {
98 const zeichen = zeile[i]!;
99 if (zeichen === '"') {
100 if (inAnfuehrung && zeile[i + 1] === '"') {
101 aktuell += '"';
102 i += 1;
103 } else {
104 inAnfuehrung = !inAnfuehrung;
105 }
106 } else if (zeichen === ';' && !inAnfuehrung) {
107 zellen.push(aktuell);
108 aktuell = '';
109 } else {
110 aktuell += zeichen;
111 }
112 }
113 zellen.push(aktuell);
114 return zellen;
115 });
116 }
117
118 /**
119 * Wie der Text im PDF wirklich steht.
120 *
121 * Die PDF-Standardschriften kennen kein Halbgeviertstrich; toWinAnsi ersetzt
122 * ihn durch den Bindestrich. Ein Vergleich mit der Zeichenkette der
123 * Tabellenausgabe muss dieselbe Ersetzung durchlaufen, sonst prueft er nichts.
124 */
125 function imAusdruck(text: string): string {
126 return toWinAnsi(text);
127 }
128
129 function spalte(zeilen: string[][], name: string): string[] {
130 const index = zeilen[0]!.indexOf(name);
131 expect(index, `Spalte "${name}"`).toBeGreaterThanOrEqual(0);
132 return zeilen.slice(1).map((zeile) => zeile[index] ?? '');
133 }
134
135 /**
136 * Vierphasiger Umlauf, in dem K1/K2 in Phase 1 und 3 und K3/K4 in Phase 2 und 4
137 * freigegeben sind.
138 *
139 * Vier Phasen und nicht drei: Bei drei Phasen waeren Phase 1 und Phase 3 ueber
140 * das Umlaufende BENACHBART, und der Planaufbau fasste beide zu einem einzigen
141 * Fenster zusammen (cyclicRuns). Erst der Wechsel 1-2-3-4 erzeugt zwei wirklich
142 * getrennte Fenster - genau der Fall, den die alte Tabelle verschwieg.
143 */
144 function zweiFenster(): Project {
145 const roh = createStandardIntersectionProject('Zwei Fenster', FIXED_DATE);
146 const id = (name: string) => roh.signalGroups.find((g) => g.name === name)!.id;
147 const nordSued = [id('K1'), id('K2'), id('F1')];
148 const ostWest = [id('K3'), id('K4'), id('F2')];
149 const phasen = [
150 createPhase('Phase 1 - Nord/Süd', nordSued),
151 createPhase('Phase 2 - Ost/West', ostWest),
152 createPhase('Phase 3 - Nord/Süd', nordSued),
153 createPhase('Phase 4 - Ost/West', ostWest),
154 ];
155 return {
156 ...roh,
157 phases: phasen,
158 program: { ...roh.program, phaseOrder: phasen.map((p) => p.id) },
159 // Vier Phasen kosten doppelt so viele Uebergaenge; mit den Verkehrsstaerken
160 // der Vorlage waere der Umlauf uebersaettigt und die Freigabezeiten stuenden
161 // auf einem Ersatzwert. Der Fall soll die Fensterspalte pruefen, nicht die
162 // Uebersaettigung.
163 demands: roh.demands.map((d) => ({ ...d, volume: Math.round(d.volume / 2) })),
164 };
165 }
166
167 describe('Freigabefenster: alle Fenster statt nur des ersten (Befund C22)', () => {
168 it('legt der Pruefung wirklich zwei getrennte Fenster vor', () => {
169 // Vorbedingung des ganzen Falls: Ohne zwei Fenster prueft nichts darunter
170 // etwas. K1 ist in Phase 1 und 3, F1 ebenso.
171 const plan = buildSignalPlan(zweiFenster());
172 const k1 = plan.groups.find((g) => g.name === 'K1')!;
173 expect(k1.greens).toHaveLength(2);
174 // Die Summe ist die Summe beider Fenster - nicht die Dauer des ersten.
175 const summe = k1.greens[0]!.duration + k1.greens[1]!.duration;
176 expect(k1.totalGreen).toBeCloseTo(summe, 3);
177 expect(k1.greens[0]!.duration).toBeLessThan(k1.totalGreen);
178 });
179
180 it('nennt in der Zelle beide Fenster, getrennt durch Strichpunkt', () => {
181 const plan = buildSignalPlan(zweiFenster());
182 const k1 = plan.groups.find((g) => g.name === 'K1')!;
183 const zelle = freigabefensterZelle(k1.greens, plan.cycleTime);
184
185 expect(zelle.split('; ')).toHaveLength(2);
186 for (const fenster of k1.greens) {
187 const ende = (fenster.start + fenster.duration) % plan.cycleTime;
188 expect(zelle).toContain(
189 `${fenster.start.toFixed(1).replace(/\.0$/, '').replace('.', ',')}–` +
190 `${ende.toFixed(1).replace(/\.0$/, '').replace('.', ',')}`,
191 );
192 }
193 });
194
195 it('gibt bei einem Fenster genau ein Intervall und ohne Fenster einen Strich', () => {
196 const plan = buildSignalPlan(createStandardIntersectionProject('Zwei Phasen', FIXED_DATE));
197 const k1 = plan.groups.find((g) => g.name === 'K1')!;
198 expect(k1.greens).toHaveLength(1);
199 expect(freigabefensterZelle(k1.greens, plan.cycleTime)).not.toContain(';');
200 expect(freigabefensterZelle([], plan.cycleTime)).toBe('–');
201 });
202
203 it('nennt die Dauerfreigabe als ganzen Umlauf statt als "0–0"', () => {
204 // Ein Fenster ueber den ganzen Umlauf faellt beim Rueckfall in den Umlauf
205 // auf das Ende 0 zurueck; der Altbestand druckte dafuer "0 s" und "0 s".
206 expect(freigabefensterZelle([{ start: 0, duration: 80 }], 80)).toBe('0–80 (ganzer Umlauf)');
207 });
208
209 it('druckt im Ausdruck die Spalte, die Summenbezeichnung und die Fussnote', () => {
210 const text = ausdruck(zweiFenster());
211 expect(text).toContain(imAusdruck('Freigabefenster (von–bis) [s]'));
212 expect(text).toContain('Freigabezeit (Summe)');
213 // Der Altstand fuehrte zwei Spalten aus dem ersten Fenster - im flachen
214 // Text des Ausdrucks standen ihre Koepfe nebeneinander. Auf "Freigabe von"
215 // allein laesst sich nicht pruefen: Der Pruefbericht schreibt "die Freigabe
216 // von K1" in seine Meldungen.
217 expect(text).not.toContain('Freigabe von Freigabe bis');
218 expect(text).toContain(
219 imAusdruck('Die Freigabezeit ist die Summe aller Fenster, nicht die Dauer des ersten.'),
220 );
221 });
222
223 it('druckt beide Fenster der Signalgruppe in den Ausdruck', () => {
224 const projekt = zweiFenster();
225 const plan = buildSignalPlan(projekt);
226 const k1 = plan.groups.find((g) => g.name === 'K1')!;
227 const zelle = freigabefensterZelle(k1.greens, plan.cycleTime);
228 expect(zelle).toContain('; ');
229 expect(ausdruck(projekt)).toContain(imAusdruck(zelle));
230 });
231
232 it('fuehrt in der Tabellenausgabe Fenster und Summe getrennt', () => {
233 const projekt = zweiFenster();
234 const plan = buildSignalPlan(projekt);
235 const zeilen = tabelle(signalGroupsCsv(projekt, plan));
236 const namen = spalte(zeilen, 'Signalgruppe');
237 const fenster = spalte(zeilen, 'Freigabefenster [s]');
238 const summen = spalte(zeilen, 'Freigabezeit (Summe aller Fenster) [s]');
239
240 const i = namen.indexOf('K1');
241 expect(i).toBeGreaterThanOrEqual(0);
242 const k1 = plan.groups.find((g) => g.name === 'K1')!;
243 expect(fenster[i]).toBe(freigabefensterZelle(k1.greens, plan.cycleTime));
244 expect(fenster[i]!.split('; ')).toHaveLength(2);
245 expect(Number(summen[i]!.replace(',', '.'))).toBeCloseTo(k1.totalGreen, 1);
246 });
247
248 it('zeigt in Ausdruck und Tabellenausgabe dieselbe Zeichenkette', () => {
249 // Wer beide Unterlagen nebeneinanderlegt, vergleicht Zeile fuer Zeile; zwei
250 // Schreibweisen derselben Fenster waeren ein neuer Widerspruch.
251 const projekt = zweiFenster();
252 const plan = buildSignalPlan(projekt);
253 const zeilen = tabelle(signalGroupsCsv(projekt, plan));
254 const namen = spalte(zeilen, 'Signalgruppe');
255 const fenster = spalte(zeilen, 'Freigabefenster [s]');
256 const text = ausdruck(projekt);
257
258 for (const [i, name] of namen.entries()) {
259 const gruppe = plan.groups.find((g) => g.name === name);
260 if (gruppe === undefined || gruppe.greens.length === 0) continue;
261 expect(fenster[i], name).toBe(freigabefensterZelle(gruppe.greens, plan.cycleTime));
262 expect(text, name).toContain(imAusdruck(fenster[i]!));
263 }
264 });
265
266 it('haelt die Fussnote und den Spaltennamen der Tabellenausgabe beisammen', () => {
267 expect(FREIGABEFENSTER_FUSSNOTE).toContain('Summe aller Fenster');
268 expect(FREIGABEFENSTER_FUSSNOTE).toContain('getrennten Phasen');
269 });
270
271 /*
272 * Zwei Randfaelle, die der erste Entwurf offen liess.
273 */
274 it('ordnet die Fenster nach Umlaufposition, nicht nach Speicherreihenfolge', () => {
275 // greens entsteht aus der Phasenfolge und ist nicht sortiert. Eine
276 // Signalzeitentabelle wird gegen den Umlauf gelesen; eine Zeile, die
277 // rueckwaerts springt, liest sich wie ein Fehler in der Schaltung.
278 expect(
279 freigabefensterZelle(
280 [
281 { start: 116, duration: 28 },
282 { start: 56, duration: 28 },
283 ],
284 120,
285 ),
286 ).toBe('56–84; 116–24');
287 });
288
289 it('nennt auch die Signalgruppentabelle ihre Freigabespalte eine Summe', () => {
290 // Die Spalte druckt totalGreen, also ebenfalls die Summe aller Fenster,
291 // sagte das aber nicht - und die C22-Fussnote steht auf einer anderen
292 // Seite.
293 expect(ausdruck(zweiFenster())).toContain(imAusdruck('Freigabe im Plan (Summe)'));
294 });
295
296 it('nennt ein Fenster am Umlaufende "60–80" und nicht "60–0"', () => {
297 // Dieselbe Modulo-Null wie bei der Dauerfreigabe, nur an einem Fenster,
298 // das nicht den ganzen Umlauf belegt: "60–0" behauptete ein Fenster, das
299 // rueckwaerts laeuft, statt eines, das bis zum Umlaufende reicht.
300 expect(freigabefensterZelle([{ start: 60, duration: 20 }], 80)).toBe('60–80');
301 // Der echte Ueberlauf bleibt, wie er war - das Ende liegt dann vor dem
302 // Beginn, und die Fussnote sagt es.
303 expect(freigabefensterZelle([{ start: 116, duration: 28 }], 120)).toBe('116–24');
304 });
305 });
306
307 describe('Furt mit Mittelinsel wird als durchgehende Furt gerechnet (Befund D2)', () => {
308 it('vermerkt es im Furtregel-Absatz', () => {
309 const absatz = furtregelAbsatz(
310 buildSignalPlan(createStandardIntersectionProject('Furt', FIXED_DATE)),
311 )!;
312 expect(absatz).not.toBeNull();
313 expect(absatz).toContain(MITTELINSEL_VERMERK);
314 });
315
316 it('sagt, WOMIT gerechnet wurde, und behauptet nichts ueber die Eingabe', () => {
317 // BERICHTIGT 5.38.0. Hier stand die Erwartung, der Vermerk nenne die
318 // Rechnung "konservativ". Das war eine Zusicherung ueber eine EINGABE:
319 // Gerechnet wird mit dem je Konfliktbeziehung erfassten Raeumweg, und ob
320 // der bis zur gegenueberliegenden Bordkante reicht oder an der Insel endet,
321 // kann dieser Plan nicht feststellen. Wer die Furt nur bis zur Insel
322 // vermass, bekam die kuerzeren Zeiten UND einen gedruckten Satz, der sie
323 // konservativ nannte - in einer Unterlage, die zur Anordnung geht.
324 expect(MITTELINSEL_VERMERK).toContain('erfassten Räumweg');
325 expect(MITTELINSEL_VERMERK).toContain('kann dieser Plan nicht feststellen');
326 expect(MITTELINSEL_VERMERK).toContain('progressive (zweistufige) Signalisierung');
327 expect(MITTELINSEL_VERMERK).toContain('bildet das Programm nicht ab');
328 // Die zurueckgenommene Zusicherung darf nicht wiederkommen.
329 expect(MITTELINSEL_VERMERK).not.toContain('konservativ');
330 });
331
332 it('nennt alle drei Groessen, die der halbe Weg verkuerzt', () => {
333 // NACHGEBESSERT 5.38.1: Die erste Berichtigung liess die RAEUMZEIT fallen -
334 // sie verband den erfassten Weg nur noch mit der Mindestfreigabezeit.
335 // Derselbe halbierte Weg traegt aber auch jede Zwischenzeit der Gruppe.
336 for (const groesse of ['Räumzeit', 'Zwischenzeit', 'Furtbedarf']) {
337 expect(MITTELINSEL_VERMERK, groesse).toContain(groesse);
338 }
339 });
340
341 it('sagt, in welche Richtung die Vereinfachung geht, und unter welcher Bedingung', () => {
342 // Ebenfalls 5.38.1: Wer nur das PDF vor sich hat, konnte der ersten
343 // Berichtigung nicht mehr entnehmen, ob die Vereinfachung zu seinen Gunsten
344 // oder zu seinen Lasten geht.
345 expect(MITTELINSEL_VERMERK).toContain('auf der sicheren Seite');
346 expect(MITTELINSEL_VERMERK).toContain('fallen alle drei zu kurz aus');
347 });
348
349 it('verspricht keine Furtlaenge, die es nicht immer gibt', () => {
350 // Und noch einmal 5.38.1: "die daraus gebildete Furtlaenge steht oben bei
351 // jeder Gruppe" war die naechste unbelegbare Zusage - fuer eine Gruppe ohne
352 // erfassten Raeumweg druckt derselbe Absatz "Furtlaenge unbekannt".
353 expect(MITTELINSEL_VERMERK).not.toContain('steht oben bei jeder Gruppe');
354 expect(MITTELINSEL_VERMERK).toContain('oder dass');
355 });
356
357 it('steht im Ausdruck, sobald eine Fussgaengergruppe vorkommt', () => {
358 const text = ausdruck(createStandardIntersectionProject('Furt', FIXED_DATE));
359 expect(text).toContain(
360 imAusdruck('Eine Furt mit Mittelinsel ist über die Insel hinweg zu vermessen'),
361 );
362 expect(text).toContain('bildet das Programm nicht ab');
363 });
364 });
365
366 /**
367 * Fussgaengerschutzanlage mit vier Phasen - der Umlauf ist uebersaettigt, und
368 * der Pruefbericht raet zum Anforderungsbetrieb (signalplan.uebersaettigt).
369 */
370 function fsaUebersaettigt(): Project {
371 return { ...zweiFensterOhneEntlastung(), anlagenart: 'fussgaengerschutzanlage' };
372 }
373
374 /** Wie zweiFenster(), aber mit den Verkehrsstaerken der Vorlage. */
375 function zweiFensterOhneEntlastung(): Project {
376 const roh = createStandardIntersectionProject('Anforderung', FIXED_DATE);
377 const id = (name: string) => roh.signalGroups.find((g) => g.name === name)!.id;
378 const nordSued = [id('K1'), id('K2'), id('F1')];
379 const ostWest = [id('K3'), id('K4'), id('F2')];
380 const phasen = [
381 createPhase('Phase 1', nordSued),
382 createPhase('Phase 2', ostWest),
383 createPhase('Phase 3', nordSued),
384 createPhase('Phase 4', ostWest),
385 ];
386 return {
387 ...roh,
388 phases: phasen,
389 program: { ...roh.program, phaseOrder: phasen.map((p) => p.id) },
390 };
391 }
392
393 /**
394 * Fussgaengerschutzanlage mit langer Wartezeit - der Pruefbericht raet ebenfalls
395 * zum Anforderungsbetrieb, aber aus einer anderen Regel (wartezeitBefunde in
396 * rules.ts statt uebersaettigtVorschlag in engine.ts). Beide Wege muessen den
397 * Vorbehalt tragen; er steht deshalb einmal als Konstante.
398 */
399 function fsaLangeWartezeit(): Project {
400 return {
401 ...createStandardIntersectionProject('Wartezeit', FIXED_DATE),
402 anlagenart: 'fussgaengerschutzanlage',
403 };
404 }
405
406 describe('Anforderungsbetrieb ist nicht abgebildet (Befund D3)', () => {
407 it('haengt den Vorbehalt an den Vorschlag bei Übersättigung', () => {
408 const projekt = fsaUebersaettigt();
409 const bericht = validateProject(projekt, buildSignalPlan(projekt), FIXED_DATE);
410 const befund = bericht.findings.find((f) => f.rule === 'signalplan.uebersaettigt');
411 expect(befund, 'Fall erreicht die Regel').toBeDefined();
412 expect(befund!.suggestion).toContain('auf Anforderung');
413 expect(befund!.suggestion).toContain(ANFORDERUNGSBETRIEB_VORBEHALT);
414 });
415
416 it('haengt den Vorbehalt an den Vorschlag bei zu langer Wartezeit', () => {
417 const projekt = fsaLangeWartezeit();
418 const bericht = validateProject(projekt, buildSignalPlan(projekt), FIXED_DATE);
419 const befund = bericht.findings.find((f) => f.rule.startsWith('signalplan.wartezeit'));
420 expect(befund, 'Fall erreicht die Regel').toBeDefined();
421 expect(befund!.suggestion).toContain('auf Anforderung');
422 expect(befund!.suggestion).toContain(ANFORDERUNGSBETRIEB_VORBEHALT);
423 });
424
425 it('laesst keinen Anforderungs-Vorschlag ohne Vorbehalt durch', () => {
426 // Die eigentliche Zusage: Nicht "an zwei Stellen steht es", sondern "es
427 // gibt keine Stelle ohne". Ein neuer Vorschlagstext faellt hier auf.
428 let gepruefte = 0;
429 for (const projekt of [fsaUebersaettigt(), fsaLangeWartezeit(), zweiFensterOhneEntlastung()]) {
430 const bericht = validateProject(projekt, buildSignalPlan(projekt), FIXED_DATE);
431 for (const finding of bericht.findings) {
432 if (!finding.suggestion.includes('auf Anforderung')) continue;
433 gepruefte += 1;
434 expect(finding.suggestion, finding.rule).toContain(ANFORDERUNGSBETRIEB_VORBEHALT);
435 }
436 }
437 expect(gepruefte, 'die Schleife prüft wirklich etwas').toBeGreaterThan(1);
438 });
439
440 it('nennt Ruecksprung, Alles-Rot und Anforderungslogik - und dass nichts davon geprueft wird', () => {
441 expect(ANFORDERUNGSBETRIEB_VORBEHALT).toContain('Rücksprung');
442 expect(ANFORDERUNGSBETRIEB_VORBEHALT).toContain('Alles-Rot');
443 expect(ANFORDERUNGSBETRIEB_VORBEHALT).toContain('Anforderungslogik');
444 expect(ANFORDERUNGSBETRIEB_VORBEHALT).toContain('weder gerechnet noch geprüft');
445 // Befund D4: keine Abschnittsnummer, nur das Regelwerk.
446 expect(ANFORDERUNGSBETRIEB_VORBEHALT).not.toMatch(/Abschnitt \d/);
447 });
448
449 it('traegt den Vorbehalt in Ausdruck und Pruefbericht-Tabelle', () => {
450 const projekt = fsaLangeWartezeit();
451 const plan = buildSignalPlan(projekt);
452 const bericht = validateProject(projekt, plan, FIXED_DATE);
453
454 expect(reportCsv(bericht)).toContain(ANFORDERUNGSBETRIEB_VORBEHALT);
455 expect(ausdruck(projekt)).toContain(imAusdruck(ANFORDERUNGSBETRIEB_VORBEHALT));
456 });
457 });
458
459 /*
460 * D1 im Ausdruck: Der Schranken-Absatz der einstreifigen Verkehrsfuehrung
461 * rechnete Zwischenzeit und Umlaufzeit mit den Regelwerten statt mit den
462 * Vorgaben des Plans - und genau diese beiden Zahlen sind dort die BEGRUENDUNG
463 * der Umlaufzeitobergrenze. Ein Pruefer, der sie nachrechnet, fand mit den
464 * Kennwerten desselben Dokuments etwas anderes.
465 */
466 describe('Herleitung der Umlaufzeitobergrenze rechnet mit den Vorgaben des Plans (Hinweis D1)', () => {
467 function einstreifig(vorgaben: Record<string, number>): Project {
468 const roh = createStandardIntersectionProject('Engstelle', FIXED_DATE);
469 return {
470 ...roh,
471 anlagenart: 'einstreifig',
472 settings: { ...roh.settings, rilsa: vorgaben },
473 };
474 }
475
476 /** Die gerechneten Zahlen des Absatzes, aus derselben Quelle wie der Plan. */
477 function erwartet(vorgaben: Record<string, number>): { zwischenzeit: number; umlauf: number } {
478 const defaults = resolveRilsaSettings(vorgaben, undefined, 'einstreifig').defaults;
479 const laenge = ANLAGENART_GRENZEN.einstreifig.maxClearingDistance;
480 return {
481 zwischenzeit: engstellenFolgen(laenge, 50, defaults).zwischenzeit,
482 umlauf: umlaufBeiFreigabe(laenge, 50, 20, defaults),
483 };
484 }
485
486 it('nennt mit den Regelwerten die Zahlen des Regelwerts', () => {
487 const soll = erwartet({});
488 const text = ausdruck(einstreifig({}));
489 expect(text).toContain(`${soll.zwischenzeit} s Zwischenzeit`);
490 expect(text).toContain(`${soll.umlauf} s Umlaufzeit`);
491 });
492
493 it('folgt einer abweichenden Raeumgeschwindigkeit in beide Zahlen', () => {
494 const vorgaben = { clearingSpeedKfzStraight: 8 };
495 const regel = erwartet({});
496 const soll = erwartet(vorgaben);
497 // Vorbedingung: Ohne Unterschied prueft der Fall nichts.
498 expect(soll.zwischenzeit).not.toBe(regel.zwischenzeit);
499 expect(soll.umlauf).not.toBe(regel.umlauf);
500
501 const text = ausdruck(einstreifig(vorgaben));
502 expect(text).toContain(`${soll.zwischenzeit} s Zwischenzeit`);
503 expect(text).toContain(`${soll.umlauf} s Umlaufzeit`);
504 // Und nicht mehr die Regelwertzahlen, die im Plan daneben nicht vorkommen.
505 expect(text).not.toContain(`${regel.zwischenzeit} s Zwischenzeit`);
506 expect(text).not.toContain(`${regel.umlauf} s Umlaufzeit`);
507 });
508 });
509
510 /*
511 * C21 im gedruckten Nachweis: Der VwV-StVO-Eintrag des
512 * Fundstellenverzeichnisses nannte Rot-Gelb-Zeit und Gelbzeitstaffel als
513 * Regelwerte - unter einer Ueberschrift, die "keine einstellbaren Vorgaben"
514 * behauptete, obwohl beide Vorgabenfelder sind.
515 */
516 describe('Fundstellenverzeichnis nennt die wirksame Gelb- und Rot-Gelb-Zeit (Befund C21)', () => {
517 const eintrag = (defaults = RILSA_DEFAULTS) =>
518 quellenOhneFeld('HBS', 'knotenpunkt', defaults).find((e) => e.schluessel === 'vwv-stvo')!;
519
520 function kennwert(defaults: RilsaDefaults, label: string): string {
521 return eintrag(defaults).kennwerte.find((k) => k.label.startsWith(label))!.wert;
522 }
523
524 it('behauptet in der Ueberschrift nicht mehr, hier gebe es keine Vorgaben', () => {
525 expect(eintrag().bezeichnung).not.toContain('keine einstellbaren Vorgaben');
526 expect(eintrag().bezeichnung).toContain('einstellbar');
527 });
528
529 it('nennt mit den Regelwerten den Regelwert ohne Zusatz', () => {
530 expect(kennwert(RILSA_DEFAULTS, 'Gelbzeit')).toBe(
531 '3 s bis 50 km/h, 4 s bis 60 km/h, 5 s bis 70 km/h (Rn. 17)',
532 );
533 expect(kennwert(RILSA_DEFAULTS, 'Rot-Gelb-Zeit')).not.toContain('abweichend');
534 });
535
536 it('nennt bei abweichender Vorgabe den wirksamen Wert UND den Regelwert', () => {
537 const geaendert = resolveRilsaSettings(
538 { yellowKfzUpTo50: 4, redYellow: 2 },
539 undefined,
540 'knotenpunkt',
541 ).defaults;
542 const gelb = kennwert(geaendert, 'Gelbzeit');
543 expect(gelb).toContain('4 s bis 50 km/h');
544 expect(gelb).toContain('abweichend vom Regelwert 3 s bis 50 km/h');
545 const rotGelb = kennwert(geaendert, 'Rot-Gelb-Zeit');
546 expect(rotGelb).toContain('2 s – abweichend vom Regelwert 1 s');
547 });
548
549 it('druckt den wirksamen Wert auch wirklich in den Ausdruck', () => {
550 const roh = createStandardIntersectionProject('Gelbstaffel', FIXED_DATE);
551 const projekt: Project = {
552 ...roh,
553 settings: { ...roh.settings, rilsa: { yellowKfzUpTo50: 4 } },
554 };
555 expect(ausdruck(projekt)).toContain(imAusdruck('abweichend vom Regelwert 3 s bis 50 km/h'));
556 });
557 });