lsa-planer

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

/ tests export angesetzteSchranken.test.ts

8,5 KB Rohdatei
tests/export/angesetzteSchranken.test.ts — 198 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_GRENZEN } from '@/domain/rilsa/constants';
8 import { PRUEFSTAND_LABELS } from '@/domain/rilsa/quellen';
9 import { toWinAnsi } from '@/render/pdfSurface';
10 import type { AnlagenartGrenzen } from '@/domain/rilsa/constants';
11 import type { Project } from '@/domain/model/project';
12 import * as fmt from '@/ui/format';
13
14 /*
15 * BEFUND 7 (Fassung 5.11.0): Der Abschnitt "Schranken der Anlagenart" las seine
16 * Grenzen aus der Konstanten ANLAGENART_GRENZEN statt aus `plan.grenzen`.
17 *
18 * Was der Anwender sonst erlebt: Er traegt in der Vorgabenverwaltung eine
19 * engere Umlaufzeit ein (45 bis 100 s). Der Pruefbericht misst daran
20 * (engine.ts: `plan.grenzen.cycleTime`), die Aufstellung "Abweichungen von den
21 * Regelwerten" fuehrt die Vorgabe als Abweichung - und zwei Seiten weiter
22 * druckt die Spalte "Angesetzt" der Schrankentabelle "30 s bis 120 s" mit dem
23 * Pruefstand "gesichert". Ein Pruefer bekommt zwei Zahlenwerke in EINER
24 * Anordnungsunterlage und kann sie nicht zur Deckung bringen; zugleich verweist
25 * quellen.ts im selben Dokument ausdruecklich auf diesen Abschnitt als die
26 * Stelle, die die WIRKSAME Schranke nennt.
27 *
28 * Der Satz unter der Tabelle bleibt beim Regelwerkswert: Die 120 s der RiLSA
29 * sind als Sicherheitsgrenze begruendet, eine selbst gesetzte Obergrenze traegt
30 * diese Begruendung nicht (Grundsatz: selbstgesetzte Schwellen tragen nie
31 * "gesichert").
32 */
33
34 const STICHTAG = new Date('2026-01-01T12:00:00Z');
35
36 /** Sichtbaren Text aus dem PDF lesen; die Inhaltsstroeme sind komprimiert. */
37 function pdfText(bytes: Uint8Array): string {
38 const roh = new TextDecoder('latin1').decode(bytes);
39 const teile: string[] = [];
40 const suche = /stream\r?\n/g;
41 let treffer: RegExpExecArray | null;
42
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 t of inhalt.matchAll(/\(((?:[^()\\]|\\.)*)\)\s*Tj/g)) {
54 teile.push(t[1]!.replace(/\\([()\\])/g, '$1'));
55 }
56 }
57 return teile.join(' ').replace(/\s+/g, ' ');
58 }
59
60 function projekt(vorgaben: Project['settings']['rilsa']): Project {
61 const basis = createStandardIntersectionProject('Hauptstrasse / Bahnhofstrasse', STICHTAG);
62 return {
63 ...basis,
64 settings: { ...basis.settings, rilsa: { ...basis.settings.rilsa, ...vorgaben } },
65 };
66 }
67
68 function ausdruck(vorgaben: Project['settings']['rilsa']): {
69 text: string;
70 grenzen: ReturnType<typeof buildSignalPlan>['grenzen'];
71 } {
72 const project = projekt(vorgaben);
73 const plan = buildSignalPlan(project);
74 const bericht = validateProject(project, plan, STICHTAG);
75 return {
76 text: pdfText(buildProjectPdf(project, plan, bericht, DEFAULT_PDF_OPTIONS, STICHTAG)),
77 grenzen: plan.grenzen,
78 };
79 }
80
81 const gedruckt = (text: string): string => toWinAnsi(text).replace(/\s+/g, ' ');
82
83 const REGELWERK = ANLAGENART_GRENZEN.knotenpunkt;
84
85 const spanne = (g: AnlagenartGrenzen, welche: 'zulaessig' | 'regelbereich'): string =>
86 welche === 'zulaessig'
87 ? `${fmt.seconds(g.cycleTime.min)} bis ${fmt.seconds(g.cycleTime.max)}`
88 : `${fmt.seconds(g.cycleTime.recommendedMin)} bis ${fmt.seconds(g.cycleTime.recommendedMax)}`;
89
90 /*
91 * NACHTRAG ZU BEFUND 7: Der angesetzte Wert allein genuegt nicht. Grundlage
92 * und Pruefstand derselben Zeile kommen aus `grenzen.herkunft`, und das ist
93 * unveraendert die Herkunft der Anlagenart - neben einer eingeengten Umlaufzeit
94 * stand damit "RiLSA 2015 ... (30 bis 120 s)" und der Pruefstand "gesichert".
95 * Geprueft wird deshalb die GANZE Zeile: Wo der angesetzte Wert vom Regelwert
96 * abweicht, traegt der Zeilenkopf den Vermerk, und erst danach folgen Wert,
97 * Grundlage und Pruefstand.
98 */
99 function zeile(
100 kopf: string,
101 grenzen: AnlagenartGrenzen,
102 welche: 'zulaessig' | 'regelbereich',
103 ): string {
104 const angesetzt = spanne(grenzen, welche);
105 const regelwert = spanne(REGELWERK, welche);
106 const vermerk =
107 angesetzt === regelwert ? '' : ` (eigene Vorgabe, abweichend vom Regelwert ${regelwert})`;
108 const h =
109 welche === 'zulaessig' ? REGELWERK.herkunft.umlaufzeit : REGELWERK.herkunft.regelbereich;
110 return gedruckt(`${kopf}${vermerk} ${angesetzt} ${h.text} ${PRUEFSTAND_LABELS[h.pruefstand]}`);
111 }
112
113 describe('Befund 7 - Spalte "Angesetzt" der Schrankentabelle', () => {
114 it('druckt die Umlaufzeitschranken dieses Plans, nicht die der Anlagenart', () => {
115 const { text, grenzen } = ausdruck({ cycleTimeMin: 45, cycleTimeMax: 100 });
116
117 expect(grenzen.cycleTime.min, 'Vorbedingung: die Vorgabe wirkt').toBe(45);
118 expect(grenzen.cycleTime.max, 'Vorbedingung: die Vorgabe wirkt').toBe(100);
119
120 const angesetzt = spanne(grenzen, 'zulaessig');
121 const regelwerk = spanne(REGELWERK, 'zulaessig');
122
123 expect(text, `die Spalte "Angesetzt" nennt ${angesetzt}`).toContain(gedruckt(angesetzt));
124 expect(text, `nicht die Werte der Anlagenart (${regelwerk})`).not.toContain(
125 gedruckt(`Umlaufzeit, zulässig ${regelwerk}`),
126 );
127 // Die eingeengte Zeile steht nicht ohne Kennzeichnung neben "gesichert".
128 expect(text, 'Kennwert samt Vermerk, dann Wert, Grundlage und Pruefstand').toContain(
129 zeile('Umlaufzeit, zulässig', grenzen, 'zulaessig'),
130 );
131 });
132
133 it('druckt den empfohlenen Bereich dieses Plans, auch wenn nur die Obergrenze vorgegeben ist', () => {
134 // Schon eine eingetragene Obergrenze allein verschiebt den Regelbereich
135 // ueber clamp() (signalPlan.ts): gemessen 60 bis 60 s bei gedruckten
136 // 60 bis 90 s.
137 const { text, grenzen } = ausdruck({ cycleTimeMax: 60 });
138
139 expect(grenzen.cycleTime.recommendedMax, 'Vorbedingung: der Regelbereich wird eingeengt').toBe(
140 60,
141 );
142
143 expect(text).toContain(gedruckt('Umlaufzeit, empfohlener Bereich (eigene Vorgabe'));
144 // Beide Zeilen sind hier verschoben: die zulaessige Obergrenze durch die
145 // Vorgabe selbst, der Regelbereich durch clamp() - und beide tragen den
146 // Vermerk vor Wert, Grundlage und Pruefstand.
147 expect(text).toContain(zeile('Umlaufzeit, empfohlener Bereich', grenzen, 'regelbereich'));
148 expect(text).toContain(zeile('Umlaufzeit, zulässig', grenzen, 'zulaessig'));
149 });
150
151 it('vermerkt keine Abweichung, wo keine ist, und sagt es im Einleitungsabsatz', () => {
152 const { text, grenzen } = ausdruck({});
153
154 expect(grenzen.cycleTime, 'Vorbedingung: ohne Vorgabe gelten die Werte der Anlagenart').toEqual(
155 REGELWERK.cycleTime,
156 );
157 expect(text, 'ohne eigene Vorgabe steht kein Vermerk in der Tabelle').not.toContain(
158 gedruckt('(eigene Vorgabe, abweichend vom Regelwert'),
159 );
160 expect(text).toContain(zeile('Umlaufzeit, zulässig', grenzen, 'zulaessig'));
161 expect(
162 text,
163 'der Einleitungsabsatz sagt, wofuer Grundlage und Pruefstand bei eigener Vorgabe stehen',
164 ).toContain(
165 gedruckt(
166 'Steht unter "Angesetzt" eine eigene Vorgabe aus der Vorgabenverwaltung, ist das an der ' +
167 'Zeile vermerkt: Grundlage und Prüfstand nennen dann weiterhin den Regelwert, von dem ' +
168 'die Vorgabe abweicht, und nicht die Herkunft des angesetzten Wertes.',
169 ),
170 );
171 });
172
173 it('nennt die Sicherheitsgrenze der RiLSA weiter mit ihrem Regelwert und die eigene Vorgabe daneben', () => {
174 const { text } = ausdruck({ cycleTimeMax: 100 });
175 const regelwert = fmt.seconds(ANLAGENART_GRENZEN.knotenpunkt.cycleTime.max);
176
177 expect(
178 text,
179 'die Begruendung "Sicherheitsgrenze" gehoert zum Regelwerkswert, nicht zur eigenen Vorgabe',
180 ).toContain(
181 gedruckt(`Die Obergrenze der Umlaufzeit von ${regelwert} ist eine Sicherheitsgrenze`),
182 );
183 expect(text, 'die engere eigene Vorgabe wird als solche benannt').toContain(
184 gedruckt(`Für diesen Plan ist eine engere Obergrenze von ${fmt.seconds(100)} vorgegeben`),
185 );
186 });
187
188 it('laesst den Satz ohne eigene Vorgabe unveraendert', () => {
189 const { text } = ausdruck({});
190 expect(text).toContain(
191 gedruckt(
192 `Die Obergrenze der Umlaufzeit von ${fmt.seconds(ANLAGENART_GRENZEN.knotenpunkt.cycleTime.max)} ` +
193 'ist eine Sicherheitsgrenze',
194 ),
195 );
196 expect(text).not.toContain(gedruckt('ist eine engere Obergrenze von'));
197 });
198 });