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 { messeignungSatz } from '@/domain/validation/rules'; import { messgenauigkeit, type Lageplan, type Planlinie } from '@/domain/geometrie/lageplan'; import { createStandardIntersectionProject } from '@/domain/model/factory'; import type { Project } from '@/domain/model/project'; /** * Die Eignung der Messgrundlage im Ausdruck. * * Der Abschnitt "Planungsgrundlage" ist ein Nachweis: Ein Pruefer muss ihm * entnehmen koennen, ob die Zwischenzeiten auf einer tauglichen Unterlage * beruhen. Ein Luftbild sieht grob wie fein gleich aus - ohne die Zahl bliebe * verborgen, dass eine Fussgaengerfurt auf einem 2000-m-Ausschnitt abgegriffen * wurde und die Zwischenzeit deshalb zu kurz sein kann. * * Zugleich gilt fuer diesen Abschnitt: kein Satz darf mehr behaupten, als das * Modell weiss. Woher ein einzelner Raeumweg stammt, weiss es nicht. */ const FIXED_DATE = new Date('2026-01-01T12:00:00Z'); /** * Genau auf der Fussgaenger-Schranke: 0,25 s / (2 * (1/1,0 + 1/11,1)) = * 0,114669 m je Bildpunkt - der raeumende Fussgaenger gegen das einfahrende * Kraftfahrzeug. Diese Unterlage ist gerade noch fuer alle Verkehrsarten * geeignet. */ const AN_DER_SCHRANKE = 0.25 / (2 * (1 / 1.0 + 1 / 11.1)); /** 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 Saetze sind auf Zeilen umbrochen; fuer die Suche wieder zusammenziehen. return teile.join(' ').replace(/\s+/g, ' '); } function ausdruck(project: Project): string { const plan = buildSignalPlan(project); const report = validateProject(project, plan, FIXED_DATE); return pdfText(buildProjectPdf(project, plan, report, DEFAULT_PDF_OPTIONS, FIXED_DATE)); } function fahrlinie(id: string): Planlinie { return { id, name: id, mode: 'kfz', movement: 'geradeaus', breiteMeter: 3.25, punkte: [ { x: 10, y: 10 }, { x: 10, y: 90 }, ], haltlinieId: null, startT: 0.5, signalGroupId: null, }; } /** Unterlage mit einem Massstab von `meterJeBildpunkt` (Bezugsstrecke 100 Bildpunkte). */ function lageplan(meterJeBildpunkt: number | null): Lageplan { return { arbeitsbereiche: [], signalgeber: [], bild: { datenUrl: 'data:image/png;base64,AAAA', breite: 7000, hoehe: 7000, herkunft: 'Luftbild NRW - Kante 2000 m', geladenAm: '2026-03-04T09:15:00.000Z', }, kalibrierung: meterJeBildpunkt === null ? null : { von: { x: 0, y: 0 }, bis: { x: 100, y: 0 }, laengeMeter: meterJeBildpunkt * 100, herkunft: 'georeferenziert', }, haltlinien: [], linien: [fahrlinie('l1')], }; } function mitLageplan(meterJeBildpunkt: number | null): Project { const basis = createStandardIntersectionProject('Ortsdurchfahrt', FIXED_DATE); return { ...basis, lageplan: lageplan(meterJeBildpunkt) }; } describe('Eignung als Messgrundlage im Ausdruck', () => { it('weist die Eignung mit der Zahl aus, an der sie haengt', () => { // 2000 m Kantenlaenge auf 7000 Bildpunkten - der Ausschnitt der Gruenen // Welle. Kfz-Wege bleiben abgreifbar, eine Furt nicht. const text = ausdruck(mitLageplan(2000 / 7000)); expect(text).toContain('Eignung als Messgrundlage'); expect(text).toContain('Nur für Kraftfahrzeugströme geeignet'); expect(text).toContain('28,6 cm je Bildpunkt'); // 2 * 0,285714 m * (1/1,0 + 1/11,1) = 0,622909 s an der Furt: Ein // Fussgaenger raeumt, ein Kraftfahrzeug faehrt ein. expect(text).toContain('0,62 s'); }); it('druckt denselben Befund wie der Pruefbericht', () => { // Zwei getrennt formulierte Befunde ueber dieselbe Unterlage waeren fuer // einen Pruefer nicht gegeneinander pruefbar. const project = mitLageplan(1.0); const satz = messeignungSatz(messgenauigkeit(project.lageplan)); const text = ausdruck(project); const bericht = validateProject(project, buildSignalPlan(project), FIXED_DATE); const befund = bericht.findings.find( (f) => f.rule === 'zwischenzeiten.messgrundlage-uebersicht', ); expect(befund?.message).toContain(satz); for (const stueck of satz.split(' ')) { expect(text).toContain(stueck); } expect(text).toContain('Zum Vermessen nicht geeignet - Übersichtsbild'); }); it('behauptet ohne Massstab keine Eignung', () => { const text = ausdruck(mitLageplan(null)); expect(text).toContain('Eignung als Messgrundlage'); expect(text).toContain('Nicht beurteilbar'); }); it('erklaert, worauf die Beurteilung beruht', () => { const text = ausdruck(mitLageplan(2000 / 7000)); expect(text).toContain('Angesetzt ist ein Zeichenfehler von 2 Bildpunkten'); expect(text).toContain('0,25 s'); }); it('behauptet nicht, die eingetragenen Wege stammten aus dieser Unterlage', () => { const text = ausdruck(mitLageplan(2000 / 7000)); expect(text).toContain('nicht, welche Wege tatsächlich hier abgegriffen wurden'); }); it('folgt den Kennwerten des Projekts, nicht dem Regelwerk unmittelbar', () => { // 0,114669 m je Bildpunkt halten die Schranke genau ein. Setzt der // Bearbeiter die Raeumgeschwindigkeit des mobilitaetseingeschraenkten // Fussgaengers auf 0,6 m/s herab, tut es dieselbe Unterlage nicht mehr // (0,402893 s statt 0,25 s) - und der Ausdruck muss das zeigen. Der // Kennwert clearingSpeedFuss, der hier frueher stand, geht in die // Einstufung nicht mehr ein und wuerde am Ausdruck nichts aendern. const project = mitLageplan(AN_DER_SCHRANKE); const streng: Project = { ...project, settings: { ...project.settings, rilsa: { clearingSpeedFussReducedMobility: 0.6 } }, }; expect(ausdruck(project)).toContain('Für alle Verkehrsarten geeignet'); expect(ausdruck(streng)).toContain('Nur für Kraftfahrzeugströme geeignet'); }); });