lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests export pdfHerkunftsangaben.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 { REGELBREITE } from '@/domain/geometrie/vermessung'; |
| 8 | import type { Lageplan, Planlinie } from '@/domain/geometrie/lageplan'; |
| 9 | import type { Project } from '@/domain/model/project'; |
| 10 | |
| 11 | /** |
| 12 | * Ehrlichkeit der Herkunftsangaben im Ausdruck. |
| 13 | * |
| 14 | * Das Modell fuehrt nicht mit, woher Raeum- und Einfahrweg einer |
| 15 | * Konfliktbeziehung stammen: `Conflict.note` ist ein freies Bemerkungsfeld. |
| 16 | * uebernimmWege hinterlaesst dort zwar "Aus dem Lageplan vermessen", doch der |
| 17 | * Bearbeiter kann den Text ueberschreiben, und vor allem bleibt er stehen, wenn |
| 18 | * er anschliessend die Wege von Hand aendert - die Massenuebernahme im |
| 19 | * Konfliktdialog tut genau das. |
| 20 | * |
| 21 | * Ein Ausdruck, der diesen Text als Herkunftsnachweis ausgibt, macht in einer |
| 22 | * Unterlage fuer ein verkehrsbehoerdliches Verfahren eine falsche Angabe zur |
| 23 | * Herkunft eines sicherheitsrelevanten Masses. Diese Tests halten fest, dass |
| 24 | * der Ausdruck nur behauptet, was das Modell hergibt. |
| 25 | */ |
| 26 | |
| 27 | const FIXED_DATE = new Date('2026-01-01T12:00:00Z'); |
| 28 | |
| 29 | /** Sichtbaren Text aus dem PDF lesen; die Inhaltsstroeme sind komprimiert. */ |
| 30 | function pdfText(bytes: Uint8Array): string { |
| 31 | const roh = new TextDecoder('latin1').decode(bytes); |
| 32 | const teile: string[] = []; |
| 33 | const suche = /stream\r?\n/g; |
| 34 | let treffer: RegExpExecArray | null; |
| 35 | |
| 36 | while ((treffer = suche.exec(roh)) !== null) { |
| 37 | const start = treffer.index + treffer[0].length; |
| 38 | const ende = roh.indexOf('endstream', start); |
| 39 | if (ende < 0) continue; |
| 40 | let inhalt: string; |
| 41 | try { |
| 42 | inhalt = inflateSync(Buffer.from(roh.slice(start, ende), 'latin1')).toString('latin1'); |
| 43 | } catch { |
| 44 | continue; |
| 45 | } |
| 46 | for (const stueck of inhalt.matchAll(/\((?:\\.|[^\\()])*\)\s*Tj/g)) { |
| 47 | teile.push(stueck[0].slice(1, stueck[0].lastIndexOf(')'))); |
| 48 | } |
| 49 | } |
| 50 | // Lange Absaetze sind auf Zeilen umbrochen; fuer die Suche wieder |
| 51 | // zusammenziehen, sonst findet contains() den halben Satz nicht. |
| 52 | return teile.join(' ').replace(/\s+/g, ' '); |
| 53 | } |
| 54 | |
| 55 | function ausdruck(project: Project): string { |
| 56 | const plan = buildSignalPlan(project); |
| 57 | const report = validateProject(project, plan, FIXED_DATE); |
| 58 | return pdfText( |
| 59 | buildProjectPdf( |
| 60 | project, |
| 61 | plan, |
| 62 | report, |
| 63 | { ...DEFAULT_PDF_OPTIONS, includeCalculationDetails: true }, |
| 64 | FIXED_DATE, |
| 65 | ), |
| 66 | ); |
| 67 | } |
| 68 | |
| 69 | /** Fahrlinie mit zwei Punkten - genug, damit der Lageplan auswertbar ist. */ |
| 70 | function fahrlinie(id: string): Planlinie { |
| 71 | return { |
| 72 | id, |
| 73 | name: id, |
| 74 | mode: 'kfz', |
| 75 | movement: 'geradeaus', |
| 76 | breiteMeter: REGELBREITE.kfz, |
| 77 | punkte: [ |
| 78 | { x: 10, y: 10 }, |
| 79 | { x: 10, y: 90 }, |
| 80 | ], |
| 81 | haltlinieId: null, |
| 82 | startT: 0.5, |
| 83 | signalGroupId: null, |
| 84 | }; |
| 85 | } |
| 86 | |
| 87 | const AUSWERTBARER_LAGEPLAN: Lageplan = { |
| 88 | arbeitsbereiche: [], |
| 89 | signalgeber: [], |
| 90 | bild: { |
| 91 | datenUrl: 'data:image/png;base64,AAAA', |
| 92 | breite: 800, |
| 93 | hoehe: 600, |
| 94 | herkunft: 'Luftbild NRW · Land NRW · Kante 400 m', |
| 95 | geladenAm: '2026-03-04T09:15:00.000Z', |
| 96 | }, |
| 97 | kalibrierung: { |
| 98 | von: { x: 100, y: 200 }, |
| 99 | bis: { x: 200, y: 200 }, |
| 100 | laengeMeter: 50, |
| 101 | herkunft: 'gemessen', |
| 102 | }, |
| 103 | haltlinien: [], |
| 104 | linien: [fahrlinie('l1'), fahrlinie('l2')], |
| 105 | }; |
| 106 | |
| 107 | describe('Herkunftsangaben im Ausdruck', () => { |
| 108 | it('weist die Bemerkung einer Beziehung als Bemerkung aus, nicht als Herkunft', () => { |
| 109 | const text = ausdruck(createStandardIntersectionProject('Testknoten', FIXED_DATE)); |
| 110 | |
| 111 | expect(text).toContain( |
| 112 | 'Die Spalte "Bemerkung" gibt daneben den freien Vermerk des Bearbeiters', |
| 113 | ); |
| 114 | expect(text).toContain('Sie ist kein Nachweis der Herkunft'); |
| 115 | // Der Vorgaenger erhob den freien Text zum Nachweis der Vermessung. |
| 116 | expect(text).not.toContain('setzt die Übernahme aus dem Lageplan'); |
| 117 | // Der Nachweis steht jetzt in einer EIGENEN Spalte, die das Modell fuehrt. |
| 118 | expect(text).toContain('Herkunft der Wege'); |
| 119 | expect(text).toContain('Das Programm führt die Angabe selbst und lässt sie nicht beschreiben'); |
| 120 | }); |
| 121 | |
| 122 | it('erfindet fuer eine leere Bemerkung keine Herkunft', () => { |
| 123 | // Die Beziehungen des Regelknotenpunkts tragen keine Bemerkung. Ihre Wege |
| 124 | // stammen aus der Vorlage, nicht aus der Hand des Bearbeiters - "von Hand |
| 125 | // erfasst" waere schon hier falsch. |
| 126 | const text = ausdruck(createStandardIntersectionProject('Testknoten', FIXED_DATE)); |
| 127 | |
| 128 | expect(text).not.toContain('von Hand erfasst'); |
| 129 | }); |
| 130 | |
| 131 | it('behauptet keine Vermessung, wenn der Weg nach der Uebernahme geaendert wurde', () => { |
| 132 | const basis = createStandardIntersectionProject('Testknoten', FIXED_DATE); |
| 133 | const erster = basis.conflicts[0]; |
| 134 | expect(erster).toBeDefined(); |
| 135 | if (erster === undefined) return; |
| 136 | |
| 137 | // Genau der gefaehrliche Ablauf: aus dem Lageplan uebernommen, danach den |
| 138 | // Raeumweg von Hand verlaengert. Die Bemerkung bleibt stehen. |
| 139 | const project: Project = { |
| 140 | ...basis, |
| 141 | conflicts: basis.conflicts.map((c) => |
| 142 | c.id === erster.id |
| 143 | ? { ...c, note: 'Aus dem Lageplan vermessen.', clearingDistance: 41 } |
| 144 | : c, |
| 145 | ), |
| 146 | }; |
| 147 | |
| 148 | const text = ausdruck(project); |
| 149 | |
| 150 | // Die Bemerkung darf erscheinen - aber nur als Bemerkung, mit dem Hinweis, |
| 151 | // dass sie aelter sein kann als der Wert daneben. |
| 152 | expect(text).toContain('Aus dem Lageplan vermessen.'); |
| 153 | expect(text).toContain('kann aus der Zeit vor einer späteren Änderung der Wege stammen'); |
| 154 | // Und die Spalte, die den Nachweis traegt, darf sich davon nicht |
| 155 | // anstecken lassen: Die Beziehungen des Regelknotenpunkts sind nicht aus |
| 156 | // einem Lageplan uebernommen worden, egal was in der Bemerkung steht. |
| 157 | expect(text).toContain('Herkunft nicht nachgewiesen'); |
| 158 | }); |
| 159 | |
| 160 | it('behauptet in der Planungsgrundlage nicht, die Masse stammten aus der Unterlage', () => { |
| 161 | const basis = createStandardIntersectionProject('Testknoten', FIXED_DATE); |
| 162 | const text = ausdruck({ ...basis, lageplan: AUSWERTBARER_LAGEPLAN }); |
| 163 | |
| 164 | expect(text).toContain('Der Lageplan ist maßstäblich auswertbar'); |
| 165 | // Seit das Modell die Herkunft je Beziehung fuehrt, waere der alte Satz |
| 166 | // ("Das Projekt fuehrt die Herkunft der einzelnen Masse nicht mit") falsch. |
| 167 | expect(text).not.toContain('führt die Herkunft der einzelnen Maße nicht mit'); |
| 168 | expect(text).toContain('führt das Projekt je Beziehung mit'); |
| 169 | // Der Vorgaenger verwies fuer die Uebernahme auf die Bemerkungsspalte. |
| 170 | expect(text).not.toContain('Welche Beziehungen daraus übernommen wurden'); |
| 171 | }); |
| 172 | |
| 173 | it('behauptet ohne Lageplan nicht, die Wege seien von Hand erfasst', () => { |
| 174 | const text = ausdruck(createStandardIntersectionProject('Testknoten', FIXED_DATE)); |
| 175 | |
| 176 | expect(text).toContain('Diesen Unterlagen liegt kein Lageplan und kein Luftbild bei'); |
| 177 | expect(text).toContain('stammen daher nicht aus diesen Unterlagen'); |
| 178 | }); |
| 179 | }); |