lsa-planer

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

/ tests export pdfHerkunftsangaben.test.ts

7,0 KB Rohdatei
tests/export/pdfHerkunftsangaben.test.ts — 179 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 { REGELBREITE } from '@/domain/geometrie/vermessung';
8 import type { Lageplan, Planlinie } from '@/domain/geometrie/lageplan';
9 import type { Project } from '@/domain/model/project';
10
11 /**
12 * Ehrlichkeit der Herkunftsangaben im Ausdruck.
13 *
14 * Das Modell fuehrt nicht mit, woher Raeum- und Einfahrweg einer
15 * Konfliktbeziehung stammen: `Conflict.note` ist ein freies Bemerkungsfeld.
16 * uebernimmWege hinterlaesst dort zwar "Aus dem Lageplan vermessen", doch der
17 * Bearbeiter kann den Text ueberschreiben, und vor allem bleibt er stehen, wenn
18 * er anschliessend die Wege von Hand aendert - die Massenuebernahme im
19 * Konfliktdialog tut genau das.
20 *
21 * Ein Ausdruck, der diesen Text als Herkunftsnachweis ausgibt, macht in einer
22 * Unterlage fuer ein verkehrsbehoerdliches Verfahren eine falsche Angabe zur
23 * Herkunft eines sicherheitsrelevanten Masses. Diese Tests halten fest, dass
24 * der Ausdruck nur behauptet, was das Modell hergibt.
25 */
26
27 const FIXED_DATE = new Date('2026-01-01T12:00:00Z');
28
29 /** Sichtbaren Text aus dem PDF lesen; die Inhaltsstroeme sind komprimiert. */
30 function pdfText(bytes: Uint8Array): string {
31 const roh = new TextDecoder('latin1').decode(bytes);
32 const teile: string[] = [];
33 const suche = /stream\r?\n/g;
34 let treffer: RegExpExecArray | null;
35
36 while ((treffer = suche.exec(roh)) !== null) {
37 const start = treffer.index + treffer[0].length;
38 const ende = roh.indexOf('endstream', start);
39 if (ende < 0) continue;
40 let inhalt: string;
41 try {
42 inhalt = inflateSync(Buffer.from(roh.slice(start, ende), 'latin1')).toString('latin1');
43 } catch {
44 continue;
45 }
46 for (const stueck of inhalt.matchAll(/\((?:\\.|[^\\()])*\)\s*Tj/g)) {
47 teile.push(stueck[0].slice(1, stueck[0].lastIndexOf(')')));
48 }
49 }
50 // Lange Absaetze sind auf Zeilen umbrochen; fuer die Suche wieder
51 // zusammenziehen, sonst findet contains() den halben Satz nicht.
52 return teile.join(' ').replace(/\s+/g, ' ');
53 }
54
55 function ausdruck(project: Project): string {
56 const plan = buildSignalPlan(project);
57 const report = validateProject(project, plan, FIXED_DATE);
58 return pdfText(
59 buildProjectPdf(
60 project,
61 plan,
62 report,
63 { ...DEFAULT_PDF_OPTIONS, includeCalculationDetails: true },
64 FIXED_DATE,
65 ),
66 );
67 }
68
69 /** Fahrlinie mit zwei Punkten - genug, damit der Lageplan auswertbar ist. */
70 function fahrlinie(id: string): Planlinie {
71 return {
72 id,
73 name: id,
74 mode: 'kfz',
75 movement: 'geradeaus',
76 breiteMeter: REGELBREITE.kfz,
77 punkte: [
78 { x: 10, y: 10 },
79 { x: 10, y: 90 },
80 ],
81 haltlinieId: null,
82 startT: 0.5,
83 signalGroupId: null,
84 };
85 }
86
87 const AUSWERTBARER_LAGEPLAN: Lageplan = {
88 arbeitsbereiche: [],
89 signalgeber: [],
90 bild: {
91 datenUrl: 'data:image/png;base64,AAAA',
92 breite: 800,
93 hoehe: 600,
94 herkunft: 'Luftbild NRW · Land NRW · Kante 400 m',
95 geladenAm: '2026-03-04T09:15:00.000Z',
96 },
97 kalibrierung: {
98 von: { x: 100, y: 200 },
99 bis: { x: 200, y: 200 },
100 laengeMeter: 50,
101 herkunft: 'gemessen',
102 },
103 haltlinien: [],
104 linien: [fahrlinie('l1'), fahrlinie('l2')],
105 };
106
107 describe('Herkunftsangaben im Ausdruck', () => {
108 it('weist die Bemerkung einer Beziehung als Bemerkung aus, nicht als Herkunft', () => {
109 const text = ausdruck(createStandardIntersectionProject('Testknoten', FIXED_DATE));
110
111 expect(text).toContain(
112 'Die Spalte "Bemerkung" gibt daneben den freien Vermerk des Bearbeiters',
113 );
114 expect(text).toContain('Sie ist kein Nachweis der Herkunft');
115 // Der Vorgaenger erhob den freien Text zum Nachweis der Vermessung.
116 expect(text).not.toContain('setzt die Übernahme aus dem Lageplan');
117 // Der Nachweis steht jetzt in einer EIGENEN Spalte, die das Modell fuehrt.
118 expect(text).toContain('Herkunft der Wege');
119 expect(text).toContain('Das Programm führt die Angabe selbst und lässt sie nicht beschreiben');
120 });
121
122 it('erfindet fuer eine leere Bemerkung keine Herkunft', () => {
123 // Die Beziehungen des Regelknotenpunkts tragen keine Bemerkung. Ihre Wege
124 // stammen aus der Vorlage, nicht aus der Hand des Bearbeiters - "von Hand
125 // erfasst" waere schon hier falsch.
126 const text = ausdruck(createStandardIntersectionProject('Testknoten', FIXED_DATE));
127
128 expect(text).not.toContain('von Hand erfasst');
129 });
130
131 it('behauptet keine Vermessung, wenn der Weg nach der Uebernahme geaendert wurde', () => {
132 const basis = createStandardIntersectionProject('Testknoten', FIXED_DATE);
133 const erster = basis.conflicts[0];
134 expect(erster).toBeDefined();
135 if (erster === undefined) return;
136
137 // Genau der gefaehrliche Ablauf: aus dem Lageplan uebernommen, danach den
138 // Raeumweg von Hand verlaengert. Die Bemerkung bleibt stehen.
139 const project: Project = {
140 ...basis,
141 conflicts: basis.conflicts.map((c) =>
142 c.id === erster.id
143 ? { ...c, note: 'Aus dem Lageplan vermessen.', clearingDistance: 41 }
144 : c,
145 ),
146 };
147
148 const text = ausdruck(project);
149
150 // Die Bemerkung darf erscheinen - aber nur als Bemerkung, mit dem Hinweis,
151 // dass sie aelter sein kann als der Wert daneben.
152 expect(text).toContain('Aus dem Lageplan vermessen.');
153 expect(text).toContain('kann aus der Zeit vor einer späteren Änderung der Wege stammen');
154 // Und die Spalte, die den Nachweis traegt, darf sich davon nicht
155 // anstecken lassen: Die Beziehungen des Regelknotenpunkts sind nicht aus
156 // einem Lageplan uebernommen worden, egal was in der Bemerkung steht.
157 expect(text).toContain('Herkunft nicht nachgewiesen');
158 });
159
160 it('behauptet in der Planungsgrundlage nicht, die Masse stammten aus der Unterlage', () => {
161 const basis = createStandardIntersectionProject('Testknoten', FIXED_DATE);
162 const text = ausdruck({ ...basis, lageplan: AUSWERTBARER_LAGEPLAN });
163
164 expect(text).toContain('Der Lageplan ist maßstäblich auswertbar');
165 // Seit das Modell die Herkunft je Beziehung fuehrt, waere der alte Satz
166 // ("Das Projekt fuehrt die Herkunft der einzelnen Masse nicht mit") falsch.
167 expect(text).not.toContain('führt die Herkunft der einzelnen Maße nicht mit');
168 expect(text).toContain('führt das Projekt je Beziehung mit');
169 // Der Vorgaenger verwies fuer die Uebernahme auf die Bemerkungsspalte.
170 expect(text).not.toContain('Welche Beziehungen daraus übernommen wurden');
171 });
172
173 it('behauptet ohne Lageplan nicht, die Wege seien von Hand erfasst', () => {
174 const text = ausdruck(createStandardIntersectionProject('Testknoten', FIXED_DATE));
175
176 expect(text).toContain('Diesen Unterlagen liegt kein Lageplan und kein Luftbild bei');
177 expect(text).toContain('stammen daher nicht aus diesen Unterlagen');
178 });
179 });