lsa-planer

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

/ tests export pdfFurtregel.test.ts

10,1 KB Rohdatei
tests/export/pdfFurtregel.test.ts — 234 Zeilen
1 import { inflateSync } from 'node:zlib';
2 import { describe, expect, it } from 'vitest';
3 import {
4 buildProjectPdf,
5 DEFAULT_PDF_OPTIONS,
6 furtmerkmale,
7 furtregelAbsatz,
8 } from '@/services/export/pdf';
9 import { buildSignalPlan } from '@/domain/plan/signalPlan';
10 import { validateProject } from '@/domain/validation';
11 import { createStandardIntersectionProject } from '@/domain/model/factory';
12 import { ENGSTELLE_PRAXIS } from '@/domain/rilsa/constants';
13 import type { Project } from '@/domain/model/project';
14
15 /**
16 * Planunterlagen zu den Befunden C1 und C5.
17 *
18 * C1: Die Signalgruppentabelle kennzeichnet die Zusatzeinrichtung fuer Blinde
19 * und Sehbehinderte, und unter der Tabelle steht der Rechenweg der Furtregel -
20 * eine Mindestfreigabezeit ueber dem Regelwert muss sich aus gedruckten
21 * Groessen nachrechnen lassen. C5: Der Abschnitt zur Anlagenart nennt bei
22 * einstreifiger Fuehrung die Laengen 400/600 m, und zwar als Behoerdenpraxis,
23 * kein Regelwerkswert.
24 *
25 * Gegen den Altstand schlagen die Faelle fehl: Dort gab es weder Spalte noch
26 * Absatz noch Zeile.
27 */
28
29 const FIXED_DATE = new Date('2026-01-01T12:00:00Z');
30
31 /** Sichtbaren Text aus dem PDF lesen; die Inhaltsstroeme sind komprimiert. */
32 function pdfText(bytes: Uint8Array): string {
33 const roh = new TextDecoder('latin1').decode(bytes);
34 const teile: string[] = [];
35 const suche = /stream\r?\n/g;
36 let treffer: RegExpExecArray | null;
37
38 while ((treffer = suche.exec(roh)) !== null) {
39 const start = treffer.index + treffer[0].length;
40 const ende = roh.indexOf('endstream', start);
41 if (ende < 0) continue;
42 let inhalt: string;
43 try {
44 inhalt = inflateSync(Buffer.from(roh.slice(start, ende), 'latin1')).toString('latin1');
45 } catch {
46 continue;
47 }
48 for (const stueck of inhalt.matchAll(/\((?:\\.|[^\\()])*\)\s*Tj/g)) {
49 teile.push(stueck[0].slice(1, stueck[0].lastIndexOf(')')));
50 }
51 }
52 // Umbrochene Absaetze und Zellen wieder zusammenziehen; maskierte Klammern
53 // entwerten, damit sich "Blindenzusatz (ganze Furt)" wortwoertlich suchen laesst.
54 return teile
55 .join(' ')
56 .replace(/\\([()])/g, '$1')
57 .replace(/\s+/g, ' ');
58 }
59
60 function ausdruck(project: Project): string {
61 const plan = buildSignalPlan(project);
62 const report = validateProject(project, plan, FIXED_DATE);
63 return pdfText(buildProjectPdf(project, plan, report, DEFAULT_PDF_OPTIONS, FIXED_DATE));
64 }
65
66 function mitBlindenzusatz(): Project {
67 const roh = createStandardIntersectionProject('Furt', FIXED_DATE);
68 return {
69 ...roh,
70 signalGroups: roh.signalGroups.map((g) =>
71 g.name === 'F1' ? { ...g, blindenzusatz: true } : g,
72 ),
73 };
74 }
75
76 describe('Signalgruppentabelle: Furtmerkmale (Befund C1)', () => {
77 it('kennzeichnet den Blindenzusatz und die halbe Furt', () => {
78 const p = mitBlindenzusatz();
79 const f1 = p.signalGroups.find((g) => g.name === 'F1')!;
80 const f2 = p.signalGroups.find((g) => g.name === 'F2')!;
81 const k1 = p.signalGroups.find((g) => g.name === 'K1')!;
82 expect(furtmerkmale(f1)).toBe('Blindenzusatz (ganze Furt)');
83 expect(furtmerkmale(f2)).toBe('halbe Furt');
84 expect(furtmerkmale({ ...f2, reducedMobility: true })).toBe('erhöhter Zeitbedarf');
85 expect(furtmerkmale({ ...f1, reducedMobility: true })).toBe(
86 'Blindenzusatz (ganze Furt), erhöhter Zeitbedarf',
87 );
88 expect(furtmerkmale(k1)).toBe('–');
89 });
90
91 it('druckt Spalte und Kennzeichnung', () => {
92 const text = ausdruck(mitBlindenzusatz());
93 expect(text).toContain('Furt');
94 expect(text).toContain('Blindenzusatz (ganze Furt)');
95 expect(text).toContain('halbe Furt');
96 });
97
98 it('druckt den Rechenweg der Furtregel unter der Tabelle', () => {
99 const p = mitBlindenzusatz();
100 const plan = buildSignalPlan(p);
101 const absatz = furtregelAbsatz(plan)!;
102 expect(absatz).not.toBeNull();
103 expect(absatz).toContain('Furtregel der RiLSA');
104 // F1: ganze Furt 12 m / 1,2 m/s = 10 s, massgebend statt 5 s Regelwert.
105 // Furtweg mit zwei, Zeitbedarf mit drei Nachkommastellen (nachrechenbar).
106 expect(absatz).toContain(
107 'F1: Furt 12,00 m, ganze Furt (Blindenzusatz) 12,00 m / 1,2 m/s = 10,000 s, aufgerundet 10 s; maßgebend 10 s.',
108 );
109 // F2: halbe Furt 6 m / 1,2 m/s = 5 s - der Regelwert bleibt massgebend.
110 expect(absatz).toContain(
111 'F2: Furt 12,00 m, halbe Furt 6,00 m / 1,2 m/s = 5,000 s, aufgerundet 5 s; maßgebend der Regelwert von 5 s.',
112 );
113
114 const text = ausdruck(p);
115 expect(text).toContain('Furtregel der RiLSA');
116 expect(text).toContain('12,00 m / 1,2 m/s = 10,000 s');
117 });
118
119 it('druckt bei 12,01 m einen Rechenweg, der die 6 s hergibt (6,01 m / 1,2 m/s = 5,004 s)', () => {
120 // Mit "6,0 m / 1,2 m/s = 5,00 s, aufgerundet 6 s" liesse sich das Ergebnis
121 // aus den gedruckten Groessen nicht nachrechnen.
122 const roh = createStandardIntersectionProject('Furt', FIXED_DATE);
123 const f1 = roh.signalGroups.find((g) => g.name === 'F1')!;
124 const p: Project = {
125 ...roh,
126 conflicts: roh.conflicts.map((c) =>
127 c.fromId === f1.id ? { ...c, clearingDistance: 12.01 } : c,
128 ),
129 };
130 const absatz = furtregelAbsatz(buildSignalPlan(p))!;
131 expect(absatz).toContain(
132 'F1: Furt 12,01 m, halbe Furt 6,01 m / 1,2 m/s = 5,004 s, aufgerundet 6 s; maßgebend 6 s.',
133 );
134 });
135
136 it('sagt, wenn die Furtlaenge unbekannt ist, statt eine Pruefung zu behaupten', () => {
137 const roh = createStandardIntersectionProject('Furt', FIXED_DATE);
138 const f1 = roh.signalGroups.find((g) => g.name === 'F1')!;
139 const p: Project = { ...roh, conflicts: roh.conflicts.filter((c) => c.fromId !== f1.id) };
140 const absatz = furtregelAbsatz(buildSignalPlan(p))!;
141 expect(absatz).toContain('F1: Furtlänge unbekannt');
142 expect(absatz).toContain('Furtregel nicht prüfbar');
143 });
144
145 it('laesst den Absatz ohne Fussgaengergruppen weg', () => {
146 const roh = createStandardIntersectionProject('Furt', FIXED_DATE);
147 const fussIds = roh.signalGroups.filter((g) => g.mode === 'fuss').map((g) => g.id);
148 const p: Project = {
149 ...roh,
150 signalGroups: roh.signalGroups.filter((g) => g.mode !== 'fuss'),
151 conflicts: roh.conflicts.filter(
152 (c) => !fussIds.includes(c.fromId) && !fussIds.includes(c.toId),
153 ),
154 phases: roh.phases.map((ph) => ({
155 ...ph,
156 signalGroupIds: ph.signalGroupIds.filter((id) => !fussIds.includes(id)),
157 })),
158 };
159 expect(furtregelAbsatz(buildSignalPlan(p))).toBeNull();
160 });
161 });
162
163 describe('Schranken der Anlagenart: Engstellenlaenge nach Behoerdenpraxis (Befund C5)', () => {
164 it('nennt bei einstreifiger Fuehrung 400/600 m als Behoerdenpraxis, kein Regelwerkswert', () => {
165 const basis = createStandardIntersectionProject('Engstelle', FIXED_DATE);
166 const text = ausdruck({ ...basis, anlagenart: 'einstreifig' });
167 expect(text).toContain('Engstellenlänge: Hinweis / Warnung');
168 expect(text).toContain(
169 `ab ${ENGSTELLE_PRAXIS.hinweisAb} m / ab ${ENGSTELLE_PRAXIS.warnungAb} m`,
170 );
171 expect(text).toContain('Behördenpraxis (z. B. Hessen Mobil), kein Regelwerkswert');
172 expect(text).toContain(`üblicherweise nur bis ${ENGSTELLE_PRAXIS.hinweisAb} m auf`);
173 // Der volle Name der RSA steht in der Unterlage - seit Befund B8 aber nicht
174 // mehr in der Grundlagen-Spalte der Schrankentabelle (die Umlaufzeit- und
175 // Wartezeitzahlen stehen nicht in der RSA), sondern in der
176 // Rechtsgrundlagentabelle, wo die RSA seit Befund C13 einen eigenen Eintrag
177 // hat.
178 expect(text).toContain('Arbeitsstellen an Straßen');
179 });
180
181 it('fuehrt die Zeile am Knotenpunkt nicht', () => {
182 const text = ausdruck(createStandardIntersectionProject('Knoten', FIXED_DATE));
183 expect(text).not.toContain('Engstellenlänge: Hinweis / Warnung');
184 expect(text).not.toContain('Hessen Mobil');
185 });
186 });
187
188 /*
189 * Fundstellenverzeichnis des Ausdrucks: Die Quellen 'vwv-stvo' und
190 * 'rsa-arbeitsstellen' standen in QUELLEN, aber nicht im Verzeichnis - das
191 * entsteht aus den Vorgabenfeldern plus quellenOhneFeld, und an keiner der
192 * beiden haengt ein Feld. Und der Hinweis "Zu beachten" einer Quelle MIT
193 * Feldern (etwa der Furtregel-Vorbehalt der RiLSA-Signalzeiten samt DIN 32981)
194 * fehlte im Ausdruck ganz. Gegen den Altstand schlagen alle drei Faelle fehl.
195 */
196 describe('Fundstellenverzeichnis: VwV-StVO, RSA 21 und die Vorbehalte der Feldgruppen', () => {
197 it('druckt die VwV-StVO fuer jede Anlagenart mit 70 km/h, Rot-Gelb und Gelbzeitstaffel', () => {
198 const text = ausdruck(createStandardIntersectionProject('Knoten', FIXED_DATE));
199 expect(text).toContain('VwV-StVO');
200 expect(text).toContain(
201 'Zulässige Höchstgeschwindigkeit für den Betrieb einer LSA: bis 70 km/h (Rn. 10)',
202 );
203 expect(text).toContain('Rot-Gelb-Zeit: 1 s, höchstens 2 s (Rn. 17)');
204 expect(text).toContain('3 s bis 50 km/h, 4 s bis 60 km/h, 5 s bis 70 km/h (Rn. 17)');
205 // Am Knotenpunkt keine RSA-Anhaltswerte: Sie gehen dort in keine Meldung ein.
206 expect(text).not.toContain('Kurze Engstelle, für die eine Regelung ohne Lichtsignalanlage');
207 });
208
209 it('druckt bei einstreifiger Fuehrung die RSA 21 mit 50 m, 500 Kfz/h, 50 km/h - und 400/600 m als Behoerdenpraxis', () => {
210 const basis = createStandardIntersectionProject('Engstelle', FIXED_DATE);
211 const text = ausdruck({ ...basis, anlagenart: 'einstreifig' });
212 expect(text).toContain('RSA 21');
213 expect(text).toContain(
214 'Kurze Engstelle, für die eine Regelung ohne Lichtsignalanlage genügen kann: bis 50 m',
215 );
216 expect(text).toContain(
217 'Richtwert der Verkehrsstärke für die Anordnung (beide Richtungen zusammen): über 500 Kfz/h',
218 );
219 expect(text).toContain(
220 'Zulässige Höchstgeschwindigkeit an Arbeitsstellen längerer Dauer: in der Regel 50 km/h',
221 );
222 expect(text).toContain(
223 'Engstellenlänge 400 m / 600 m (Hinweis / Warnung): Behördenpraxis (z. B. Hessen Mobil), KEIN Wert der RSA',
224 );
225 });
226
227 it('druckt zu Feldgruppen den Hinweis "Zu beachten" - etwa den Furtregel-Vorbehalt mit DIN 32981', () => {
228 const text = ausdruck(createStandardIntersectionProject('Knoten', FIXED_DATE));
229 expect(text).toContain('DIN 32981');
230 expect(text).toContain(
231 'Zu beachten: Gesichert sind Gelbzeitstaffel, Rot-Gelb-Zeit und Mindestfreigabezeiten.',
232 );
233 });
234 });