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 61: Der Absatz zum OePNV-Anfahransatz (RiLSA Fall 4) nannte als * Fundort der massgebenden Vmax die Spalte "V zul" der Signalgruppentabelle. * Diese Tabelle entsteht nur bei options.includeSignalGroups, der Rechenweg * der Zwischenzeiten haengt an anderen Schaltern - drawIntergreenDetails * bekam den Schalter gar nicht uebergeben. * * Was der Anwender sonst erlebt: Er hat eine OePNV-Signalgruppe mit "Halt vor * dem Knotenpunkt", waehlt in der Ansicht "Ausgabe" die Signalgruppentabelle * ab und laesst Zwischenzeitenmatrix und Rechenweg an. Die Unterlage verweist * dann auf eine Spalte einer Tabelle, die sie nicht enthaelt, und die * gedruckte Raeumzeit laesst sich aus keiner gedruckten Groesse nachrechnen, * obwohl derselbe Absatz sich gerade auf diese Nachrechenbarkeit beruft. */ const STICHTAG = new Date('2026-01-01T12:00:00Z'); function gedruckt(text: string): string { return toWinAnsi(text); } 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 volltext(bytes: Uint8Array): string { const roh = new TextDecoder('latin1').decode(bytes); const stuecke: string[] = []; 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; for (const t of inhalt.matchAll(/\(((?:[^()\\]|\\.)*)\)\s*Tj/g)) { stuecke.push(t[1]!.replace(/\\([()\\])/g, '$1')); } } return stuecke.join(' ').replace(/\s+/g, ' '); } /** Knotenpunkt mit OePNV-Signalgruppen, die vor dem Knotenpunkt halten. */ function mitHaltefall(zweiteGruppe: boolean): Project { const basis = createStandardIntersectionProject('Musterkreuzung', STICHTAG); const bahn = { ...basis.signalGroups[0]!, id: 'sg-bahn', name: 'Strassenbahn Nordzufahrt', mode: 'oepnv' as const, vehicleClass: 'strassenbahn' as const, vZul: 50, }; const bus = { ...bahn, id: 'sg-bus', name: 'Buslinie Suedzufahrt', vehicleClass: 'bus' as const, vZul: 30, }; const halt = { ...basis.conflicts[0]!, id: 'cf-bahn', fromId: 'sg-bahn', toId: basis.signalGroups[1]!.id, clearingDistance: 25, enteringDistance: 8, haltVorKnoten: true, }; return { ...basis, signalGroups: [...basis.signalGroups, bahn, ...(zweiteGruppe ? [bus] : [])], conflicts: [ ...basis.conflicts, halt, ...(zweiteGruppe ? [{ ...halt, id: 'cf-bus', fromId: 'sg-bus' }] : []), ], }; } function ausdruck(mitSignalgruppen: boolean, zweiteGruppe = false): string { const projekt = mitHaltefall(zweiteGruppe); const plan = buildSignalPlan(projekt); return volltext( buildProjectPdf( projekt, plan, validateProject(projekt, plan, STICHTAG), { ...DEFAULT_PDF_OPTIONS, includeSignalGroups: mitSignalgruppen, includeIntergreenMatrix: true, includeCalculationDetails: true, }, STICHTAG, ), ); } describe('Anfahransatz ohne Signalgruppentabelle', () => { it('verweist nicht auf die Spalte einer Tabelle, die diese Ausgabe nicht enthaelt', () => { const text = ausdruck(false); // Vorbedingung: Der Absatz zum Anfahransatz steht im Dokument. expect(text).toContain(gedruckt('Anfahransatz')); expect(text).not.toContain(gedruckt('Spalte "V zul" der Signalgruppentabelle')); }); /* * Nachtrag zu Befund 61: Der Einschub lautete "Vmax betraegt * Strassenbahn Nordzufahrt 50 km/h" - Name und Zahl standen unverbunden * hintereinander, und bei zwei raeumenden Gruppen erfuhr der Leser nicht, * welcher Wert zu welcher Zeile der Rechenwegtabelle gehoert. */ it('nennt die massgebende Vmax in einem lesbaren Satz, damit tr nachrechenbar bleibt', () => { const text = ausdruck(false); expect(text).toContain( gedruckt( 'Die räumenden Signalgruppen dieses Ansatzes und ihre Vmax: ' + '„Strassenbahn Nordzufahrt" 50 km/h.', ), ); expect(text).toContain( gedruckt('Maßgebend ist je Zeile die in der Spalte „Beziehung" zuerst genannte'), ); }); it('zaehlt bei mehreren raeumenden Signalgruppen jede mit ihrer eigenen Vmax auf', () => { const text = ausdruck(false, true); expect(text).toContain( gedruckt( 'Die räumenden Signalgruppen dieses Ansatzes und ihre Vmax: ' + '„Strassenbahn Nordzufahrt" 50 km/h, „Buslinie Suedzufahrt" 30 km/h.', ), ); }); it('behaelt den Verweis, solange die Signalgruppentabelle im Dokument steht', () => { const text = ausdruck(true); expect(text).toContain(gedruckt('Spalte "V zul" der Signalgruppentabelle')); expect(text).toContain(gedruckt('tr = Vmax/(3,6 · a)')); // Doppelt genannt braucht die Vmax nicht zu werden: Die Tabelle fuehrt sie. expect(text).not.toContain(gedruckt('Die räumenden Signalgruppen dieses Ansatzes')); }); });