lsa-planer

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

/ tests export fundstellenRegelwert.test.ts

6,0 KB Rohdatei
tests/export/fundstellenRegelwert.test.ts — 166 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 { toWinAnsi } from '@/render/pdfSurface';
8 import type { Project } from '@/domain/model/project';
9
10 /*
11 * Befund 24: Das gedruckte Fundstellenverzeichnis speiste seine Kennwertzeilen
12 * aus `plan.defaults` - das sind die AUFGELOESTEN Kennwerte einschliesslich
13 * aller Vorgaben des Anwenders - und druckte diesen Wert in die Spalte
14 * "Regelwert", mit dem Pruefstand der Quelle daneben.
15 *
16 * Was der Anwender sonst erlebt: Er traegt unter "Vorgaben" eine Gelbzeit von
17 * 4 s ein (die RiLSA nennt 3 s) und druckt die Planunterlage. Im Abschnitt
18 * "Abweichungen von den Regelwerten" steht dann "Gelbzeit Kfz bis 50 km/h |
19 * 3 | 4 | s", im Fundstellenverzeichnis desselben PDF unter der
20 * Spaltenueberschrift "Regelwert" aber 4 s - Regelwerk "RiLSA 2015",
21 * Pruefstand "gesichert". Der VwV-StVO-Eintrag desselben Verzeichnisses
22 * kennzeichnet die Abweichung sogar ausdruecklich. Vor der Behoerde erscheint
23 * ein selbst gesetzter Wert damit als gesicherter Wert des Regelwerks.
24 */
25
26 const STICHTAG = new Date('2026-01-01T12:00:00Z');
27
28 const PAGE_MARGIN = 36;
29 const CONTENT_WIDTH = 841.89 - 2 * PAGE_MARGIN;
30 /*
31 * Die Spalten des Fundstellenverzeichnisses, aus den festen Breiten des
32 * Ausdrucks gerechnet: Regelwerk 70, Kennwert (Rest), Regelwert 62,
33 * Sachgebiet 175, Fundstelle 110, Pruefstand 62.
34 */
35 const KENNWERT_X = PAGE_MARGIN + 70;
36 const KENNWERT_BREITE = CONTENT_WIDTH - (70 + 62 + 175 + 110 + 62);
37 const REGELWERT_X = KENNWERT_X + KENNWERT_BREITE;
38 const REGELWERT_ENDE = REGELWERT_X + 62;
39 /** Innenabstand der Tabellenzellen dieses Verzeichnisses. */
40 const ZELLENRAND = 2.5;
41
42 interface Stueck {
43 readonly blatt: number;
44 readonly x: number;
45 readonly y: number;
46 readonly text: string;
47 }
48
49 function entpacke(roh: string, von: number): string | null {
50 const anfang = roh.indexOf('stream', von);
51 if (anfang < 0) return null;
52 const start = anfang + (roh.startsWith('stream\r\n', anfang) ? 8 : 7);
53 const ende = roh.indexOf('endstream', start);
54 if (ende < 0) return null;
55 try {
56 return inflateSync(Buffer.from(roh.slice(start, ende), 'latin1')).toString('latin1');
57 } catch {
58 return null;
59 }
60 }
61
62 function textstuecke(bytes: Uint8Array): readonly Stueck[] {
63 const roh = new TextDecoder('latin1').decode(bytes);
64 const raus: Stueck[] = [];
65 let blatt = 0;
66
67 for (const treffer of roh.matchAll(/\/Type \/Page\b[^>]*?\/Contents (\d+) 0 R/g)) {
68 const objekt = roh.indexOf(`\n${treffer[1]!} 0 obj`);
69 const inhalt = objekt < 0 ? null : entpacke(roh, objekt);
70 if (inhalt === null) continue;
71 blatt += 1;
72 let x = 0;
73 let y = 0;
74 let abstand = 0;
75 for (const roheZeile of inhalt.split('\n')) {
76 const zeile = roheZeile.trim();
77 const tl = /^(-?[\d.]+) TL$/.exec(zeile);
78 if (tl) {
79 abstand = Number(tl[1]);
80 continue;
81 }
82 const td = /^(-?[\d.]+) (-?[\d.]+) Td$/.exec(zeile);
83 if (td) {
84 x = Number(td[1]);
85 y = Number(td[2]);
86 continue;
87 }
88 let rest = zeile;
89 while (rest.startsWith('T*')) {
90 y -= abstand;
91 rest = rest.slice(2).trim();
92 }
93 const tj = /^\(((?:[^()\\]|\\.)*)\)\s*Tj$/.exec(rest);
94 if (tj) raus.push({ blatt, x, y, text: tj[1]!.replace(/\\([()\\])/g, '$1') });
95 }
96 }
97 return raus;
98 }
99
100 /** Knotenpunkt mit einer selbst gesetzten Gelbzeit von 4 s (Regelwert 3 s). */
101 function mitVorgabe(): Project {
102 const basis = createStandardIntersectionProject('Musterkreuzung', STICHTAG);
103 return {
104 ...basis,
105 settings: { ...basis.settings, rilsa: { ...basis.settings.rilsa, yellowKfzUpTo50: 4 } },
106 };
107 }
108
109 function unterlage(projekt: Project): readonly Stueck[] {
110 const plan = buildSignalPlan(projekt);
111 return textstuecke(
112 buildProjectPdf(
113 projekt,
114 plan,
115 validateProject(projekt, plan, STICHTAG),
116 DEFAULT_PDF_OPTIONS,
117 STICHTAG,
118 ),
119 );
120 }
121
122 /** Die Zelle "Regelwert" der Zeile, deren Kennwertzelle mit `label` beginnt. */
123 function regelwertzelle(stuecke: readonly Stueck[], label: string): readonly string[] {
124 const kennwert = stuecke.filter(
125 (s) => Math.abs(s.x - (KENNWERT_X + ZELLENRAND)) < 0.5 && s.text.startsWith(label),
126 );
127 expect(kennwert.length, `Vorbedingung: Zeile "${label}" im Fundstellenverzeichnis`).toBe(1);
128 const zeile = kennwert[0]!;
129 return stuecke
130 .filter((s) => s.blatt === zeile.blatt && Math.abs(s.y - zeile.y) < 0.5)
131 .filter((s) => s.x >= REGELWERT_X && s.x < REGELWERT_ENDE)
132 .map((s) => s.text);
133 }
134
135 describe('Regelwertspalte des Fundstellenverzeichnisses', () => {
136 it('druckt unter "Regelwert" den Wert des Regelwerks, nicht die Vorgabe des Anwenders', () => {
137 const stuecke = unterlage(mitVorgabe());
138
139 expect(regelwertzelle(stuecke, toWinAnsi('Gelbzeit Kfz bis 50 km/h'))).toEqual(['3 s']);
140 });
141
142 it('verschweigt den angesetzten Wert nicht, sondern kennzeichnet ihn als Abweichung', () => {
143 const stuecke = unterlage(mitVorgabe());
144 const kennwert = stuecke.filter(
145 (s) =>
146 Math.abs(s.x - (KENNWERT_X + ZELLENRAND)) < 0.5 &&
147 s.text.startsWith(toWinAnsi('Gelbzeit Kfz bis 50 km/h')),
148 );
149
150 expect(kennwert.map((s) => s.text)).toEqual([
151 toWinAnsi('Gelbzeit Kfz bis 50 km/h (angesetzt: 4 s, abweichend vom Regelwert)'),
152 ]);
153 });
154
155 it('laesst die Zeile unveraendert, solange keine Vorgabe gesetzt ist', () => {
156 const stuecke = unterlage(createStandardIntersectionProject('Musterkreuzung', STICHTAG));
157
158 expect(regelwertzelle(stuecke, toWinAnsi('Gelbzeit Kfz bis 50 km/h'))).toEqual(['3 s']);
159 const kennwert = stuecke.filter(
160 (s) =>
161 Math.abs(s.x - (KENNWERT_X + ZELLENRAND)) < 0.5 &&
162 s.text.startsWith(toWinAnsi('Gelbzeit Kfz bis 50 km/h')),
163 );
164 expect(kennwert.map((s) => s.text)).toEqual([toWinAnsi('Gelbzeit Kfz bis 50 km/h')]);
165 });
166 });