lsa-planer

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

/ tests export pdfAnlagenart.test.ts

5,2 KB Rohdatei
tests/export/pdfAnlagenart.test.ts — 122 Zeilen
1 import { inflateSync } from 'node:zlib';
2 import { describe, expect, it } from 'vitest';
3 import { buildProjectPdf, DEFAULT_PDF_OPTIONS } 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 { ANLAGENART_LABELS } from '@/domain/rilsa/constants';
8 import type { Anlagenart, Project } from '@/domain/model/project';
9
10 /**
11 * Die Anlagenart in den Planunterlagen.
12 *
13 * Zur verkehrsbehoerdlichen Anordnung nach Paragraf 45 StVO gehoert, welche Art
14 * Anlage angeordnet wird. Ein Signalzeitenplan ohne diese Angabe laesst offen,
15 * wonach er zu beurteilen ist: Dieselbe Umlaufzeit von 240 s ist an einem
16 * Knotenpunkt eine grobe Ueberschreitung der Sicherheitsgrenze und an einer
17 * Arbeitsstelle die zwangslaeufige Folge einer langen Engstelle.
18 *
19 * Der zweite Punkt ist die Herkunft der Schranken: Bei einer einstreifigen
20 * Verkehrsfuehrung stammen die Umlaufzeitgrenzen NICHT aus der RiLSA. Ein
21 * Ausdruck, der unter "Grundlagen der Kennwerte" pauschal die RiLSA nennt,
22 * behauptet eine Deckung durch das Regelwerk, die es fuer diesen Wert nicht
23 * gibt.
24 */
25
26 const FIXED_DATE = new Date('2026-01-01T12:00:00Z');
27
28 /** Sichtbaren Text aus dem PDF lesen; die Inhaltsstroeme sind komprimiert. */
29 function pdfText(bytes: Uint8Array): string {
30 const roh = new TextDecoder('latin1').decode(bytes);
31 const teile: string[] = [];
32 const suche = /stream\r?\n/g;
33 let treffer: RegExpExecArray | null;
34
35 while ((treffer = suche.exec(roh)) !== null) {
36 const start = treffer.index + treffer[0].length;
37 const ende = roh.indexOf('endstream', start);
38 if (ende < 0) continue;
39 let inhalt: string;
40 try {
41 inhalt = inflateSync(Buffer.from(roh.slice(start, ende), 'latin1')).toString('latin1');
42 } catch {
43 continue;
44 }
45 for (const stueck of inhalt.matchAll(/\((?:\\.|[^\\()])*\)\s*Tj/g)) {
46 teile.push(stueck[0].slice(1, stueck[0].lastIndexOf(')')));
47 }
48 }
49 // Lange Absaetze sind auf Zeilen umbrochen; fuer die Suche wieder
50 // zusammenziehen, sonst findet contains() den halben Satz nicht.
51 return teile.join(' ').replace(/\s+/g, ' ');
52 }
53
54 function projekt(anlagenart: Anlagenart): Project {
55 const basis = createStandardIntersectionProject('Hauptstrasse / Bahnhofstrasse', FIXED_DATE);
56 return { ...basis, anlagenart };
57 }
58
59 function ausdruck(project: Project, optionen = DEFAULT_PDF_OPTIONS): string {
60 const plan = buildSignalPlan(project);
61 const report = validateProject(project, plan, FIXED_DATE);
62 return pdfText(buildProjectPdf(project, plan, report, optionen, FIXED_DATE));
63 }
64
65 describe('Anlagenart im Ausdruck', () => {
66 it('nennt die Anlagenart auf dem Deckblatt', () => {
67 for (const art of ['knotenpunkt', 'fussgaengerschutzanlage', 'einstreifig'] as const) {
68 const text = ausdruck(projekt(art));
69 expect(text).toContain('Anlagenart');
70 // Ohne die Klammer, weil runde Klammern im PDF maskiert werden.
71 const bezeichnung = ANLAGENART_LABELS[art].split(' (')[0];
72 expect(text, art).toContain(bezeichnung);
73 }
74 });
75
76 it('nennt die Anlagenart auch ohne den Nachweisteil', () => {
77 // Der Nachweis der Kennwerte laesst sich abwaehlen, die Anlagenart nicht:
78 // Sie sagt, wonach der Plan zu beurteilen ist, und gehoert deshalb auf das
79 // Deckblatt und nicht in einen wahlweisen Anhang.
80 const text = ausdruck(projekt('einstreifig'), {
81 ...DEFAULT_PDF_OPTIONS,
82 includeSources: false,
83 });
84 // Ohne die Klammer, weil runde Klammern im PDF maskiert werden.
85 expect(text).toContain(ANLAGENART_LABELS.einstreifig.split(' (')[0]);
86 });
87
88 it('weist die Umlaufzeitgrenzen des Knotenpunkts als Sicherheitsgrenze aus', () => {
89 const text = ausdruck(projekt('knotenpunkt'));
90 expect(text).toContain('Schranken der Anlagenart');
91 expect(text).toContain('30 s bis 120 s');
92 expect(text).toContain('Sicherheitsgrenze');
93 // Die RSA hat mit einem Knotenpunkt nichts zu tun.
94 expect(text).not.toContain('Arbeitsstellen an Straßen');
95 });
96
97 it('sagt bei einstreifiger Fuehrung, dass die Grenzen nicht aus der RiLSA stammen', () => {
98 const text = ausdruck(projekt('einstreifig'));
99 expect(text).toContain('30 s bis 300 s');
100 expect(text).toContain('stammen NICHT aus der RiLSA');
101 expect(text).toContain('Arbeitsstellen an Straßen');
102 // Der Pruefstand muss die Zahl als nicht gesichert kennzeichnen.
103 expect(text).toContain('abzugleichen');
104 // Die 300 s sind hergeleitet und keine Zahl aus einem Regelwerk - der
105 // Ausdruck muss das sagen, sonst liest ein Pruefer sie als Regelwert.
106 expect(text).toContain('keine Zahl aus einem Regelwerk');
107 });
108
109 it('behauptet an einer Arbeitsstelle keinen Knotenpunkt', () => {
110 // Die Zeile mit dem Namen der Anlage trug die feste Beschriftung
111 // "Knotenpunkt". An einer Arbeitsstelle gibt es keinen, und eine
112 // Planunterlage darf keinen behaupten.
113 const basis = projekt('einstreifig');
114 const mitNamen: Project = {
115 ...basis,
116 intersection: { ...basis.intersection, name: 'Engstelle B441' },
117 };
118 const text = ausdruck(mitNamen);
119 expect(text).toContain('Bezeichnung der Anlage Engstelle B441');
120 expect(text).not.toContain('Knotenpunkt Engstelle B441');
121 });
122 });