lsa-planer

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

/ tests export massgebenderStromUnterlage.test.ts

8,2 KB Rohdatei
tests/export/massgebenderStromUnterlage.test.ts — 193 Zeilen
1 import { readFileSync } from 'node:fs';
2 import { inflateSync } from 'node:zlib';
3 import { describe, expect, it } from 'vitest';
4 import { buildProjectPdf, DEFAULT_PDF_OPTIONS } from '@/services/export/pdf';
5 import {
6 buildSignalPlan,
7 intergreenKey,
8 massgebendeRaeumbeziehung,
9 } from '@/domain/plan/signalPlan';
10 import { validateProject } from '@/domain/validation';
11 import { raeumbeziehungBefunde } from '@/domain/validation/rules';
12 import { createStandardIntersectionProject } from '@/domain/model/factory';
13 import { parseProject } from '@/domain/model/schema';
14 import { RILSA_DEFAULTS } from '@/domain/rilsa/constants';
15 import { toWinAnsi } from '@/render/pdfSurface';
16 import type { Conflict, Project } from '@/domain/model/project';
17 import * as fmt from '@/ui/format';
18
19 /*
20 * Der massgebende Strom in der PLANUNTERLAGE.
21 *
22 * Verlangt war ausdruecklich dieser Fall: EINE Beziehung, an der Plan,
23 * Pruefbericht und die gedruckte Zeile gegeneinander gehalten werden. Bis zu
24 * diesem Befund liefen sie auseinander - der Plan raeumte mit der Fahrbeziehung
25 * des massgebenden STROMS, die Aufstellung "Abweichungen von den Regelwerten"
26 * zaehlte mit der Fahrbeziehung der SIGNALGRUPPE. Gedruckt stand dann: das
27 * Merkmal "enger Innenradius" ist an einer Beziehung gesetzt und wirkt an
28 * null - waehrend derselbe Ausdruck zwei Seiten davor 5,0 m/s in die Spalte
29 * "vr" derselben Zeile schrieb.
30 */
31
32 const STICHTAG = new Date('2026-01-01T12:00:00Z');
33 const BEISPIEL = 'beispiele/vierarmiger-knotenpunkt.lsap';
34
35 /** Sichtbaren Text aus dem PDF lesen; die Inhaltsstroeme sind komprimiert. */
36 function pdfText(bytes: Uint8Array): string {
37 const roh = new TextDecoder('latin1').decode(bytes);
38 const teile: string[] = [];
39 const suche = /stream\r?\n/g;
40 let treffer: RegExpExecArray | null;
41
42 while ((treffer = suche.exec(roh)) !== null) {
43 const start = treffer.index + treffer[0].length;
44 const ende = roh.indexOf('endstream', start);
45 if (ende < 0) continue;
46 let inhalt: string;
47 try {
48 inhalt = inflateSync(Buffer.from(roh.slice(start, ende), 'latin1')).toString('latin1');
49 } catch {
50 continue;
51 }
52 for (const t of inhalt.matchAll(/\(((?:[^()\\]|\\.)*)\)\s*Tj/g)) {
53 teile.push(t[1]!.replace(/\\([()\\])/g, '$1'));
54 }
55 }
56 return teile.join(' ').replace(/\s+/g, ' ');
57 }
58
59 const gedruckt = (text: string): string => toWinAnsi(text).replace(/\s+/g, ' ');
60
61 /**
62 * Standardknotenpunkt, an dem "K1" geradeaus UND rechts fuehrt: Die Gruppe
63 * bleibt bei 'geradeaus', die Beziehung K1 -> K3 traegt die Fahrbeziehung
64 * ihres massgebenden Stroms und das Merkmal "enger Innenradius".
65 */
66 function sammelgruppe(): Project {
67 const basis = createStandardIntersectionProject('Sammelgruppe', STICHTAG);
68 const k1 = basis.signalGroups.find((g) => g.name === 'K1')!;
69 const k3 = basis.signalGroups.find((g) => g.name === 'K3')!;
70 return {
71 ...basis,
72 meta: { ...basis.meta, planner: 'O. Willerding' },
73 conflicts: basis.conflicts.map((c) =>
74 c.fromId === k1.id && c.toId === k3.id
75 ? { ...c, raeumbeziehung: 'rechts' as const, engerRadius: true }
76 : c,
77 ),
78 };
79 }
80
81 function beziehung(project: Project): Conflict {
82 return project.conflicts.find((c) => c.engerRadius === true)!;
83 }
84
85 function ausdruck(project: Project): string {
86 const plan = buildSignalPlan(project);
87 const bericht = validateProject(project, plan, STICHTAG);
88 return pdfText(buildProjectPdf(project, plan, bericht, DEFAULT_PDF_OPTIONS, STICHTAG));
89 }
90
91 describe('N1/3: Plan, Pruefbericht und gedruckte Zeile legen dieselbe Fahrbeziehung zugrunde', () => {
92 it('haelt die drei an derselben Beziehung gegeneinander', () => {
93 const project = sammelgruppe();
94 const konflikt = beziehung(project);
95 const from = project.signalGroups.find((g) => g.id === konflikt.fromId)!;
96 const plan = buildSignalPlan(project);
97 const bericht = validateProject(project, plan, STICHTAG);
98 const text = pdfText(buildProjectPdf(project, plan, bericht, DEFAULT_PDF_OPTIONS, STICHTAG));
99
100 // Die eine Entscheidung, an die sich alle drei zu halten haben.
101 expect(from.movement).toBe('geradeaus');
102 expect(massgebendeRaeumbeziehung(from, konflikt)).toBe('rechts');
103
104 // PLAN: raeumt als Rechtsabbieger mit engem Innenradius.
105 const gerechnet = plan.intergreens.get(intergreenKey(konflikt.fromId, konflikt.toId));
106 expect(gerechnet?.calculation?.clearingSpeed).toBe(
107 RILSA_DEFAULTS.clearingSpeed.kfzTurningEngerRadius,
108 );
109
110 // PRUEFBERICHT: meldet das Merkmal als wirksam, nicht als wirkungslos.
111 const zurBeziehung = bericht.findings.filter(
112 (f) => f.target?.kind === 'konflikt' && f.target.id === konflikt.id,
113 );
114 expect(zurBeziehung.map((f) => f.rule)).toContain('zwischenzeiten.enger-radius');
115 expect(zurBeziehung.map((f) => f.rule)).not.toContain(
116 'zwischenzeiten.enger-radius-ohne-wirkung',
117 );
118
119 // GEDRUCKTE ZEILE: dieselbe Raeumgeschwindigkeit in der Spalte "vr" ...
120 expect(text).toContain(
121 gedruckt(fmt.metersPerSecond(RILSA_DEFAULTS.clearingSpeed.kfzTurningEngerRadius, 1)),
122 );
123 // ... und die Abweichungsaufstellung zaehlt die Beziehung als wirksam.
124 // 16 Beziehungen seit 5.43.0: Die Vorlage erfasst jede Furt auch gegen den
125 // Strom, der sie beim Ausfahren quert (K2/F2, K4/F1; Gruppe Furt). Bis
126 // dahin waren es 12. Gezaehlt wird weiter die eine wirksame Beziehung.
127 expect(text).toContain(gedruckt('an 1 von 16 Konfliktbeziehungen'));
128 expect(text).toContain(gedruckt('an den gekennzeichneten Beziehungen.'));
129 expect(text).not.toContain(gedruckt('an den 0 Beziehungen, an denen es wirkt'));
130 });
131
132 it('zaehlt weiter nur die Beziehungen, an denen das Merkmal wirkt', () => {
133 // Gegenprobe: dieselbe Gruppe ohne die Fahrbeziehung des massgebenden
134 // Stroms. Dann raeumt der Plan geradeaus, das Merkmal wirkt nicht - und
135 // die Aufstellung muss das sagen, sonst prueft der Fall oben nichts.
136 const basis = sammelgruppe();
137 const project: Project = {
138 ...basis,
139 conflicts: basis.conflicts.map((c) => {
140 const { raeumbeziehung: _weg, ...ohne } = c;
141 return ohne;
142 }),
143 };
144 const text = ausdruck(project);
145
146 expect(text).toContain(gedruckt('an den 0 Beziehungen, an denen es wirkt'));
147 expect(text).not.toContain(gedruckt('an den gekennzeichneten Beziehungen.'));
148 });
149
150 it('sagt im Erlaeuterungsabsatz, welche Fahrbeziehung ueber das Merkmal entscheidet', () => {
151 /*
152 * Ohne diesen Satz steht in der Unterlage eine Zeile mit 5,0 m/s neben
153 * einer Signalgruppentabelle, die "geradeaus" fuehrt, und nichts erklaert
154 * den Unterschied. Der Absatz nennt deshalb den Strom, dem der Raeumweg
155 * dieser Zeile gehoert - nicht die Signalgruppe.
156 */
157 const text = ausdruck(sammelgruppe());
158
159 expect(text).toContain(
160 gedruckt(
161 'Ob ein Strom abbiegt, entscheidet dabei die Fahrbeziehung des Stroms, dem der Räumweg ' +
162 'dieser Zeile gehört, und nicht die der räumenden Signalgruppe',
163 ),
164 );
165 });
166
167 it('nennt im selben Absatz den Rueckfall auf die Fahrbeziehung der Signalgruppe', () => {
168 /*
169 * NACHTRAG (Fassung 5.11.0): Festgehalten wird der massgebende
170 * Strom allein bei der Uebernahme aus dem Lageplan. In der mitgelieferten
171 * Beispieldatei traegt keine der 72 Beziehungen das Feld; gedruckt stand
172 * trotzdem unbedingt, ueber das Abbiegen entscheide der Strom und nicht
173 * die Signalgruppe. Der Pruefer haelt die Zeile damit fuer nachgewiesen -
174 * die gefaehrliche Richtung des Irrtums.
175 */
176 expect(ausdruck(sammelgruppe())).toContain(
177 gedruckt(
178 'Ist nicht festgehalten, welcher Strom den Räumweg gefahren ist, wird mit der ' +
179 'Fahrbeziehung der Signalgruppe gerechnet, und der Prüfbericht meldet das.',
180 ),
181 );
182 });
183
184 it('sagt den Rueckfall mit dem Wortlaut des Pruefberichts', () => {
185 // Derselbe Sachverhalt, dieselben Worte: Der Bericht meldet ihn an der
186 // Beispieldatei, die Unterlage erklaert ihn im Absatz zur Spalte.
187 const gemeinsam = 'mit der Fahrbeziehung der Signalgruppe';
188 const beispiel = parseProject(JSON.parse(readFileSync(BEISPIEL, 'utf8')), STICHTAG).project;
189
190 expect(raeumbeziehungBefunde(beispiel)[0]?.message).toContain(gemeinsam);
191 expect(ausdruck(sammelgruppe())).toContain(gedruckt(gemeinsam));
192 });
193 });