lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests export pdfStauraum.test.ts
| 1 | import { inflateSync } from 'node:zlib'; |
| 2 | import { describe, expect, it } from 'vitest'; |
| 3 | import { buildProjectPdf, DEFAULT_PDF_OPTIONS } from '@/services/export/pdf'; |
| 4 | import { buildSignalPlan } from '@/domain/plan/signalPlan'; |
| 5 | import { validateProject } from '@/domain/validation'; |
| 6 | import { createStandardIntersectionProject } from '@/domain/model/factory'; |
| 7 | import { STAURAUMBEDARF_JE_FAHRZEUG } from '@/domain/rilsa/constants'; |
| 8 | import { toWinAnsi } from '@/render/pdfSurface'; |
| 9 | import * as fmt from '@/ui/format'; |
| 10 | import type { Project } from '@/domain/model/project'; |
| 11 | |
| 12 | /** |
| 13 | * Der Rueckstau in der Planunterlage. |
| 14 | * |
| 15 | * Die Unterlage druckte die Fahrzeugzahl NGE seit jeher. Was fehlte, war die |
| 16 | * Laenge - und damit die Antwort auf die Frage, an der eine Abwaegung nach |
| 17 | * Paragraf 45 StVO haengt: Laeuft der Aufstellstreifen ueber? |
| 18 | * |
| 19 | * DIE FAELLE BEWACHEN VOR ALLEM DIE EHRLICHKEIT: dass der Ausdruck sagt, |
| 20 | * welche Groesse verglichen wurde, und dass die Laenge je Fahrzeug als |
| 21 | * Praxiswert ausgewiesen ist. |
| 22 | */ |
| 23 | |
| 24 | const STICHTAG = new Date('2026-01-01T12:00:00Z'); |
| 25 | |
| 26 | /** Der sichtbare Text der Datei, in einer Zeile. */ |
| 27 | function pdfText(bytes: Uint8Array): string { |
| 28 | const roh = new TextDecoder('latin1').decode(bytes); |
| 29 | const teile: string[] = []; |
| 30 | const suche = /stream\r?\n/g; |
| 31 | let treffer: RegExpExecArray | null; |
| 32 | while ((treffer = suche.exec(roh)) !== null) { |
| 33 | const start = treffer.index + treffer[0].length; |
| 34 | const ende = roh.indexOf('endstream', start); |
| 35 | if (ende < 0) continue; |
| 36 | let inhalt: string; |
| 37 | try { |
| 38 | inhalt = inflateSync(Buffer.from(roh.slice(start, ende), 'latin1')).toString('latin1'); |
| 39 | } catch { |
| 40 | continue; |
| 41 | } |
| 42 | for (const t of inhalt.matchAll(/\(((?:[^()\\]|\\.)*)\)\s*Tj/g)) { |
| 43 | teile.push(t[1]!.replace(/\\([()\\])/g, '$1')); |
| 44 | } |
| 45 | } |
| 46 | return teile.join(' ').replace(/\s+/g, ' '); |
| 47 | } |
| 48 | |
| 49 | function mitStauraum(stauraum: number | null): Project { |
| 50 | const roh = createStandardIntersectionProject('Hauptstraße / Bahnhofstraße', STICHTAG); |
| 51 | return { |
| 52 | ...roh, |
| 53 | signalGroups: roh.signalGroups.map((gruppe) => |
| 54 | gruppe.mode === 'kfz' && stauraum !== null ? { ...gruppe, stauraumlaenge: stauraum } : gruppe, |
| 55 | ), |
| 56 | demands: roh.signalGroups |
| 57 | .filter((g) => g.mode === 'kfz') |
| 58 | .map((g) => ({ signalGroupId: g.id, volume: 700, heavyVehicleShare: 0.05 })), |
| 59 | }; |
| 60 | } |
| 61 | |
| 62 | function ausdruck(project: Project): string { |
| 63 | const plan = buildSignalPlan(project); |
| 64 | const bericht = validateProject(project, plan, STICHTAG); |
| 65 | return pdfText(buildProjectPdf(project, plan, bericht, DEFAULT_PDF_OPTIONS, STICHTAG)); |
| 66 | } |
| 67 | |
| 68 | describe('Planunterlage - Rückstau und Stauraum', () => { |
| 69 | it('druckt einen eigenen Abschnitt mit der Länge', () => { |
| 70 | const text = ausdruck(mitStauraum(45)); |
| 71 | expect(text).toContain(toWinAnsi('Rückstau und Stauraum')); |
| 72 | expect(text).toContain(toWinAnsi('Rückstau je Fahrstreifen')); |
| 73 | expect(text).toContain(toWinAnsi(fmt.meters(45, 1))); |
| 74 | }); |
| 75 | |
| 76 | it('sagt, dass ein Mittelwert verglichen wurde', () => { |
| 77 | /* |
| 78 | * DER WICHTIGSTE FALL DIESER DATEI. Ohne den Satz liest eine Behörde |
| 79 | * "Auslastung 80 %" und schließt, der Stauraum reiche – während er in |
| 80 | * jedem fünften Umlauf überläuft. |
| 81 | */ |
| 82 | const text = ausdruck(mitStauraum(45)); |
| 83 | expect(text).toContain(toWinAnsi('MITTLERE RÜCKSTAU')); |
| 84 | expect(text).toContain(toWinAnsi('Hälfte der Umläufe steht mehr')); |
| 85 | expect(text).toContain(toWinAnsi('95-Prozent-Wert')); |
| 86 | }); |
| 87 | |
| 88 | it('weist die Länge je Fahrzeug als Praxiswert aus', () => { |
| 89 | const text = ausdruck(mitStauraum(45)); |
| 90 | expect(text).toContain(toWinAnsi(`NGE · ${fmt.meters(STAURAUMBEDARF_JE_FAHRZEUG, 0)}`)); |
| 91 | expect(text).toContain(toWinAnsi('weder die RiLSA 2015 noch das HBS 2015 nennen einen')); |
| 92 | // Und im Fundstellenverzeichnis, mit Prüfstand. |
| 93 | expect(text).toContain(toWinAnsi('Rückstaulänge aus dem mittleren Rückstau')); |
| 94 | }); |
| 95 | |
| 96 | it('druckt den Abschnitt auch ohne erfassten Stauraum', () => { |
| 97 | // Die Länge hilft auch ohne Vergleich weiter; nur die Spalte bleibt leer. |
| 98 | const text = ausdruck(mitStauraum(null)); |
| 99 | expect(text).toContain(toWinAnsi('Rückstau und Stauraum')); |
| 100 | expect(text).toContain(toWinAnsi('nicht erfasst')); |
| 101 | }); |
| 102 | |
| 103 | it('sagt, dass der Stauraum in keine Zahl der Bemessung eingeht', () => { |
| 104 | const text = ausdruck(mitStauraum(45)); |
| 105 | expect(text).toContain(toWinAnsi('geht in keine Zahl der Bemessung ein')); |
| 106 | }); |
| 107 | }); |