lsa-planer

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

/ tests export signalgeberblatt.test.ts

6,9 KB Rohdatei
tests/export/signalgeberblatt.test.ts — 174 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 { signalgeberBefunde } from '@/domain/validation/rules';
7 import * as actions from '@/app/actions';
8 import { ASSISTENT_VORGABE, projektAusAngaben } from '@/ui/assistent';
9 import type { Project } from '@/domain/model/project';
10 import type { Signalgeber } from '@/domain/geometrie/lageplan';
11
12 /*
13 * STUFE 4 DES SIGNALLAGEPLAN-ENTWURFS: DAS DRITTE BLATT UND DIE UMGEDREHTE
14 * ZUSAGE.
15 *
16 * Die Zeile "Bemasster Signallageplan - NICHT Bestandteil" ist die Behebung
17 * des Befundes C14 und die einzige Stelle, an der die Unterlage ueber sich
18 * selbst die unangenehme Auskunft gibt. Wer sie zu einem "ja" macht, richtet
19 * das Vertrauen eines Pruefers danach - und ein Signallageplan ohne Bauform,
20 * Hoehe, Blendenmass, Sichtpruefung und Restbreitennachweis ist keiner.
21 *
22 * DESHALB WIRD SIE DREIWERTIG UND NICHT ZWEIWERTIG. Diese Datei bewacht genau
23 * das: dass der Satz mit dem beginnt, was FEHLT, dass er die fuenf fehlenden
24 * Angaben beim Namen nennt, und dass ohne erfasste Signalgeber alles bleibt,
25 * wie es war.
26 */
27
28 const DATUM = new Date('2026-09-12T11:00:00Z');
29
30 const GEBER: Signalgeber = {
31 id: 'sg-1',
32 name: 'K1',
33 punkt: { x: 60, y: 40 },
34 ausrichtung: 0,
35 signalGroupId: null,
36 };
37
38 function pdfText(bytes: Uint8Array): string {
39 const roh = new TextDecoder('latin1').decode(bytes);
40 const teile: string[] = [];
41 const suche = /stream\r?\n/g;
42 let treffer: RegExpExecArray | null;
43 while ((treffer = suche.exec(roh)) !== null) {
44 const start = treffer.index + treffer[0].length;
45 const ende = roh.indexOf('endstream', start);
46 if (ende < 0) continue;
47 let inhalt: string;
48 try {
49 inhalt = inflateSync(Buffer.from(roh.slice(start, ende), 'latin1')).toString('latin1');
50 } catch {
51 continue;
52 }
53 for (const stueck of inhalt.matchAll(/\((?:\\.|[^\\()])*\)\s*Tj/g)) {
54 teile.push(stueck[0].slice(1, stueck[0].lastIndexOf(')')));
55 }
56 }
57 return teile.join(' ').replace(/\s+/g, ' ');
58 }
59
60 /** Eine Engstelle aus dem gefuehrten Einstieg, wahlweise mit Signalgebern. */
61 function projekt(geber: readonly Signalgeber[] = []): Project {
62 const basis = projektAusAngaben(
63 { ...ASSISTENT_VORGABE, form: 'einstreifig' },
64 'Arbeitsstelle Musterweg',
65 DATUM,
66 );
67 return geber.reduce((p, g) => actions.addSignalgeber(p, g), basis);
68 }
69
70 function ausdruck(p: Project): string {
71 const plan = buildSignalPlan(p);
72 return pdfText(
73 buildProjectPdf(p, plan, validateProject(p, plan, DATUM), DEFAULT_PDF_OPTIONS, DATUM),
74 );
75 }
76
77 describe('Die Bestandteilezeile zum Signallageplan', () => {
78 it('sagt ohne erfasste Signalgeber genau das, was sie vorher sagte', () => {
79 const text = ausdruck(projekt());
80 expect(text).toContain('Bemaßter Signallageplan');
81 expect(text).toContain('NICHT Bestandteil dieser Unterlage');
82 expect(text).not.toContain('TEILWEISE');
83 });
84
85 it('wird mit erfassten Signalgebern dreiwertig und nennt die Zahl', () => {
86 const text = ausdruck(projekt([GEBER]));
87 expect(text).toContain('TEILWEISE');
88 expect(text).toContain('1 Signalgeberstandort');
89 });
90
91 it('nennt die fünf Angaben, die weiterhin fehlen', () => {
92 /*
93 * Der Kern. Ein "teilweise" ohne Aufzaehlung waere eine Andeutung; ein
94 * Pruefer soll lesen koennen, WAS ihm fehlt, ohne es zu erraten.
95 */
96 const text = ausdruck(projekt([GEBER]));
97 for (const angabe of ['Bauform', 'Höhe', 'Blendenmaß', 'Sichtprüfung', 'Fahrbahnbreite']) {
98 expect(text, `die Zeile verschweigt: ${angabe}`).toContain(angabe);
99 }
100 });
101
102 it('beginnt der Satz mit dem Fehlenden und nicht mit dem Vorhandenen', () => {
103 // Wer die Zeile ueberfliegt, liest den Anfang. Stuende dort die Zahl der
104 // Signalgeber, laese er "enthalten".
105 const text = ausdruck(projekt([GEBER]));
106 const zeile = text.slice(text.indexOf('TEILWEISE'));
107 expect(zeile.indexOf('Nicht enthalten')).toBeLessThan(zeile.indexOf('Bauform') + 1);
108 expect(text.indexOf('TEILWEISE')).toBeLessThan(text.indexOf('1 Signalgeberstandort') + 1);
109 });
110
111 it('druckt das dritte Blatt nur, wenn Signalgeber erfasst sind', () => {
112 expect(ausdruck(projekt())).not.toContain('Lageplan mit Signalgeberstandorten');
113 // Ohne Bildgrundlage gibt es gar kein Lageplanblatt; das Projekt des
114 // gefuehrten Einstiegs fuehrt keines. Geprueft wird deshalb die
115 // Bestandteilezeile, die es in jedem Fall gibt.
116 expect(ausdruck(projekt([GEBER]))).toContain('TEILWEISE');
117 });
118 });
119
120 describe('Die Prüfregeln zu den Signalgeberstandorten', () => {
121 it('schweigen, solange keiner erfasst ist', () => {
122 expect(signalgeberBefunde(projekt())).toEqual([]);
123 });
124
125 it('melden eine Signalgruppe ohne Signalgeber', () => {
126 const befunde = signalgeberBefunde(projekt([GEBER]));
127 const befund = befunde.find((b) => b.rule === 'lageplan.gruppe-ohne-signalgeber');
128 expect(befund, 'kein Befund zur Gruppe ohne Geber').toBeDefined();
129 expect(befund!.severity).toBe('hinweis');
130 });
131
132 it('melden einen Signalgeber ohne Signalgruppe', () => {
133 const befund = signalgeberBefunde(projekt([GEBER])).find(
134 (b) => b.rule === 'lageplan.signalgeber-ohne-gruppe',
135 );
136 expect(befund).toBeDefined();
137 expect(befund!.message).toContain('K1');
138 });
139
140 it('warnen bei doppelter Kennung', () => {
141 const befund = signalgeberBefunde(
142 projekt([GEBER, { ...GEBER, id: 'sg-2', punkt: { x: 90, y: 40 } }]),
143 ).find((b) => b.rule === 'lageplan.signalgeber-kennung-doppelt');
144 expect(befund, 'doppelte Kennung nicht gemeldet').toBeDefined();
145 expect(befund!.severity).toBe('warnung');
146 });
147
148 it('prüfen keinen Abstand zur Haltlinie', () => {
149 /*
150 * DIE REGEL, DIE ES NICHT GIBT, UND WARUM. Ein Sollabstand von 3 bis 5 m
151 * ist in keinem vorliegenden Regelwerk nachgewiesen; wer ihn als Prüfung
152 * liest und die Haltlinie naeher an den Geber setzt, verkuerzt an der
153 * Engstelle den Raeumweg und damit die Zwischenzeit. Die gefaehrliche
154 * Richtung.
155 */
156 const kennungen = signalgeberBefunde(projekt([GEBER])).map((b) => b.rule);
157 expect(kennungen).not.toContain('lageplan.signalgeber-abstand');
158 const texte = signalgeberBefunde(projekt([GEBER]))
159 .map((b) => `${b.message} ${b.suggestion ?? ''}`)
160 .join(' ');
161 expect(texte, 'eine Abstandsspanne steht in der Meldung').not.toMatch(
162 /3[,.]0?\s*(bis|–|-)\s*5/,
163 );
164 });
165
166 it('sperren die Ausgabe nicht', () => {
167 // Der Signalgeber geht in keine Rechnung ein; nichts an ihm kann einen
168 // Plan falsch machen. Ein Fehler waere hier eine Anmassung.
169 const schweren = signalgeberBefunde(projekt([GEBER, { ...GEBER, id: 'sg-2' }])).map(
170 (b) => b.severity,
171 );
172 expect(schweren).not.toContain('fehler');
173 });
174 });