lsa-planer

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

/ tests export schwerverkehrZusage.test.ts

7,4 KB Rohdatei
tests/export/schwerverkehrZusage.test.ts — 164 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 { buildSignalPlan } from '@/domain/plan/signalPlan';
6 import { validateProject } from '@/domain/validation';
7 import { createStandardIntersectionProject } from '@/domain/model/factory';
8 import { toWinAnsi } from '@/render/pdfSurface';
9 import type { Project } from '@/domain/model/project';
10
11 /*
12 * Befund 34 zur Fassung 5.9.0, der Rest im Ausdruck.
13 *
14 * Der gedruckte Erlaeuterungsabsatz ist berichtigt (bewertung.ts,
15 * schwerverkehrSatz: die Spalte fuehrt qS0 · n · fA / fSV), und die
16 * gleichlautende Zusage in csv.ts ist nachgezogen. Der Docblock von
17 * `schwerverkehrZelle` in pdf.ts trug sie weiter im alten Wortlaut: "Mit dem
18 * Faktor daneben laesst sich die Spalte 'Saettigungsverkehrsstaerke' aus dem
19 * Regelwert je Fahrstreifen nachrechnen: qS = 3600 / (fSV * tB)".
20 *
21 * Sie ist doppelt widerlegt: Die Spalte fuehrt zusaetzlich die Fahrstreifenzahl
22 * und - bei links und rechts abbiegenden Stroemen - die Abminderung fA, und das
23 * Zeichen qS steht in derselben Zeile einmal fuer den Wert je Fahrstreifen und
24 * einmal fuer den des ganzen Stroms. Eine Zusage im Kommentar ist Teil des
25 * Programms; wer das Verhalten aendert, aendert den Kommentar daneben.
26 *
27 * Der Docblock nennt jetzt dieselbe Kette wie der gedruckte Absatz und sagt,
28 * WO n und die Fahrbeziehung in der Unterlage stehen: in der
29 * Signalgruppentabelle. Eine zweite Fahrstreifenspalte in der Leistungstabelle
30 * traegt nicht - ohne die Fahrbeziehung derselben Zeile waere die Kette dort
31 * ohnehin nicht geschlossen, und die Angabe stuende ein zweites Mal in der
32 * Unterlage.
33 *
34 * NACHGEBESSERT: Diese Begruendung gilt nur, solange die Signalgruppentabelle
35 * gedruckt wird. Sie ist abwaehlbar (exportView.ts, `includeSignalGroups`), und
36 * dann stand die Kette "qS0 · n · fA / fSV" in einer Unterlage, in der weder
37 * "Fahrstr." noch "Fahrbeziehung" vorkam - aus keiner gedruckten Groesse zu
38 * schliessen. Derselbe Fall ist in dieser Datei beim Anfahransatz bereits
39 * geloest (Befund 61 zur Fassung 5.8.0): Der Absatz nennt die Abwahl und traegt
40 * die Werte selbst nach. Der Erlaeuterungsabsatz der Leistungstabelle tut das
41 * jetzt ebenso.
42 *
43 * MUTATIONSNACHWEIS, nachgemessen an zwei zurueckgesetzten Staenden:
44 *
45 * - Docblock im alten Wortlaut: 2 von 5 Faellen fallen - der erste und der
46 * zweite.
47 * - Ohne den Nachsatz des Erlaeuterungsabsatzes
48 * (`fahrstreifenHerkunftSatz`, pdf.ts): 2 von 5 fallen - der dritte und
49 * der vierte.
50 *
51 * Der fuenfte ist in beiden Staenden gruen: Er haelt fest, dass die Angabe bei
52 * gedruckter Signalgruppentabelle NICHT ein zweites Mal in der Unterlage
53 * steht, und bewacht damit die Gegenrichtung.
54 */
55
56 const STICHTAG = new Date('2026-01-01T12:00:00Z');
57 const QUELLE = readFileSync('src/services/export/pdf.ts', 'utf8');
58
59 /** Der Docblock unmittelbar vor einer Funktion. */
60 function docblock(quelle: string, funktion: string): string {
61 const stelle = quelle.indexOf(`function ${funktion}(`);
62 expect(stelle, `${funktion} steht in der Quelle`).toBeGreaterThan(0);
63 const davor = quelle.slice(0, stelle);
64 const anfang = davor.lastIndexOf('/**');
65 const ende = davor.lastIndexOf('*/');
66 expect(anfang, `${funktion} hat einen Docblock`).toBeGreaterThan(0);
67 expect(ende).toBeGreaterThan(anfang);
68 return davor.slice(anfang, ende + 2);
69 }
70
71 /** Sichtbaren Text aus dem PDF lesen; die Inhaltsstroeme sind komprimiert. */
72 function pdfText(bytes: Uint8Array): string {
73 const roh = new TextDecoder('latin1').decode(bytes);
74 const teile: string[] = [];
75 const suche = /stream\r?\n/g;
76 let treffer: RegExpExecArray | null;
77
78 while ((treffer = suche.exec(roh)) !== null) {
79 const start = treffer.index + treffer[0].length;
80 const ende = roh.indexOf('endstream', start);
81 if (ende < 0) continue;
82 let inhalt: string;
83 try {
84 inhalt = inflateSync(Buffer.from(roh.slice(start, ende), 'latin1')).toString('latin1');
85 } catch {
86 continue;
87 }
88 for (const t of inhalt.matchAll(/\(((?:[^()\\]|\\.)*)\)\s*Tj/g)) {
89 teile.push(t[1]!.replace(/\\([()\\])/g, '$1'));
90 }
91 }
92 return teile.join(' ').replace(/\s+/g, ' ');
93 }
94
95 function ausdruck(project: Project, optionen = DEFAULT_PDF_OPTIONS): string {
96 const plan = buildSignalPlan(project);
97 return pdfText(
98 buildProjectPdf(project, plan, validateProject(project, plan, STICHTAG), optionen, STICHTAG),
99 );
100 }
101
102 describe('Befund 34 - die Zusage am Ausdruck nennt die vollständige Kette', () => {
103 it('trägt die widerlegte Rechnung nicht mehr', () => {
104 const block = docblock(QUELLE, 'schwerverkehrZelle');
105
106 // Der alte Wortlaut, Zeichen fuer Zeichen: "Mit dem Faktor daneben laesst
107 // sich die Spalte ... nachrechnen: qS = 3600 / (fSV * tB)". Was der
108 // Docblock heute davon nennt, nennt er als das, was hier STAND.
109 expect(block).not.toContain('qS = 3600 / (fSV * tB)');
110 expect(block).not.toContain('laesst sich die Spalte');
111 });
112
113 it('nennt dieselbe Kette wie der gedruckte Absatz und wo ihre Glieder stehen', () => {
114 const block = docblock(QUELLE, 'schwerverkehrZelle');
115
116 expect(block).toContain('qS0 · n · fA / fSV');
117 // n und die Fahrbeziehung, aus der fA folgt, stehen in der
118 // Signalgruppentabelle - deshalb braucht die Leistungstabelle keine
119 // zweite Fahrstreifenspalte.
120 expect(block).toContain('Signalgruppentabelle');
121 });
122
123 it('druckt die Kette im Erläuterungsabsatz und die Glieder in der Signalgruppentabelle', () => {
124 // Zusammenhangspruefung: Der Kommentar sagt zu, was die Unterlage haelt.
125 const text = ausdruck(createStandardIntersectionProject('Schwerverkehr', STICHTAG));
126
127 expect(text).toContain(toWinAnsi('qS0 · n · fA / fSV'));
128 expect(text).toContain('Fahrstr.');
129 expect(text).toContain('Fahrbeziehung');
130 // Und er sagt auch, WO sie stehen - sonst muesste der Leser die Kette
131 // selbst in der Unterlage suchen.
132 expect(text).toContain(toWinAnsi('Spalten "Fahrstr." und "Fahrbeziehung"'));
133 });
134
135 it('nennt n und die Fahrbeziehung selbst, wenn die Signalgruppentabelle abgewählt ist', () => {
136 /*
137 * Der Haken "Signalgruppen" in der Ausgabeansicht (exportView.ts). Ohne
138 * die Tabelle steht die Kette qS0 · n · fA / fSV in einer Unterlage, in
139 * der n und die Fahrbeziehung nirgends vorkommen. Dasselbe Muster wie beim
140 * Anfahransatz (Befund 61 zur Fassung 5.8.0): Die Abwahl wird genannt,
141 * die Werte werden nachgetragen.
142 */
143 const projekt = createStandardIntersectionProject('Schwerverkehr', STICHTAG);
144 const text = ausdruck(projekt, { ...DEFAULT_PDF_OPTIONS, includeSignalGroups: false });
145
146 expect(text, 'Vorbedingung: die Spalte "Fahrstr." gibt es nicht mehr').not.toContain(
147 'Fahrstr.',
148 );
149 expect(text).toContain(toWinAnsi('ist für diese Ausgabe abgewählt'));
150 for (const gruppe of projekt.signalGroups.filter((g) => g.mode === 'kfz')) {
151 expect(text, gruppe.name).toContain(
152 toWinAnsi(`"${gruppe.name}" ${gruppe.lanes} Fahrstreifen, geradeaus`),
153 );
154 }
155 });
156
157 it('trägt bei gedruckter Signalgruppentabelle keine zweite Fahrstreifenangabe nach', () => {
158 // Die Gegenprobe: Steht die Tabelle in der Unterlage, bleibt die Angabe an
159 // ihrem einen Ort - sonst stuende sie zweimal da.
160 const text = ausdruck(createStandardIntersectionProject('Schwerverkehr', STICHTAG));
161
162 expect(text).not.toContain(toWinAnsi('Fahrstreifen, geradeaus'));
163 });
164 });