import { inflateSync } from 'node:zlib'; import { describe, expect, it } from 'vitest'; import { buildProjectPdf, DEFAULT_PDF_OPTIONS } from '@/services/export/pdf'; import { buildSignalPlan } from '@/domain/plan/signalPlan'; import { validateProject } from '@/domain/validation'; import { createStandardIntersectionProject } from '@/domain/model/factory'; import { toWinAnsi } from '@/render/pdfSurface'; import * as fmt from '@/ui/format'; import type { Project, SignalGroup } from '@/domain/model/project'; /** * Die Geometrie in der Planunterlage. * * WAS DIESE FAELLE BEWACHEN: Die Saettigungsverkehrsstaerke ist die Zahl, aus * der Kapazitaet, Auslastungsgrad und Qualitaetsstufe folgen. Wer die Unterlage * prueft, muss sie aus den gedruckten Groessen herleiten koennen - und dazu * gehoert seit dieser Fassung die Geometrie. Vor allem muss die Unterlage * sagen, wenn KEINE vorliegt: Der Faktor 1 steht dann fuer eine angenommene * Standardbedingung und nicht fuer eine gemessene. */ const STICHTAG = new Date('2026-01-01T12:00:00Z'); /** Der sichtbare Text der Datei, in einer Zeile. */ function pdfText(bytes: Uint8Array): string { const roh = new TextDecoder('latin1').decode(bytes); const teile: string[] = []; const suche = /stream\r?\n/g; let treffer: RegExpExecArray | null; while ((treffer = suche.exec(roh)) !== null) { const start = treffer.index + treffer[0].length; const ende = roh.indexOf('endstream', start); if (ende < 0) continue; let inhalt: string; try { inhalt = inflateSync(Buffer.from(roh.slice(start, ende), 'latin1')).toString('latin1'); } catch { continue; } for (const t of inhalt.matchAll(/\(((?:[^()\\]|\\.)*)\)\s*Tj/g)) { teile.push(t[1]!.replace(/\\([()\\])/g, '$1')); } } return teile.join(' ').replace(/\s+/g, ' '); } function mitGeometrie( geometrie: Partial>, movement?: SignalGroup['movement'], ): Project { const roh = createStandardIntersectionProject('Hauptstraße / Bahnhofstraße', STICHTAG); let gesetzt = false; return { ...roh, signalGroups: roh.signalGroups.map((gruppe) => { if (gesetzt || gruppe.mode !== 'kfz') return gruppe; gesetzt = true; return { ...gruppe, ...(movement === undefined ? {} : { movement }), ...geometrie }; }), demands: roh.signalGroups .filter((g) => g.mode === 'kfz') .map((g) => ({ signalGroupId: g.id, volume: 500, heavyVehicleShare: 0.05 })), }; } function ausdruck(project: Project): string { const plan = buildSignalPlan(project); const bericht = validateProject(project, plan, STICHTAG); return pdfText(buildProjectPdf(project, plan, bericht, DEFAULT_PDF_OPTIONS, STICHTAG)); } describe('Planunterlage - Geometrie der Zufahrt', () => { it('führt eine eigene Spalte in der Leistungstabelle', () => { const text = ausdruck(mitGeometrie({ fahrstreifenbreite: 2.5 })); expect(text).toContain(toWinAnsi('Geometrie (b, R, s; Faktor)')); expect(text).toContain(toWinAnsi('b = 2,50 m')); // fb(2,50) = 1,1875, gerundet 1,188. expect(text).toContain(toWinAnsi('Faktor 1,188')); }); it('sagt bei fehlender Vermessung, dass die Standardbedingung angenommen ist', () => { /* * DER WICHTIGSTE FALL. Ein leeres Feld heisst nicht "3,25 m gemessen", * sondern "niemand hat gemessen". Stuende dort nur "Standard", laese ein * Pruefer eine Feststellung, wo eine Annahme steht - derselbe Unterschied * wie bei der Wegherkunft 'unbestimmt'. */ const text = ausdruck(mitGeometrie({})); expect(text).toContain(toWinAnsi('Standard (angenommen)')); }); it('vermerkt den greifenden Deckel an der Zeile', () => { const text = ausdruck(mitGeometrie({ laengsneigung: -4 })); // Dieselbe Schreibweise wie die Zelle sie bildet - eine von Hand // gesetzte Zeichenkette liefe beim naechsten Formatwechsel auseinander. expect(text).toContain(toWinAnsi(`s = ${fmt.numShort(-4, 1)} %`)); expect(text).toContain(toWinAnsi('Gefälle hebt nicht an (gedeckelt)')); }); it('vermerkt den wirkungslosen Radius am Abbieger', () => { const text = ausdruck(mitGeometrie({ kurvenradius: 10 }, 'links')); expect(text).toContain(toWinAnsi('R ohne Wirkung (Abbiegerfaktor)')); }); it('nennt die Formeln und die Standardbedingungen im Fundstellenverzeichnis', () => { // Ohne sie liesse sich der gedruckte Faktor nicht nachrechnen. const text = ausdruck(mitGeometrie({ fahrstreifenbreite: 2.5 })); expect(text).toContain(toWinAnsi('geometrische Anpassungsfaktoren des Zeitbedarfswerts')); expect(text).toContain(toWinAnsi('fb = -0,375 · b + 2,125')); expect(text).toContain(toWinAnsi('max(1; max(fb, fR, fs) · min(1; fs))')); }); it('nennt beide Abweichungen von der geschlossenen Formel im Nachweis', () => { // Sie sind Entscheidungen dieses Programms. Eine Unterlage, die die // Formel nennt und die Abweichung verschweigt, ist irrefuehrender als // eine, die beides weglaesst. const text = ausdruck(mitGeometrie({ fahrstreifenbreite: 2.5 })); expect(text).toContain(toWinAnsi('nicht über den Grundwert')); expect(text).toContain(toWinAnsi('doppelt abmindern')); }); });