import { inflateSync } from 'node:zlib'; import { describe, expect, it } from 'vitest'; import { buildProjectPdf, DEFAULT_PDF_OPTIONS, type PdfOptions } 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 type { Project } from '@/domain/model/project'; /** * Die Begruendung der Umlaufzeit in der Planunterlage. * * WOZU DAS IN EINER ANORDNUNGSUNTERLAGE STEHT: Eine Anordnung nach Paragraf 45 * StVO ist eine Ermessensentscheidung und muss begruendet werden. Die * Umlaufzeit ist die Zahl, an der Wartezeit und Rueckstau haengen; bis hierher * stand in der Unterlage genau eine Zahl und ein Verfahrensname, und warum * dieses Verfahren, stand nirgends. * * DIE FAELLE BEWACHEN DREI ZUSAGEN: Der Abschnitt ist enthalten, er laesst sich * abwaehlen und sagt das dann in der Bestandteileuebersicht, und er spricht * keine Empfehlung aus. */ 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 mitVerkehr(method: Project['program']['method']): Project { const roh = createStandardIntersectionProject('Hauptstraße / Bahnhofstraße', STICHTAG); return { ...roh, program: { ...roh.program, method, manualCycleTime: method === 'manuell' ? 80 : null }, demands: roh.signalGroups .filter((g) => g.mode === 'kfz') .map((g) => ({ signalGroupId: g.id, volume: 600, heavyVehicleShare: 0.05 })), }; } function ausdruck(project: Project, options: PdfOptions = DEFAULT_PDF_OPTIONS): string { const plan = buildSignalPlan(project); const bericht = validateProject(project, plan, STICHTAG); return pdfText(buildProjectPdf(project, plan, bericht, options, STICHTAG)); } describe('Planunterlage - Begruendung der Umlaufzeit', () => { it('druckt den Abschnitt von Haus aus', () => { // Vorbelegt mit true, auf ausdrueckliche Entscheidung des Anwenders: Die // Wahl der Umlaufzeit war der eine Punkt, zu dem die Unterlage nichts // sagte. expect(DEFAULT_PDF_OPTIONS.includeCycleComparison).toBe(true); const text = ausdruck(mitVerkehr('webster')); expect(text).toContain(toWinAnsi('Umlaufzeit – Vergleich der Rechenansätze')); }); it('führt alle vier Ansätze auf und kennzeichnet den gewählten', () => { const text = ausdruck(mitVerkehr('hbs')); for (const name of ['Webster', toWinAnsi('Akçelik'), 'HBS', 'HCM']) { expect(text, name).toContain(name); } expect(text, 'der gewählte Ansatz ist gekennzeichnet').toContain(toWinAnsi('HBS (gewählt)')); }); it('spricht keine Empfehlung aus und weist die Regel als eigenen Ansatz aus', () => { const text = ausdruck(mitVerkehr('webster')); expect(text).toContain(toWinAnsi('keine Empfehlung')); expect(text).toContain(toWinAnsi('die Wahl trifft der Planer')); expect(text).toContain(toWinAnsi('ein Ansatz dieses Programms')); }); it('sagt bei fester Vorgabe, dass keines der vier Verfahren eingeht', () => { const text = ausdruck(mitVerkehr('manuell')); expect(text).toContain(toWinAnsi('von Hand vorgegeben')); expect(text).toContain(toWinAnsi('Vergleichsmaßstab')); }); it('laesst sich abwaehlen, und die Bestandteileuebersicht sagt es dann', () => { /* * Die Uebersicht ist der Ort, an dem eine Behoerde nachsieht, was die * Unterlage enthaelt. Ein abgewaehlter Abschnitt, der dort nicht als * abgewaehlt steht, ist eine Luecke, die niemand bemerkt. */ const ohne = ausdruck(mitVerkehr('webster'), { ...DEFAULT_PDF_OPTIONS, includeCycleComparison: false, }); expect(ohne, 'der Abschnitt fehlt').not.toContain( toWinAnsi('Umlaufzeit – Vergleich der Rechenansätze:'), ); expect(ohne, 'die Zeile der Übersicht steht da').toContain( toWinAnsi('Begründung der Umlaufzeit (Vergleich der vier Rechenansätze)'), ); expect(ohne, 'und sie sagt, dass er fehlt').toContain(toWinAnsi('nein')); }); it('nennt die Regel im Fundstellenverzeichnis', () => { // Sonst stuende in der Unterlage ein Wert, dessen Herkunft sie nicht // ausweist - genau der Fehlertyp, gegen den das Verzeichnis steht. const text = ausdruck(mitVerkehr('webster')); expect(text).toContain(toWinAnsi('Umlaufzeit: Vergleich der vier Rechenansätze')); }); });