lsa-planer

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

/ tests export anfahransatzVerweis.test.ts

5,7 KB Rohdatei
tests/export/anfahransatzVerweis.test.ts — 164 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 61: Der Absatz zum OePNV-Anfahransatz (RiLSA Fall 4) nannte als
12 * Fundort der massgebenden Vmax die Spalte "V zul" der Signalgruppentabelle.
13 * Diese Tabelle entsteht nur bei options.includeSignalGroups, der Rechenweg
14 * der Zwischenzeiten haengt an anderen Schaltern - drawIntergreenDetails
15 * bekam den Schalter gar nicht uebergeben.
16 *
17 * Was der Anwender sonst erlebt: Er hat eine OePNV-Signalgruppe mit "Halt vor
18 * dem Knotenpunkt", waehlt in der Ansicht "Ausgabe" die Signalgruppentabelle
19 * ab und laesst Zwischenzeitenmatrix und Rechenweg an. Die Unterlage verweist
20 * dann auf eine Spalte einer Tabelle, die sie nicht enthaelt, und die
21 * gedruckte Raeumzeit laesst sich aus keiner gedruckten Groesse nachrechnen,
22 * obwohl derselbe Absatz sich gerade auf diese Nachrechenbarkeit beruft.
23 */
24
25 const STICHTAG = new Date('2026-01-01T12:00:00Z');
26
27 function gedruckt(text: string): string {
28 return toWinAnsi(text);
29 }
30
31 function entpacke(roh: string, von: number): string | null {
32 const anfang = roh.indexOf('stream', von);
33 if (anfang < 0) return null;
34 const start = anfang + (roh.startsWith('stream\r\n', anfang) ? 8 : 7);
35 const ende = roh.indexOf('endstream', start);
36 if (ende < 0) return null;
37 try {
38 return inflateSync(Buffer.from(roh.slice(start, ende), 'latin1')).toString('latin1');
39 } catch {
40 return null;
41 }
42 }
43
44 function volltext(bytes: Uint8Array): string {
45 const roh = new TextDecoder('latin1').decode(bytes);
46 const stuecke: string[] = [];
47 for (const treffer of roh.matchAll(/\/Type \/Page\b[^>]*?\/Contents (\d+) 0 R/g)) {
48 const objekt = roh.indexOf(`\n${treffer[1]!} 0 obj`);
49 const inhalt = objekt < 0 ? null : entpacke(roh, objekt);
50 if (inhalt === null) continue;
51 for (const t of inhalt.matchAll(/\(((?:[^()\\]|\\.)*)\)\s*Tj/g)) {
52 stuecke.push(t[1]!.replace(/\\([()\\])/g, '$1'));
53 }
54 }
55 return stuecke.join(' ').replace(/\s+/g, ' ');
56 }
57
58 /** Knotenpunkt mit OePNV-Signalgruppen, die vor dem Knotenpunkt halten. */
59 function mitHaltefall(zweiteGruppe: boolean): Project {
60 const basis = createStandardIntersectionProject('Musterkreuzung', STICHTAG);
61 const bahn = {
62 ...basis.signalGroups[0]!,
63 id: 'sg-bahn',
64 name: 'Strassenbahn Nordzufahrt',
65 mode: 'oepnv' as const,
66 vehicleClass: 'strassenbahn' as const,
67 vZul: 50,
68 };
69 const bus = {
70 ...bahn,
71 id: 'sg-bus',
72 name: 'Buslinie Suedzufahrt',
73 vehicleClass: 'bus' as const,
74 vZul: 30,
75 };
76 const halt = {
77 ...basis.conflicts[0]!,
78 id: 'cf-bahn',
79 fromId: 'sg-bahn',
80 toId: basis.signalGroups[1]!.id,
81 clearingDistance: 25,
82 enteringDistance: 8,
83 haltVorKnoten: true,
84 };
85 return {
86 ...basis,
87 signalGroups: [...basis.signalGroups, bahn, ...(zweiteGruppe ? [bus] : [])],
88 conflicts: [
89 ...basis.conflicts,
90 halt,
91 ...(zweiteGruppe ? [{ ...halt, id: 'cf-bus', fromId: 'sg-bus' }] : []),
92 ],
93 };
94 }
95
96 function ausdruck(mitSignalgruppen: boolean, zweiteGruppe = false): string {
97 const projekt = mitHaltefall(zweiteGruppe);
98 const plan = buildSignalPlan(projekt);
99 return volltext(
100 buildProjectPdf(
101 projekt,
102 plan,
103 validateProject(projekt, plan, STICHTAG),
104 {
105 ...DEFAULT_PDF_OPTIONS,
106 includeSignalGroups: mitSignalgruppen,
107 includeIntergreenMatrix: true,
108 includeCalculationDetails: true,
109 },
110 STICHTAG,
111 ),
112 );
113 }
114
115 describe('Anfahransatz ohne Signalgruppentabelle', () => {
116 it('verweist nicht auf die Spalte einer Tabelle, die diese Ausgabe nicht enthaelt', () => {
117 const text = ausdruck(false);
118
119 // Vorbedingung: Der Absatz zum Anfahransatz steht im Dokument.
120 expect(text).toContain(gedruckt('Anfahransatz'));
121
122 expect(text).not.toContain(gedruckt('Spalte "V zul" der Signalgruppentabelle'));
123 });
124
125 /*
126 * Nachtrag zu Befund 61: Der Einschub lautete "Vmax betraegt
127 * Strassenbahn Nordzufahrt 50 km/h" - Name und Zahl standen unverbunden
128 * hintereinander, und bei zwei raeumenden Gruppen erfuhr der Leser nicht,
129 * welcher Wert zu welcher Zeile der Rechenwegtabelle gehoert.
130 */
131 it('nennt die massgebende Vmax in einem lesbaren Satz, damit tr nachrechenbar bleibt', () => {
132 const text = ausdruck(false);
133
134 expect(text).toContain(
135 gedruckt(
136 'Die räumenden Signalgruppen dieses Ansatzes und ihre Vmax: ' +
137 '„Strassenbahn Nordzufahrt" 50 km/h.',
138 ),
139 );
140 expect(text).toContain(
141 gedruckt('Maßgebend ist je Zeile die in der Spalte „Beziehung" zuerst genannte'),
142 );
143 });
144
145 it('zaehlt bei mehreren raeumenden Signalgruppen jede mit ihrer eigenen Vmax auf', () => {
146 const text = ausdruck(false, true);
147
148 expect(text).toContain(
149 gedruckt(
150 'Die räumenden Signalgruppen dieses Ansatzes und ihre Vmax: ' +
151 '„Strassenbahn Nordzufahrt" 50 km/h, „Buslinie Suedzufahrt" 30 km/h.',
152 ),
153 );
154 });
155
156 it('behaelt den Verweis, solange die Signalgruppentabelle im Dokument steht', () => {
157 const text = ausdruck(true);
158
159 expect(text).toContain(gedruckt('Spalte "V zul" der Signalgruppentabelle'));
160 expect(text).toContain(gedruckt('tr = Vmax/(3,6 · a)'));
161 // Doppelt genannt braucht die Vmax nicht zu werden: Die Tabelle fuehrt sie.
162 expect(text).not.toContain(gedruckt('Die räumenden Signalgruppen dieses Ansatzes'));
163 });
164 });