lsa-planer

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

/ tests export pdfMessgrundlage.test.ts

6,7 KB Rohdatei
tests/export/pdfMessgrundlage.test.ts — 181 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 { messeignungSatz } from '@/domain/validation/rules';
7 import { messgenauigkeit, type Lageplan, type Planlinie } from '@/domain/geometrie/lageplan';
8 import { createStandardIntersectionProject } from '@/domain/model/factory';
9 import type { Project } from '@/domain/model/project';
10
11 /**
12 * Die Eignung der Messgrundlage im Ausdruck.
13 *
14 * Der Abschnitt "Planungsgrundlage" ist ein Nachweis: Ein Pruefer muss ihm
15 * entnehmen koennen, ob die Zwischenzeiten auf einer tauglichen Unterlage
16 * beruhen. Ein Luftbild sieht grob wie fein gleich aus - ohne die Zahl bliebe
17 * verborgen, dass eine Fussgaengerfurt auf einem 2000-m-Ausschnitt abgegriffen
18 * wurde und die Zwischenzeit deshalb zu kurz sein kann.
19 *
20 * Zugleich gilt fuer diesen Abschnitt: kein Satz darf mehr behaupten, als das
21 * Modell weiss. Woher ein einzelner Raeumweg stammt, weiss es nicht.
22 */
23
24 const FIXED_DATE = new Date('2026-01-01T12:00:00Z');
25
26 /**
27 * Genau auf der Fussgaenger-Schranke: 0,25 s / (2 * (1/1,0 + 1/11,1)) =
28 * 0,114669 m je Bildpunkt - der raeumende Fussgaenger gegen das einfahrende
29 * Kraftfahrzeug. Diese Unterlage ist gerade noch fuer alle Verkehrsarten
30 * geeignet.
31 */
32 const AN_DER_SCHRANKE = 0.25 / (2 * (1 / 1.0 + 1 / 11.1));
33
34 /** Sichtbaren Text aus dem PDF lesen; die Inhaltsstroeme sind komprimiert. */
35 function pdfText(bytes: Uint8Array): string {
36 const roh = new TextDecoder('latin1').decode(bytes);
37 const teile: string[] = [];
38 const suche = /stream\r?\n/g;
39 let treffer: RegExpExecArray | null;
40
41 while ((treffer = suche.exec(roh)) !== null) {
42 const start = treffer.index + treffer[0].length;
43 const ende = roh.indexOf('endstream', start);
44 if (ende < 0) continue;
45 let inhalt: string;
46 try {
47 inhalt = inflateSync(Buffer.from(roh.slice(start, ende), 'latin1')).toString('latin1');
48 } catch {
49 continue;
50 }
51 for (const stueck of inhalt.matchAll(/\((?:\\.|[^\\()])*\)\s*Tj/g)) {
52 teile.push(stueck[0].slice(1, stueck[0].lastIndexOf(')')));
53 }
54 }
55 // Lange Saetze sind auf Zeilen umbrochen; fuer die Suche wieder zusammenziehen.
56 return teile.join(' ').replace(/\s+/g, ' ');
57 }
58
59 function ausdruck(project: Project): string {
60 const plan = buildSignalPlan(project);
61 const report = validateProject(project, plan, FIXED_DATE);
62 return pdfText(buildProjectPdf(project, plan, report, DEFAULT_PDF_OPTIONS, FIXED_DATE));
63 }
64
65 function fahrlinie(id: string): Planlinie {
66 return {
67 id,
68 name: id,
69 mode: 'kfz',
70 movement: 'geradeaus',
71 breiteMeter: 3.25,
72 punkte: [
73 { x: 10, y: 10 },
74 { x: 10, y: 90 },
75 ],
76 haltlinieId: null,
77 startT: 0.5,
78 signalGroupId: null,
79 };
80 }
81
82 /** Unterlage mit einem Massstab von `meterJeBildpunkt` (Bezugsstrecke 100 Bildpunkte). */
83 function lageplan(meterJeBildpunkt: number | null): Lageplan {
84 return {
85 arbeitsbereiche: [],
86 signalgeber: [],
87 bild: {
88 datenUrl: 'data:image/png;base64,AAAA',
89 breite: 7000,
90 hoehe: 7000,
91 herkunft: 'Luftbild NRW - Kante 2000 m',
92 geladenAm: '2026-03-04T09:15:00.000Z',
93 },
94 kalibrierung:
95 meterJeBildpunkt === null
96 ? null
97 : {
98 von: { x: 0, y: 0 },
99 bis: { x: 100, y: 0 },
100 laengeMeter: meterJeBildpunkt * 100,
101 herkunft: 'georeferenziert',
102 },
103 haltlinien: [],
104 linien: [fahrlinie('l1')],
105 };
106 }
107
108 function mitLageplan(meterJeBildpunkt: number | null): Project {
109 const basis = createStandardIntersectionProject('Ortsdurchfahrt', FIXED_DATE);
110 return { ...basis, lageplan: lageplan(meterJeBildpunkt) };
111 }
112
113 describe('Eignung als Messgrundlage im Ausdruck', () => {
114 it('weist die Eignung mit der Zahl aus, an der sie haengt', () => {
115 // 2000 m Kantenlaenge auf 7000 Bildpunkten - der Ausschnitt der Gruenen
116 // Welle. Kfz-Wege bleiben abgreifbar, eine Furt nicht.
117 const text = ausdruck(mitLageplan(2000 / 7000));
118
119 expect(text).toContain('Eignung als Messgrundlage');
120 expect(text).toContain('Nur für Kraftfahrzeugströme geeignet');
121 expect(text).toContain('28,6 cm je Bildpunkt');
122 // 2 * 0,285714 m * (1/1,0 + 1/11,1) = 0,622909 s an der Furt: Ein
123 // Fussgaenger raeumt, ein Kraftfahrzeug faehrt ein.
124 expect(text).toContain('0,62 s');
125 });
126
127 it('druckt denselben Befund wie der Pruefbericht', () => {
128 // Zwei getrennt formulierte Befunde ueber dieselbe Unterlage waeren fuer
129 // einen Pruefer nicht gegeneinander pruefbar.
130 const project = mitLageplan(1.0);
131 const satz = messeignungSatz(messgenauigkeit(project.lageplan));
132 const text = ausdruck(project);
133 const bericht = validateProject(project, buildSignalPlan(project), FIXED_DATE);
134 const befund = bericht.findings.find(
135 (f) => f.rule === 'zwischenzeiten.messgrundlage-uebersicht',
136 );
137
138 expect(befund?.message).toContain(satz);
139 for (const stueck of satz.split(' ')) {
140 expect(text).toContain(stueck);
141 }
142 expect(text).toContain('Zum Vermessen nicht geeignet - Übersichtsbild');
143 });
144
145 it('behauptet ohne Massstab keine Eignung', () => {
146 const text = ausdruck(mitLageplan(null));
147
148 expect(text).toContain('Eignung als Messgrundlage');
149 expect(text).toContain('Nicht beurteilbar');
150 });
151
152 it('erklaert, worauf die Beurteilung beruht', () => {
153 const text = ausdruck(mitLageplan(2000 / 7000));
154
155 expect(text).toContain('Angesetzt ist ein Zeichenfehler von 2 Bildpunkten');
156 expect(text).toContain('0,25 s');
157 });
158
159 it('behauptet nicht, die eingetragenen Wege stammten aus dieser Unterlage', () => {
160 const text = ausdruck(mitLageplan(2000 / 7000));
161
162 expect(text).toContain('nicht, welche Wege tatsächlich hier abgegriffen wurden');
163 });
164
165 it('folgt den Kennwerten des Projekts, nicht dem Regelwerk unmittelbar', () => {
166 // 0,114669 m je Bildpunkt halten die Schranke genau ein. Setzt der
167 // Bearbeiter die Raeumgeschwindigkeit des mobilitaetseingeschraenkten
168 // Fussgaengers auf 0,6 m/s herab, tut es dieselbe Unterlage nicht mehr
169 // (0,402893 s statt 0,25 s) - und der Ausdruck muss das zeigen. Der
170 // Kennwert clearingSpeedFuss, der hier frueher stand, geht in die
171 // Einstufung nicht mehr ein und wuerde am Ausdruck nichts aendern.
172 const project = mitLageplan(AN_DER_SCHRANKE);
173 const streng: Project = {
174 ...project,
175 settings: { ...project.settings, rilsa: { clearingSpeedFussReducedMobility: 0.6 } },
176 };
177
178 expect(ausdruck(project)).toContain('Für alle Verkehrsarten geeignet');
179 expect(ausdruck(streng)).toContain('Nur für Kraftfahrzeugströme geeignet');
180 });
181 });