lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests export schwerverkehrZusage.test.ts
| 1 | import { readFileSync } from 'node:fs'; |
| 2 | import { inflateSync } from 'node:zlib'; |
| 3 | import { describe, expect, it } from 'vitest'; |
| 4 | import { buildProjectPdf, DEFAULT_PDF_OPTIONS } from '@/services/export/pdf'; |
| 5 | import { buildSignalPlan } from '@/domain/plan/signalPlan'; |
| 6 | import { validateProject } from '@/domain/validation'; |
| 7 | import { createStandardIntersectionProject } from '@/domain/model/factory'; |
| 8 | import { toWinAnsi } from '@/render/pdfSurface'; |
| 9 | import type { Project } from '@/domain/model/project'; |
| 10 | |
| 11 | /* |
| 12 | * Befund 34 zur Fassung 5.9.0, der Rest im Ausdruck. |
| 13 | * |
| 14 | * Der gedruckte Erlaeuterungsabsatz ist berichtigt (bewertung.ts, |
| 15 | * schwerverkehrSatz: die Spalte fuehrt qS0 · n · fA / fSV), und die |
| 16 | * gleichlautende Zusage in csv.ts ist nachgezogen. Der Docblock von |
| 17 | * `schwerverkehrZelle` in pdf.ts trug sie weiter im alten Wortlaut: "Mit dem |
| 18 | * Faktor daneben laesst sich die Spalte 'Saettigungsverkehrsstaerke' aus dem |
| 19 | * Regelwert je Fahrstreifen nachrechnen: qS = 3600 / (fSV * tB)". |
| 20 | * |
| 21 | * Sie ist doppelt widerlegt: Die Spalte fuehrt zusaetzlich die Fahrstreifenzahl |
| 22 | * und - bei links und rechts abbiegenden Stroemen - die Abminderung fA, und das |
| 23 | * Zeichen qS steht in derselben Zeile einmal fuer den Wert je Fahrstreifen und |
| 24 | * einmal fuer den des ganzen Stroms. Eine Zusage im Kommentar ist Teil des |
| 25 | * Programms; wer das Verhalten aendert, aendert den Kommentar daneben. |
| 26 | * |
| 27 | * Der Docblock nennt jetzt dieselbe Kette wie der gedruckte Absatz und sagt, |
| 28 | * WO n und die Fahrbeziehung in der Unterlage stehen: in der |
| 29 | * Signalgruppentabelle. Eine zweite Fahrstreifenspalte in der Leistungstabelle |
| 30 | * traegt nicht - ohne die Fahrbeziehung derselben Zeile waere die Kette dort |
| 31 | * ohnehin nicht geschlossen, und die Angabe stuende ein zweites Mal in der |
| 32 | * Unterlage. |
| 33 | * |
| 34 | * NACHGEBESSERT: Diese Begruendung gilt nur, solange die Signalgruppentabelle |
| 35 | * gedruckt wird. Sie ist abwaehlbar (exportView.ts, `includeSignalGroups`), und |
| 36 | * dann stand die Kette "qS0 · n · fA / fSV" in einer Unterlage, in der weder |
| 37 | * "Fahrstr." noch "Fahrbeziehung" vorkam - aus keiner gedruckten Groesse zu |
| 38 | * schliessen. Derselbe Fall ist in dieser Datei beim Anfahransatz bereits |
| 39 | * geloest (Befund 61 zur Fassung 5.8.0): Der Absatz nennt die Abwahl und traegt |
| 40 | * die Werte selbst nach. Der Erlaeuterungsabsatz der Leistungstabelle tut das |
| 41 | * jetzt ebenso. |
| 42 | * |
| 43 | * MUTATIONSNACHWEIS, nachgemessen an zwei zurueckgesetzten Staenden: |
| 44 | * |
| 45 | * - Docblock im alten Wortlaut: 2 von 5 Faellen fallen - der erste und der |
| 46 | * zweite. |
| 47 | * - Ohne den Nachsatz des Erlaeuterungsabsatzes |
| 48 | * (`fahrstreifenHerkunftSatz`, pdf.ts): 2 von 5 fallen - der dritte und |
| 49 | * der vierte. |
| 50 | * |
| 51 | * Der fuenfte ist in beiden Staenden gruen: Er haelt fest, dass die Angabe bei |
| 52 | * gedruckter Signalgruppentabelle NICHT ein zweites Mal in der Unterlage |
| 53 | * steht, und bewacht damit die Gegenrichtung. |
| 54 | */ |
| 55 | |
| 56 | const STICHTAG = new Date('2026-01-01T12:00:00Z'); |
| 57 | const QUELLE = readFileSync('src/services/export/pdf.ts', 'utf8'); |
| 58 | |
| 59 | /** Der Docblock unmittelbar vor einer Funktion. */ |
| 60 | function docblock(quelle: string, funktion: string): string { |
| 61 | const stelle = quelle.indexOf(`function ${funktion}(`); |
| 62 | expect(stelle, `${funktion} steht in der Quelle`).toBeGreaterThan(0); |
| 63 | const davor = quelle.slice(0, stelle); |
| 64 | const anfang = davor.lastIndexOf('/**'); |
| 65 | const ende = davor.lastIndexOf('*/'); |
| 66 | expect(anfang, `${funktion} hat einen Docblock`).toBeGreaterThan(0); |
| 67 | expect(ende).toBeGreaterThan(anfang); |
| 68 | return davor.slice(anfang, ende + 2); |
| 69 | } |
| 70 | |
| 71 | /** Sichtbaren Text aus dem PDF lesen; die Inhaltsstroeme sind komprimiert. */ |
| 72 | function pdfText(bytes: Uint8Array): string { |
| 73 | const roh = new TextDecoder('latin1').decode(bytes); |
| 74 | const teile: string[] = []; |
| 75 | const suche = /stream\r?\n/g; |
| 76 | let treffer: RegExpExecArray | null; |
| 77 | |
| 78 | while ((treffer = suche.exec(roh)) !== null) { |
| 79 | const start = treffer.index + treffer[0].length; |
| 80 | const ende = roh.indexOf('endstream', start); |
| 81 | if (ende < 0) continue; |
| 82 | let inhalt: string; |
| 83 | try { |
| 84 | inhalt = inflateSync(Buffer.from(roh.slice(start, ende), 'latin1')).toString('latin1'); |
| 85 | } catch { |
| 86 | continue; |
| 87 | } |
| 88 | for (const t of inhalt.matchAll(/\(((?:[^()\\]|\\.)*)\)\s*Tj/g)) { |
| 89 | teile.push(t[1]!.replace(/\\([()\\])/g, '$1')); |
| 90 | } |
| 91 | } |
| 92 | return teile.join(' ').replace(/\s+/g, ' '); |
| 93 | } |
| 94 | |
| 95 | function ausdruck(project: Project, optionen = DEFAULT_PDF_OPTIONS): string { |
| 96 | const plan = buildSignalPlan(project); |
| 97 | return pdfText( |
| 98 | buildProjectPdf(project, plan, validateProject(project, plan, STICHTAG), optionen, STICHTAG), |
| 99 | ); |
| 100 | } |
| 101 | |
| 102 | describe('Befund 34 - die Zusage am Ausdruck nennt die vollständige Kette', () => { |
| 103 | it('trägt die widerlegte Rechnung nicht mehr', () => { |
| 104 | const block = docblock(QUELLE, 'schwerverkehrZelle'); |
| 105 | |
| 106 | // Der alte Wortlaut, Zeichen fuer Zeichen: "Mit dem Faktor daneben laesst |
| 107 | // sich die Spalte ... nachrechnen: qS = 3600 / (fSV * tB)". Was der |
| 108 | // Docblock heute davon nennt, nennt er als das, was hier STAND. |
| 109 | expect(block).not.toContain('qS = 3600 / (fSV * tB)'); |
| 110 | expect(block).not.toContain('laesst sich die Spalte'); |
| 111 | }); |
| 112 | |
| 113 | it('nennt dieselbe Kette wie der gedruckte Absatz und wo ihre Glieder stehen', () => { |
| 114 | const block = docblock(QUELLE, 'schwerverkehrZelle'); |
| 115 | |
| 116 | expect(block).toContain('qS0 · n · fA / fSV'); |
| 117 | // n und die Fahrbeziehung, aus der fA folgt, stehen in der |
| 118 | // Signalgruppentabelle - deshalb braucht die Leistungstabelle keine |
| 119 | // zweite Fahrstreifenspalte. |
| 120 | expect(block).toContain('Signalgruppentabelle'); |
| 121 | }); |
| 122 | |
| 123 | it('druckt die Kette im Erläuterungsabsatz und die Glieder in der Signalgruppentabelle', () => { |
| 124 | // Zusammenhangspruefung: Der Kommentar sagt zu, was die Unterlage haelt. |
| 125 | const text = ausdruck(createStandardIntersectionProject('Schwerverkehr', STICHTAG)); |
| 126 | |
| 127 | expect(text).toContain(toWinAnsi('qS0 · n · fA / fSV')); |
| 128 | expect(text).toContain('Fahrstr.'); |
| 129 | expect(text).toContain('Fahrbeziehung'); |
| 130 | // Und er sagt auch, WO sie stehen - sonst muesste der Leser die Kette |
| 131 | // selbst in der Unterlage suchen. |
| 132 | expect(text).toContain(toWinAnsi('Spalten "Fahrstr." und "Fahrbeziehung"')); |
| 133 | }); |
| 134 | |
| 135 | it('nennt n und die Fahrbeziehung selbst, wenn die Signalgruppentabelle abgewählt ist', () => { |
| 136 | /* |
| 137 | * Der Haken "Signalgruppen" in der Ausgabeansicht (exportView.ts). Ohne |
| 138 | * die Tabelle steht die Kette qS0 · n · fA / fSV in einer Unterlage, in |
| 139 | * der n und die Fahrbeziehung nirgends vorkommen. Dasselbe Muster wie beim |
| 140 | * Anfahransatz (Befund 61 zur Fassung 5.8.0): Die Abwahl wird genannt, |
| 141 | * die Werte werden nachgetragen. |
| 142 | */ |
| 143 | const projekt = createStandardIntersectionProject('Schwerverkehr', STICHTAG); |
| 144 | const text = ausdruck(projekt, { ...DEFAULT_PDF_OPTIONS, includeSignalGroups: false }); |
| 145 | |
| 146 | expect(text, 'Vorbedingung: die Spalte "Fahrstr." gibt es nicht mehr').not.toContain( |
| 147 | 'Fahrstr.', |
| 148 | ); |
| 149 | expect(text).toContain(toWinAnsi('ist für diese Ausgabe abgewählt')); |
| 150 | for (const gruppe of projekt.signalGroups.filter((g) => g.mode === 'kfz')) { |
| 151 | expect(text, gruppe.name).toContain( |
| 152 | toWinAnsi(`"${gruppe.name}" ${gruppe.lanes} Fahrstreifen, geradeaus`), |
| 153 | ); |
| 154 | } |
| 155 | }); |
| 156 | |
| 157 | it('trägt bei gedruckter Signalgruppentabelle keine zweite Fahrstreifenangabe nach', () => { |
| 158 | // Die Gegenprobe: Steht die Tabelle in der Unterlage, bleibt die Angabe an |
| 159 | // ihrem einen Ort - sonst stuende sie zweimal da. |
| 160 | const text = ausdruck(createStandardIntersectionProject('Schwerverkehr', STICHTAG)); |
| 161 | |
| 162 | expect(text).not.toContain(toWinAnsi('Fahrstreifen, geradeaus')); |
| 163 | }); |
| 164 | }); |