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 { STAURAUMBEDARF_JE_FAHRZEUG } from '@/domain/rilsa/constants'; import { toWinAnsi } from '@/render/pdfSurface'; import * as fmt from '@/ui/format'; import type { Project } from '@/domain/model/project'; /** * Der Rueckstau in der Planunterlage. * * Die Unterlage druckte die Fahrzeugzahl NGE seit jeher. Was fehlte, war die * Laenge - und damit die Antwort auf die Frage, an der eine Abwaegung nach * Paragraf 45 StVO haengt: Laeuft der Aufstellstreifen ueber? * * DIE FAELLE BEWACHEN VOR ALLEM DIE EHRLICHKEIT: dass der Ausdruck sagt, * welche Groesse verglichen wurde, und dass die Laenge je Fahrzeug als * Praxiswert ausgewiesen ist. */ 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 mitStauraum(stauraum: number | null): Project { const roh = createStandardIntersectionProject('Hauptstraße / Bahnhofstraße', STICHTAG); return { ...roh, signalGroups: roh.signalGroups.map((gruppe) => gruppe.mode === 'kfz' && stauraum !== null ? { ...gruppe, stauraumlaenge: stauraum } : gruppe, ), demands: roh.signalGroups .filter((g) => g.mode === 'kfz') .map((g) => ({ signalGroupId: g.id, volume: 700, 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 - Rückstau und Stauraum', () => { it('druckt einen eigenen Abschnitt mit der Länge', () => { const text = ausdruck(mitStauraum(45)); expect(text).toContain(toWinAnsi('Rückstau und Stauraum')); expect(text).toContain(toWinAnsi('Rückstau je Fahrstreifen')); expect(text).toContain(toWinAnsi(fmt.meters(45, 1))); }); it('sagt, dass ein Mittelwert verglichen wurde', () => { /* * DER WICHTIGSTE FALL DIESER DATEI. Ohne den Satz liest eine Behörde * "Auslastung 80 %" und schließt, der Stauraum reiche – während er in * jedem fünften Umlauf überläuft. */ const text = ausdruck(mitStauraum(45)); expect(text).toContain(toWinAnsi('MITTLERE RÜCKSTAU')); expect(text).toContain(toWinAnsi('Hälfte der Umläufe steht mehr')); expect(text).toContain(toWinAnsi('95-Prozent-Wert')); }); it('weist die Länge je Fahrzeug als Praxiswert aus', () => { const text = ausdruck(mitStauraum(45)); expect(text).toContain(toWinAnsi(`NGE · ${fmt.meters(STAURAUMBEDARF_JE_FAHRZEUG, 0)}`)); expect(text).toContain(toWinAnsi('weder die RiLSA 2015 noch das HBS 2015 nennen einen')); // Und im Fundstellenverzeichnis, mit Prüfstand. expect(text).toContain(toWinAnsi('Rückstaulänge aus dem mittleren Rückstau')); }); it('druckt den Abschnitt auch ohne erfassten Stauraum', () => { // Die Länge hilft auch ohne Vergleich weiter; nur die Spalte bleibt leer. const text = ausdruck(mitStauraum(null)); expect(text).toContain(toWinAnsi('Rückstau und Stauraum')); expect(text).toContain(toWinAnsi('nicht erfasst')); }); it('sagt, dass der Stauraum in keine Zahl der Bemessung eingeht', () => { const text = ausdruck(mitStauraum(45)); expect(text).toContain(toWinAnsi('geht in keine Zahl der Bemessung ein')); }); });