lsa-planer

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

/ tests export pdfKoordinierung.test.ts

8,1 KB Rohdatei
tests/export/pdfKoordinierung.test.ts — 198 Zeilen
1 import { inflateSync } from 'node:zlib';
2 import { describe, expect, it } from 'vitest';
3 import * as actions from '@/app/actions';
4 import { buildProjectPdf, DEFAULT_PDF_OPTIONS } from '@/services/export/pdf';
5 import { freigabefensterZelle } from '@/services/export/csv';
6 import { buildSignalPlan } from '@/domain/plan/signalPlan';
7 import { validateProject } from '@/domain/validation';
8 import { createPhase, createStandardIntersectionProject } from '@/domain/model/factory';
9 import { toWinAnsi } from '@/render/pdfSurface';
10 import * as fmt from '@/ui/format';
11 import type { Project } from '@/domain/model/project';
12
13 /**
14 * Die Koordinierung in der Unterlage.
15 *
16 * DER SATZ, AUF DEN ES ANKOMMT: Der Versatz ist eine Empfehlung fuer die
17 * NACHBARANLAGEN und verschiebt den Signalzeitenplan dieses Knotenpunkts
18 * nicht. Wer das nicht liest, sucht die Wirkung im Zeitdiagramm daneben - und
19 * findet sie nicht.
20 */
21
22 const STICHTAG = new Date('2026-01-01T12:00:00Z');
23
24 function pdfText(bytes: Uint8Array): string {
25 const roh = new TextDecoder('latin1').decode(bytes);
26 const teile: string[] = [];
27 const suche = /stream\r?\n/g;
28 let treffer: RegExpExecArray | null;
29 while ((treffer = suche.exec(roh)) !== null) {
30 const start = treffer.index + treffer[0].length;
31 const ende = roh.indexOf('endstream', start);
32 if (ende < 0) continue;
33 let inhalt: string;
34 try {
35 inhalt = inflateSync(Buffer.from(roh.slice(start, ende), 'latin1')).toString('latin1');
36 } catch {
37 continue;
38 }
39 for (const t of inhalt.matchAll(/\(((?:[^()\\]|\\.)*)\)\s*Tj/g)) {
40 teile.push(t[1]!.replace(/\\([()\\])/g, '$1'));
41 }
42 }
43 return teile.join(' ').replace(/\s+/g, ' ');
44 }
45
46 function ausdruck(project: Project): string {
47 const plan = buildSignalPlan(project);
48 return pdfText(
49 buildProjectPdf(
50 project,
51 plan,
52 validateProject(project, plan, STICHTAG),
53 DEFAULT_PDF_OPTIONS,
54 STICHTAG,
55 ),
56 );
57 }
58
59 function ohneKette(): Project {
60 return createStandardIntersectionProject('Hauptstraße / Bahnhofstraße', STICHTAG);
61 }
62
63 function mitKette(
64 optionen: { gegenrichtung?: boolean; beginn?: number; dauer?: number } = {},
65 ): Project {
66 const roh = ohneKette();
67 const k1 = roh.signalGroups[0]!.id;
68 const k2 = roh.signalGroups[1]!.id;
69 let projekt: Project = {
70 ...roh,
71 program: { ...roh.program, method: 'manuell', manualCycleTime: 60 },
72 };
73 projekt = actions.setzeKoordinierung(projekt, {
74 hauptrichtung: k1,
75 ...(optionen.gegenrichtung === true ? { gegenrichtung: k2 } : {}),
76 progressionsgeschwindigkeit: 36,
77 platz: 0,
78 });
79 projekt = actions.koordinierungsknotenAnlegen(projekt, 'Weststraße');
80 const west = projekt.koordinierung!.nachbarn[0]!;
81 return actions.koordinierungsknotenAendern(projekt, west.id, {
82 abstand: 200,
83 freigabeBeginn: optionen.beginn ?? 20,
84 freigabeDauer: optionen.dauer ?? 25,
85 ...(optionen.gegenrichtung === true ? { gegenBeginn: 20, gegenDauer: 25 } : {}),
86 });
87 }
88
89 /** Der Ausdruck vom Kopf der Koordinierungstabelle bis zur Ueberschrift des Zeit-Weg-Diagramms. */
90 function koordinierungstabelle(text: string): string {
91 const anfang = text.indexOf(toWinAnsi('Freigabe Hauptrichtung'));
92 expect(anfang).toBeGreaterThanOrEqual(0);
93 const ende = text.indexOf(toWinAnsi('Zeit-Weg-Diagramm'), anfang);
94 expect(ende).toBeGreaterThan(anfang);
95 return text.slice(anfang, ende);
96 }
97
98 describe('Planunterlage - Koordinierung', () => {
99 it('druckt den Abschnitt mit Entfernung, Reisezeit und Versatz', () => {
100 const text = ausdruck(mitKette());
101 expect(text).toContain(toWinAnsi('Koordinierung'));
102 expect(text).toContain(toWinAnsi('Weststraße'));
103 expect(text).toContain(toWinAnsi('Empfohlener Versatz'));
104 expect(text).toContain(toWinAnsi('Zeit-Weg-Diagramm'));
105 expect(text).toContain(toWinAnsi('(dieser Knotenpunkt)'));
106 });
107
108 it('sagt, dass der Versatz den eigenen Plan nicht verschiebt', () => {
109 const text = ausdruck(mitKette());
110 expect(text).toContain(toWinAnsi('DER VERSATZ IST EINE EMPFEHLUNG FÜR DIE NACHBARANLAGEN'));
111 expect(text).toContain(toWinAnsi('gilt zwischen Anlagen, nicht innerhalb einer'));
112 });
113
114 it('sagt, dass die Nachbaranlagen hier nicht geprüft sind', () => {
115 /*
116 * Die wichtigste Abgrenzung des Abschnitts. Eine Behoerde, die eine
117 * Bandbreite und Versatzzeiten fuer drei Anlagen sieht, koennte den
118 * Ausdruck fuer die Unterlage des ganzen Strassenzugs halten.
119 */
120 const text = ausdruck(mitKette());
121 expect(text).toContain(toWinAnsi('weder wiedergegeben noch geprüft'));
122 expect(text).toContain(toWinAnsi('gemeinsame Umlaufzeit'));
123 });
124
125 it('benennt die fehlende Gegenrichtung, statt sie anzunehmen', () => {
126 const ohne = ausdruck(mitKette());
127 expect(ohne).toContain(toWinAnsi('ein zweites Band wird deshalb NICHT gerechnet'));
128
129 const mit = ausdruck(mitKette({ gegenrichtung: true }));
130 expect(mit).toContain(toWinAnsi('Bandbreite in der Gegenrichtung'));
131 expect(mit).not.toContain(toWinAnsi('ein zweites Band wird deshalb NICHT gerechnet'));
132 });
133
134 it('führt eine Zeile in der Bestandteileübersicht - auch ohne Straßenzug', () => {
135 const mit = ausdruck(mitKette());
136 expect(mit).toContain(toWinAnsi('Koordinierung im Straßenzug (Grüne Welle)'));
137 expect(mit).toContain(toWinAnsi('ja – Abschnitt „Koordinierung"'));
138
139 const ohne = ausdruck(ohneKette());
140 expect(ohne).toContain(toWinAnsi('kein Straßenzug erfasst'));
141 // Ohne Kette gibt es den Abschnitt nicht - und damit keinen seiner Sätze.
142 expect(ohne).not.toContain(toWinAnsi('Zeit-Weg-Diagramm'));
143 expect(ohne).not.toContain(toWinAnsi('Empfohlener Versatz'));
144 });
145 });
146
147 /*
148 * FASSUNG 5.43.0, BEFUND 13: Die Tabelle des Abschnitts nannte das
149 * Freigabefenster als "Beginn bis Beginn + Dauer", ohne Faltung - "91 s bis
150 * 139 s" bei 95 s Umlaufzeit, waehrend die Signalzeitentabelle derselben
151 * Unterlage "91–44" fuehrt und das Diagramm darunter (seit derselben Pruefung)
152 * mit dem gefalteten Fenster zeichnet.
153 */
154 describe('Planunterlage - Freigabefenster der Koordinierungstabelle', () => {
155 it('nennt ein einzelnes eigenes Fenster mit derselben Zeichenkette wie die Signalzeitentabelle - auch über das Umlaufende', () => {
156 // Eine dritte Phase fuer Nord/Sued: K1 ist in der letzten und der ersten
157 // Phase frei, sein Fenster laeuft ueber das Umlaufende.
158 const basis = mitKette();
159 const k1 = basis.signalGroups[0]!;
160 const k2 = basis.signalGroups[1]!;
161 const phase3 = createPhase('Phase 3 - Nord/Süd', [k1.id, k2.id]);
162 const projekt: Project = {
163 ...basis,
164 phases: [...basis.phases, phase3],
165 program: { ...basis.program, phaseOrder: [...basis.program.phaseOrder, phase3.id] },
166 };
167 const plan = buildSignalPlan(projekt);
168 const fenster = plan.groups.find((g) => g.groupId === k1.id)!.greens;
169 expect(fenster).toHaveLength(1);
170 expect(fenster[0]!.start + fenster[0]!.duration).toBeGreaterThan(plan.cycleTime);
171
172 const zelle = freigabefensterZelle(fenster, plan.cycleTime);
173 const tabelle = koordinierungstabelle(ausdruck(projekt));
174 expect(tabelle).toContain(toWinAnsi(zelle));
175 expect(tabelle).not.toContain(
176 toWinAnsi(`bis ${fmt.seconds(fenster[0]!.start + fenster[0]!.duration)}`),
177 );
178 });
179
180 it.each([
181 { beginn: 50, dauer: 20, soll: '50–10', alt: 'bis 70 s' },
182 // "Freigabe ab" darf die Umlaufzeit uebersteigen; gerechnet wird gefaltet.
183 { beginn: 70, dauer: 20, soll: '10–30', alt: 'bis 90 s' },
184 ])(
185 'faltet das Fenster einer Nachbaranlage in den Umlauf, wie die Koordinierung rechnet (Beginn $beginn s)',
186 ({ beginn, dauer, soll, alt }) => {
187 const tabelle = koordinierungstabelle(ausdruck(mitKette({ beginn, dauer })));
188 expect(tabelle).toContain(toWinAnsi(soll));
189 expect(tabelle).not.toContain(toWinAnsi(alt));
190 },
191 );
192
193 it('sagt, dass ein Fenster über den Umlaufbeginn hinaus vor seinem Beginn endet', () => {
194 const tabelle = koordinierungstabelle(ausdruck(mitKette({ beginn: 50, dauer: 20 })));
195 expect(tabelle).toContain(toWinAnsi('in den Umlauf gefaltet'));
196 expect(tabelle).toContain(toWinAnsi('liegt sein Ende vor seinem Beginn'));
197 });
198 });