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 { ANLAGENART_LABELS } from '@/domain/rilsa/constants'; import type { Anlagenart, Project } from '@/domain/model/project'; /** * Die Anlagenart in den Planunterlagen. * * Zur verkehrsbehoerdlichen Anordnung nach Paragraf 45 StVO gehoert, welche Art * Anlage angeordnet wird. Ein Signalzeitenplan ohne diese Angabe laesst offen, * wonach er zu beurteilen ist: Dieselbe Umlaufzeit von 240 s ist an einem * Knotenpunkt eine grobe Ueberschreitung der Sicherheitsgrenze und an einer * Arbeitsstelle die zwangslaeufige Folge einer langen Engstelle. * * Der zweite Punkt ist die Herkunft der Schranken: Bei einer einstreifigen * Verkehrsfuehrung stammen die Umlaufzeitgrenzen NICHT aus der RiLSA. Ein * Ausdruck, der unter "Grundlagen der Kennwerte" pauschal die RiLSA nennt, * behauptet eine Deckung durch das Regelwerk, die es fuer diesen Wert nicht * gibt. */ const FIXED_DATE = new Date('2026-01-01T12:00:00Z'); /** Sichtbaren Text aus dem PDF lesen; die Inhaltsstroeme sind komprimiert. */ 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 stueck of inhalt.matchAll(/\((?:\\.|[^\\()])*\)\s*Tj/g)) { teile.push(stueck[0].slice(1, stueck[0].lastIndexOf(')'))); } } // Lange Absaetze sind auf Zeilen umbrochen; fuer die Suche wieder // zusammenziehen, sonst findet contains() den halben Satz nicht. return teile.join(' ').replace(/\s+/g, ' '); } function projekt(anlagenart: Anlagenart): Project { const basis = createStandardIntersectionProject('Hauptstrasse / Bahnhofstrasse', FIXED_DATE); return { ...basis, anlagenart }; } function ausdruck(project: Project, optionen = DEFAULT_PDF_OPTIONS): string { const plan = buildSignalPlan(project); const report = validateProject(project, plan, FIXED_DATE); return pdfText(buildProjectPdf(project, plan, report, optionen, FIXED_DATE)); } describe('Anlagenart im Ausdruck', () => { it('nennt die Anlagenart auf dem Deckblatt', () => { for (const art of ['knotenpunkt', 'fussgaengerschutzanlage', 'einstreifig'] as const) { const text = ausdruck(projekt(art)); expect(text).toContain('Anlagenart'); // Ohne die Klammer, weil runde Klammern im PDF maskiert werden. const bezeichnung = ANLAGENART_LABELS[art].split(' (')[0]; expect(text, art).toContain(bezeichnung); } }); it('nennt die Anlagenart auch ohne den Nachweisteil', () => { // Der Nachweis der Kennwerte laesst sich abwaehlen, die Anlagenart nicht: // Sie sagt, wonach der Plan zu beurteilen ist, und gehoert deshalb auf das // Deckblatt und nicht in einen wahlweisen Anhang. const text = ausdruck(projekt('einstreifig'), { ...DEFAULT_PDF_OPTIONS, includeSources: false, }); // Ohne die Klammer, weil runde Klammern im PDF maskiert werden. expect(text).toContain(ANLAGENART_LABELS.einstreifig.split(' (')[0]); }); it('weist die Umlaufzeitgrenzen des Knotenpunkts als Sicherheitsgrenze aus', () => { const text = ausdruck(projekt('knotenpunkt')); expect(text).toContain('Schranken der Anlagenart'); expect(text).toContain('30 s bis 120 s'); expect(text).toContain('Sicherheitsgrenze'); // Die RSA hat mit einem Knotenpunkt nichts zu tun. expect(text).not.toContain('Arbeitsstellen an Straßen'); }); it('sagt bei einstreifiger Fuehrung, dass die Grenzen nicht aus der RiLSA stammen', () => { const text = ausdruck(projekt('einstreifig')); expect(text).toContain('30 s bis 300 s'); expect(text).toContain('stammen NICHT aus der RiLSA'); expect(text).toContain('Arbeitsstellen an Straßen'); // Der Pruefstand muss die Zahl als nicht gesichert kennzeichnen. expect(text).toContain('abzugleichen'); // Die 300 s sind hergeleitet und keine Zahl aus einem Regelwerk - der // Ausdruck muss das sagen, sonst liest ein Pruefer sie als Regelwert. expect(text).toContain('keine Zahl aus einem Regelwerk'); }); it('behauptet an einer Arbeitsstelle keinen Knotenpunkt', () => { // Die Zeile mit dem Namen der Anlage trug die feste Beschriftung // "Knotenpunkt". An einer Arbeitsstelle gibt es keinen, und eine // Planunterlage darf keinen behaupten. const basis = projekt('einstreifig'); const mitNamen: Project = { ...basis, intersection: { ...basis.intersection, name: 'Engstelle B441' }, }; const text = ausdruck(mitNamen); expect(text).toContain('Bezeichnung der Anlage Engstelle B441'); expect(text).not.toContain('Knotenpunkt Engstelle B441'); }); });