lsa-planer

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

/ tests export pdfUmlaufzeitvergleich.test.ts

5,1 KB Rohdatei
tests/export/pdfUmlaufzeitvergleich.test.ts — 122 Zeilen
1 import { inflateSync } from 'node:zlib';
2 import { describe, expect, it } from 'vitest';
3 import { buildProjectPdf, DEFAULT_PDF_OPTIONS, type PdfOptions } 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 { toWinAnsi } from '@/render/pdfSurface';
8 import type { Project } from '@/domain/model/project';
9
10 /**
11 * Die Begruendung der Umlaufzeit in der Planunterlage.
12 *
13 * WOZU DAS IN EINER ANORDNUNGSUNTERLAGE STEHT: Eine Anordnung nach Paragraf 45
14 * StVO ist eine Ermessensentscheidung und muss begruendet werden. Die
15 * Umlaufzeit ist die Zahl, an der Wartezeit und Rueckstau haengen; bis hierher
16 * stand in der Unterlage genau eine Zahl und ein Verfahrensname, und warum
17 * dieses Verfahren, stand nirgends.
18 *
19 * DIE FAELLE BEWACHEN DREI ZUSAGEN: Der Abschnitt ist enthalten, er laesst sich
20 * abwaehlen und sagt das dann in der Bestandteileuebersicht, und er spricht
21 * keine Empfehlung aus.
22 */
23
24 const STICHTAG = new Date('2026-01-01T12:00:00Z');
25
26 /** Der sichtbare Text der Datei, in einer Zeile. */
27 function pdfText(bytes: Uint8Array): string {
28 const roh = new TextDecoder('latin1').decode(bytes);
29 const teile: string[] = [];
30 const suche = /stream\r?\n/g;
31 let treffer: RegExpExecArray | null;
32 while ((treffer = suche.exec(roh)) !== null) {
33 const start = treffer.index + treffer[0].length;
34 const ende = roh.indexOf('endstream', start);
35 if (ende < 0) continue;
36 let inhalt: string;
37 try {
38 inhalt = inflateSync(Buffer.from(roh.slice(start, ende), 'latin1')).toString('latin1');
39 } catch {
40 continue;
41 }
42 for (const t of inhalt.matchAll(/\(((?:[^()\\]|\\.)*)\)\s*Tj/g)) {
43 teile.push(t[1]!.replace(/\\([()\\])/g, '$1'));
44 }
45 }
46 return teile.join(' ').replace(/\s+/g, ' ');
47 }
48
49 function mitVerkehr(method: Project['program']['method']): Project {
50 const roh = createStandardIntersectionProject('Hauptstraße / Bahnhofstraße', STICHTAG);
51 return {
52 ...roh,
53 program: { ...roh.program, method, manualCycleTime: method === 'manuell' ? 80 : null },
54 demands: roh.signalGroups
55 .filter((g) => g.mode === 'kfz')
56 .map((g) => ({ signalGroupId: g.id, volume: 600, heavyVehicleShare: 0.05 })),
57 };
58 }
59
60 function ausdruck(project: Project, options: PdfOptions = DEFAULT_PDF_OPTIONS): string {
61 const plan = buildSignalPlan(project);
62 const bericht = validateProject(project, plan, STICHTAG);
63 return pdfText(buildProjectPdf(project, plan, bericht, options, STICHTAG));
64 }
65
66 describe('Planunterlage - Begruendung der Umlaufzeit', () => {
67 it('druckt den Abschnitt von Haus aus', () => {
68 // Vorbelegt mit true, auf ausdrueckliche Entscheidung des Anwenders: Die
69 // Wahl der Umlaufzeit war der eine Punkt, zu dem die Unterlage nichts
70 // sagte.
71 expect(DEFAULT_PDF_OPTIONS.includeCycleComparison).toBe(true);
72 const text = ausdruck(mitVerkehr('webster'));
73 expect(text).toContain(toWinAnsi('Umlaufzeit – Vergleich der Rechenansätze'));
74 });
75
76 it('führt alle vier Ansätze auf und kennzeichnet den gewählten', () => {
77 const text = ausdruck(mitVerkehr('hbs'));
78 for (const name of ['Webster', toWinAnsi('Akçelik'), 'HBS', 'HCM']) {
79 expect(text, name).toContain(name);
80 }
81 expect(text, 'der gewählte Ansatz ist gekennzeichnet').toContain(toWinAnsi('HBS (gewählt)'));
82 });
83
84 it('spricht keine Empfehlung aus und weist die Regel als eigenen Ansatz aus', () => {
85 const text = ausdruck(mitVerkehr('webster'));
86 expect(text).toContain(toWinAnsi('keine Empfehlung'));
87 expect(text).toContain(toWinAnsi('die Wahl trifft der Planer'));
88 expect(text).toContain(toWinAnsi('ein Ansatz dieses Programms'));
89 });
90
91 it('sagt bei fester Vorgabe, dass keines der vier Verfahren eingeht', () => {
92 const text = ausdruck(mitVerkehr('manuell'));
93 expect(text).toContain(toWinAnsi('von Hand vorgegeben'));
94 expect(text).toContain(toWinAnsi('Vergleichsmaßstab'));
95 });
96
97 it('laesst sich abwaehlen, und die Bestandteileuebersicht sagt es dann', () => {
98 /*
99 * Die Uebersicht ist der Ort, an dem eine Behoerde nachsieht, was die
100 * Unterlage enthaelt. Ein abgewaehlter Abschnitt, der dort nicht als
101 * abgewaehlt steht, ist eine Luecke, die niemand bemerkt.
102 */
103 const ohne = ausdruck(mitVerkehr('webster'), {
104 ...DEFAULT_PDF_OPTIONS,
105 includeCycleComparison: false,
106 });
107 expect(ohne, 'der Abschnitt fehlt').not.toContain(
108 toWinAnsi('Umlaufzeit – Vergleich der Rechenansätze:'),
109 );
110 expect(ohne, 'die Zeile der Übersicht steht da').toContain(
111 toWinAnsi('Begründung der Umlaufzeit (Vergleich der vier Rechenansätze)'),
112 );
113 expect(ohne, 'und sie sagt, dass er fehlt').toContain(toWinAnsi('nein'));
114 });
115
116 it('nennt die Regel im Fundstellenverzeichnis', () => {
117 // Sonst stuende in der Unterlage ein Wert, dessen Herkunft sie nicht
118 // ausweist - genau der Fehlertyp, gegen den das Verzeichnis steht.
119 const text = ausdruck(mitVerkehr('webster'));
120 expect(text).toContain(toWinAnsi('Umlaufzeit: Vergleich der vier Rechenansätze'));
121 });
122 });