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 type { Project } from '@/domain/model/project'; /* * Befund 24: Das gedruckte Fundstellenverzeichnis speiste seine Kennwertzeilen * aus `plan.defaults` - das sind die AUFGELOESTEN Kennwerte einschliesslich * aller Vorgaben des Anwenders - und druckte diesen Wert in die Spalte * "Regelwert", mit dem Pruefstand der Quelle daneben. * * Was der Anwender sonst erlebt: Er traegt unter "Vorgaben" eine Gelbzeit von * 4 s ein (die RiLSA nennt 3 s) und druckt die Planunterlage. Im Abschnitt * "Abweichungen von den Regelwerten" steht dann "Gelbzeit Kfz bis 50 km/h | * 3 | 4 | s", im Fundstellenverzeichnis desselben PDF unter der * Spaltenueberschrift "Regelwert" aber 4 s - Regelwerk "RiLSA 2015", * Pruefstand "gesichert". Der VwV-StVO-Eintrag desselben Verzeichnisses * kennzeichnet die Abweichung sogar ausdruecklich. Vor der Behoerde erscheint * ein selbst gesetzter Wert damit als gesicherter Wert des Regelwerks. */ const STICHTAG = new Date('2026-01-01T12:00:00Z'); const PAGE_MARGIN = 36; const CONTENT_WIDTH = 841.89 - 2 * PAGE_MARGIN; /* * Die Spalten des Fundstellenverzeichnisses, aus den festen Breiten des * Ausdrucks gerechnet: Regelwerk 70, Kennwert (Rest), Regelwert 62, * Sachgebiet 175, Fundstelle 110, Pruefstand 62. */ const KENNWERT_X = PAGE_MARGIN + 70; const KENNWERT_BREITE = CONTENT_WIDTH - (70 + 62 + 175 + 110 + 62); const REGELWERT_X = KENNWERT_X + KENNWERT_BREITE; const REGELWERT_ENDE = REGELWERT_X + 62; /** Innenabstand der Tabellenzellen dieses Verzeichnisses. */ const ZELLENRAND = 2.5; interface Stueck { readonly blatt: number; readonly x: number; readonly y: number; readonly text: string; } function entpacke(roh: string, von: number): string | null { const anfang = roh.indexOf('stream', von); if (anfang < 0) return null; const start = anfang + (roh.startsWith('stream\r\n', anfang) ? 8 : 7); const ende = roh.indexOf('endstream', start); if (ende < 0) return null; try { return inflateSync(Buffer.from(roh.slice(start, ende), 'latin1')).toString('latin1'); } catch { return null; } } function textstuecke(bytes: Uint8Array): readonly Stueck[] { const roh = new TextDecoder('latin1').decode(bytes); const raus: Stueck[] = []; let blatt = 0; for (const treffer of roh.matchAll(/\/Type \/Page\b[^>]*?\/Contents (\d+) 0 R/g)) { const objekt = roh.indexOf(`\n${treffer[1]!} 0 obj`); const inhalt = objekt < 0 ? null : entpacke(roh, objekt); if (inhalt === null) continue; blatt += 1; let x = 0; let y = 0; let abstand = 0; for (const roheZeile of inhalt.split('\n')) { const zeile = roheZeile.trim(); const tl = /^(-?[\d.]+) TL$/.exec(zeile); if (tl) { abstand = Number(tl[1]); continue; } const td = /^(-?[\d.]+) (-?[\d.]+) Td$/.exec(zeile); if (td) { x = Number(td[1]); y = Number(td[2]); continue; } let rest = zeile; while (rest.startsWith('T*')) { y -= abstand; rest = rest.slice(2).trim(); } const tj = /^\(((?:[^()\\]|\\.)*)\)\s*Tj$/.exec(rest); if (tj) raus.push({ blatt, x, y, text: tj[1]!.replace(/\\([()\\])/g, '$1') }); } } return raus; } /** Knotenpunkt mit einer selbst gesetzten Gelbzeit von 4 s (Regelwert 3 s). */ function mitVorgabe(): Project { const basis = createStandardIntersectionProject('Musterkreuzung', STICHTAG); return { ...basis, settings: { ...basis.settings, rilsa: { ...basis.settings.rilsa, yellowKfzUpTo50: 4 } }, }; } function unterlage(projekt: Project): readonly Stueck[] { const plan = buildSignalPlan(projekt); return textstuecke( buildProjectPdf( projekt, plan, validateProject(projekt, plan, STICHTAG), DEFAULT_PDF_OPTIONS, STICHTAG, ), ); } /** Die Zelle "Regelwert" der Zeile, deren Kennwertzelle mit `label` beginnt. */ function regelwertzelle(stuecke: readonly Stueck[], label: string): readonly string[] { const kennwert = stuecke.filter( (s) => Math.abs(s.x - (KENNWERT_X + ZELLENRAND)) < 0.5 && s.text.startsWith(label), ); expect(kennwert.length, `Vorbedingung: Zeile "${label}" im Fundstellenverzeichnis`).toBe(1); const zeile = kennwert[0]!; return stuecke .filter((s) => s.blatt === zeile.blatt && Math.abs(s.y - zeile.y) < 0.5) .filter((s) => s.x >= REGELWERT_X && s.x < REGELWERT_ENDE) .map((s) => s.text); } describe('Regelwertspalte des Fundstellenverzeichnisses', () => { it('druckt unter "Regelwert" den Wert des Regelwerks, nicht die Vorgabe des Anwenders', () => { const stuecke = unterlage(mitVorgabe()); expect(regelwertzelle(stuecke, toWinAnsi('Gelbzeit Kfz bis 50 km/h'))).toEqual(['3 s']); }); it('verschweigt den angesetzten Wert nicht, sondern kennzeichnet ihn als Abweichung', () => { const stuecke = unterlage(mitVorgabe()); const kennwert = stuecke.filter( (s) => Math.abs(s.x - (KENNWERT_X + ZELLENRAND)) < 0.5 && s.text.startsWith(toWinAnsi('Gelbzeit Kfz bis 50 km/h')), ); expect(kennwert.map((s) => s.text)).toEqual([ toWinAnsi('Gelbzeit Kfz bis 50 km/h (angesetzt: 4 s, abweichend vom Regelwert)'), ]); }); it('laesst die Zeile unveraendert, solange keine Vorgabe gesetzt ist', () => { const stuecke = unterlage(createStandardIntersectionProject('Musterkreuzung', STICHTAG)); expect(regelwertzelle(stuecke, toWinAnsi('Gelbzeit Kfz bis 50 km/h'))).toEqual(['3 s']); const kennwert = stuecke.filter( (s) => Math.abs(s.x - (KENNWERT_X + ZELLENRAND)) < 0.5 && s.text.startsWith(toWinAnsi('Gelbzeit Kfz bis 50 km/h')), ); expect(kennwert.map((s) => s.text)).toEqual([toWinAnsi('Gelbzeit Kfz bis 50 km/h')]); }); });