lsa-planer

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

/ tests export pdfGeometriefaktoren.test.ts

5,2 KB Rohdatei
tests/export/pdfGeometriefaktoren.test.ts — 121 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 * as fmt from '@/ui/format';
9 import type { Project, SignalGroup } from '@/domain/model/project';
10
11 /**
12 * Die Geometrie in der Planunterlage.
13 *
14 * WAS DIESE FAELLE BEWACHEN: Die Saettigungsverkehrsstaerke ist die Zahl, aus
15 * der Kapazitaet, Auslastungsgrad und Qualitaetsstufe folgen. Wer die Unterlage
16 * prueft, muss sie aus den gedruckten Groessen herleiten koennen - und dazu
17 * gehoert seit dieser Fassung die Geometrie. Vor allem muss die Unterlage
18 * sagen, wenn KEINE vorliegt: Der Faktor 1 steht dann fuer eine angenommene
19 * Standardbedingung und nicht fuer eine gemessene.
20 */
21
22 const STICHTAG = new Date('2026-01-01T12:00:00Z');
23
24 /** Der sichtbare Text der Datei, in einer Zeile. */
25 function pdfText(bytes: Uint8Array): string {
26 const roh = new TextDecoder('latin1').decode(bytes);
27 const teile: string[] = [];
28 const suche = /stream\r?\n/g;
29 let treffer: RegExpExecArray | null;
30 while ((treffer = suche.exec(roh)) !== null) {
31 const start = treffer.index + treffer[0].length;
32 const ende = roh.indexOf('endstream', start);
33 if (ende < 0) continue;
34 let inhalt: string;
35 try {
36 inhalt = inflateSync(Buffer.from(roh.slice(start, ende), 'latin1')).toString('latin1');
37 } catch {
38 continue;
39 }
40 for (const t of inhalt.matchAll(/\(((?:[^()\\]|\\.)*)\)\s*Tj/g)) {
41 teile.push(t[1]!.replace(/\\([()\\])/g, '$1'));
42 }
43 }
44 return teile.join(' ').replace(/\s+/g, ' ');
45 }
46
47 function mitGeometrie(
48 geometrie: Partial<Pick<SignalGroup, 'fahrstreifenbreite' | 'kurvenradius' | 'laengsneigung'>>,
49 movement?: SignalGroup['movement'],
50 ): Project {
51 const roh = createStandardIntersectionProject('Hauptstraße / Bahnhofstraße', STICHTAG);
52 let gesetzt = false;
53 return {
54 ...roh,
55 signalGroups: roh.signalGroups.map((gruppe) => {
56 if (gesetzt || gruppe.mode !== 'kfz') return gruppe;
57 gesetzt = true;
58 return { ...gruppe, ...(movement === undefined ? {} : { movement }), ...geometrie };
59 }),
60 demands: roh.signalGroups
61 .filter((g) => g.mode === 'kfz')
62 .map((g) => ({ signalGroupId: g.id, volume: 500, heavyVehicleShare: 0.05 })),
63 };
64 }
65
66 function ausdruck(project: Project): string {
67 const plan = buildSignalPlan(project);
68 const bericht = validateProject(project, plan, STICHTAG);
69 return pdfText(buildProjectPdf(project, plan, bericht, DEFAULT_PDF_OPTIONS, STICHTAG));
70 }
71
72 describe('Planunterlage - Geometrie der Zufahrt', () => {
73 it('führt eine eigene Spalte in der Leistungstabelle', () => {
74 const text = ausdruck(mitGeometrie({ fahrstreifenbreite: 2.5 }));
75 expect(text).toContain(toWinAnsi('Geometrie (b, R, s; Faktor)'));
76 expect(text).toContain(toWinAnsi('b = 2,50 m'));
77 // fb(2,50) = 1,1875, gerundet 1,188.
78 expect(text).toContain(toWinAnsi('Faktor 1,188'));
79 });
80
81 it('sagt bei fehlender Vermessung, dass die Standardbedingung angenommen ist', () => {
82 /*
83 * DER WICHTIGSTE FALL. Ein leeres Feld heisst nicht "3,25 m gemessen",
84 * sondern "niemand hat gemessen". Stuende dort nur "Standard", laese ein
85 * Pruefer eine Feststellung, wo eine Annahme steht - derselbe Unterschied
86 * wie bei der Wegherkunft 'unbestimmt'.
87 */
88 const text = ausdruck(mitGeometrie({}));
89 expect(text).toContain(toWinAnsi('Standard (angenommen)'));
90 });
91
92 it('vermerkt den greifenden Deckel an der Zeile', () => {
93 const text = ausdruck(mitGeometrie({ laengsneigung: -4 }));
94 // Dieselbe Schreibweise wie die Zelle sie bildet - eine von Hand
95 // gesetzte Zeichenkette liefe beim naechsten Formatwechsel auseinander.
96 expect(text).toContain(toWinAnsi(`s = ${fmt.numShort(-4, 1)} %`));
97 expect(text).toContain(toWinAnsi('Gefälle hebt nicht an (gedeckelt)'));
98 });
99
100 it('vermerkt den wirkungslosen Radius am Abbieger', () => {
101 const text = ausdruck(mitGeometrie({ kurvenradius: 10 }, 'links'));
102 expect(text).toContain(toWinAnsi('R ohne Wirkung (Abbiegerfaktor)'));
103 });
104
105 it('nennt die Formeln und die Standardbedingungen im Fundstellenverzeichnis', () => {
106 // Ohne sie liesse sich der gedruckte Faktor nicht nachrechnen.
107 const text = ausdruck(mitGeometrie({ fahrstreifenbreite: 2.5 }));
108 expect(text).toContain(toWinAnsi('geometrische Anpassungsfaktoren des Zeitbedarfswerts'));
109 expect(text).toContain(toWinAnsi('fb = -0,375 · b + 2,125'));
110 expect(text).toContain(toWinAnsi('max(1; max(fb, fR, fs) · min(1; fs))'));
111 });
112
113 it('nennt beide Abweichungen von der geschlossenen Formel im Nachweis', () => {
114 // Sie sind Entscheidungen dieses Programms. Eine Unterlage, die die
115 // Formel nennt und die Abweichung verschweigt, ist irrefuehrender als
116 // eine, die beides weglaesst.
117 const text = ausdruck(mitGeometrie({ fahrstreifenbreite: 2.5 }));
118 expect(text).toContain(toWinAnsi('nicht über den Grundwert'));
119 expect(text).toContain(toWinAnsi('doppelt abmindern'));
120 });
121 });