lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests export angesetzteSchranken.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 { ANLAGENART_GRENZEN } from '@/domain/rilsa/constants'; |
| 8 | import { PRUEFSTAND_LABELS } from '@/domain/rilsa/quellen'; |
| 9 | import { toWinAnsi } from '@/render/pdfSurface'; |
| 10 | import type { AnlagenartGrenzen } from '@/domain/rilsa/constants'; |
| 11 | import type { Project } from '@/domain/model/project'; |
| 12 | import * as fmt from '@/ui/format'; |
| 13 | |
| 14 | /* |
| 15 | * BEFUND 7 (Fassung 5.11.0): Der Abschnitt "Schranken der Anlagenart" las seine |
| 16 | * Grenzen aus der Konstanten ANLAGENART_GRENZEN statt aus `plan.grenzen`. |
| 17 | * |
| 18 | * Was der Anwender sonst erlebt: Er traegt in der Vorgabenverwaltung eine |
| 19 | * engere Umlaufzeit ein (45 bis 100 s). Der Pruefbericht misst daran |
| 20 | * (engine.ts: `plan.grenzen.cycleTime`), die Aufstellung "Abweichungen von den |
| 21 | * Regelwerten" fuehrt die Vorgabe als Abweichung - und zwei Seiten weiter |
| 22 | * druckt die Spalte "Angesetzt" der Schrankentabelle "30 s bis 120 s" mit dem |
| 23 | * Pruefstand "gesichert". Ein Pruefer bekommt zwei Zahlenwerke in EINER |
| 24 | * Anordnungsunterlage und kann sie nicht zur Deckung bringen; zugleich verweist |
| 25 | * quellen.ts im selben Dokument ausdruecklich auf diesen Abschnitt als die |
| 26 | * Stelle, die die WIRKSAME Schranke nennt. |
| 27 | * |
| 28 | * Der Satz unter der Tabelle bleibt beim Regelwerkswert: Die 120 s der RiLSA |
| 29 | * sind als Sicherheitsgrenze begruendet, eine selbst gesetzte Obergrenze traegt |
| 30 | * diese Begruendung nicht (Grundsatz: selbstgesetzte Schwellen tragen nie |
| 31 | * "gesichert"). |
| 32 | */ |
| 33 | |
| 34 | const STICHTAG = new Date('2026-01-01T12:00:00Z'); |
| 35 | |
| 36 | /** Sichtbaren Text aus dem PDF lesen; die Inhaltsstroeme sind komprimiert. */ |
| 37 | function pdfText(bytes: Uint8Array): string { |
| 38 | const roh = new TextDecoder('latin1').decode(bytes); |
| 39 | const teile: string[] = []; |
| 40 | const suche = /stream\r?\n/g; |
| 41 | let treffer: RegExpExecArray | null; |
| 42 | |
| 43 | while ((treffer = suche.exec(roh)) !== null) { |
| 44 | const start = treffer.index + treffer[0].length; |
| 45 | const ende = roh.indexOf('endstream', start); |
| 46 | if (ende < 0) continue; |
| 47 | let inhalt: string; |
| 48 | try { |
| 49 | inhalt = inflateSync(Buffer.from(roh.slice(start, ende), 'latin1')).toString('latin1'); |
| 50 | } catch { |
| 51 | continue; |
| 52 | } |
| 53 | for (const t of inhalt.matchAll(/\(((?:[^()\\]|\\.)*)\)\s*Tj/g)) { |
| 54 | teile.push(t[1]!.replace(/\\([()\\])/g, '$1')); |
| 55 | } |
| 56 | } |
| 57 | return teile.join(' ').replace(/\s+/g, ' '); |
| 58 | } |
| 59 | |
| 60 | function projekt(vorgaben: Project['settings']['rilsa']): Project { |
| 61 | const basis = createStandardIntersectionProject('Hauptstrasse / Bahnhofstrasse', STICHTAG); |
| 62 | return { |
| 63 | ...basis, |
| 64 | settings: { ...basis.settings, rilsa: { ...basis.settings.rilsa, ...vorgaben } }, |
| 65 | }; |
| 66 | } |
| 67 | |
| 68 | function ausdruck(vorgaben: Project['settings']['rilsa']): { |
| 69 | text: string; |
| 70 | grenzen: ReturnType<typeof buildSignalPlan>['grenzen']; |
| 71 | } { |
| 72 | const project = projekt(vorgaben); |
| 73 | const plan = buildSignalPlan(project); |
| 74 | const bericht = validateProject(project, plan, STICHTAG); |
| 75 | return { |
| 76 | text: pdfText(buildProjectPdf(project, plan, bericht, DEFAULT_PDF_OPTIONS, STICHTAG)), |
| 77 | grenzen: plan.grenzen, |
| 78 | }; |
| 79 | } |
| 80 | |
| 81 | const gedruckt = (text: string): string => toWinAnsi(text).replace(/\s+/g, ' '); |
| 82 | |
| 83 | const REGELWERK = ANLAGENART_GRENZEN.knotenpunkt; |
| 84 | |
| 85 | const spanne = (g: AnlagenartGrenzen, welche: 'zulaessig' | 'regelbereich'): string => |
| 86 | welche === 'zulaessig' |
| 87 | ? `${fmt.seconds(g.cycleTime.min)} bis ${fmt.seconds(g.cycleTime.max)}` |
| 88 | : `${fmt.seconds(g.cycleTime.recommendedMin)} bis ${fmt.seconds(g.cycleTime.recommendedMax)}`; |
| 89 | |
| 90 | /* |
| 91 | * NACHTRAG ZU BEFUND 7: Der angesetzte Wert allein genuegt nicht. Grundlage |
| 92 | * und Pruefstand derselben Zeile kommen aus `grenzen.herkunft`, und das ist |
| 93 | * unveraendert die Herkunft der Anlagenart - neben einer eingeengten Umlaufzeit |
| 94 | * stand damit "RiLSA 2015 ... (30 bis 120 s)" und der Pruefstand "gesichert". |
| 95 | * Geprueft wird deshalb die GANZE Zeile: Wo der angesetzte Wert vom Regelwert |
| 96 | * abweicht, traegt der Zeilenkopf den Vermerk, und erst danach folgen Wert, |
| 97 | * Grundlage und Pruefstand. |
| 98 | */ |
| 99 | function zeile( |
| 100 | kopf: string, |
| 101 | grenzen: AnlagenartGrenzen, |
| 102 | welche: 'zulaessig' | 'regelbereich', |
| 103 | ): string { |
| 104 | const angesetzt = spanne(grenzen, welche); |
| 105 | const regelwert = spanne(REGELWERK, welche); |
| 106 | const vermerk = |
| 107 | angesetzt === regelwert ? '' : ` (eigene Vorgabe, abweichend vom Regelwert ${regelwert})`; |
| 108 | const h = |
| 109 | welche === 'zulaessig' ? REGELWERK.herkunft.umlaufzeit : REGELWERK.herkunft.regelbereich; |
| 110 | return gedruckt(`${kopf}${vermerk} ${angesetzt} ${h.text} ${PRUEFSTAND_LABELS[h.pruefstand]}`); |
| 111 | } |
| 112 | |
| 113 | describe('Befund 7 - Spalte "Angesetzt" der Schrankentabelle', () => { |
| 114 | it('druckt die Umlaufzeitschranken dieses Plans, nicht die der Anlagenart', () => { |
| 115 | const { text, grenzen } = ausdruck({ cycleTimeMin: 45, cycleTimeMax: 100 }); |
| 116 | |
| 117 | expect(grenzen.cycleTime.min, 'Vorbedingung: die Vorgabe wirkt').toBe(45); |
| 118 | expect(grenzen.cycleTime.max, 'Vorbedingung: die Vorgabe wirkt').toBe(100); |
| 119 | |
| 120 | const angesetzt = spanne(grenzen, 'zulaessig'); |
| 121 | const regelwerk = spanne(REGELWERK, 'zulaessig'); |
| 122 | |
| 123 | expect(text, `die Spalte "Angesetzt" nennt ${angesetzt}`).toContain(gedruckt(angesetzt)); |
| 124 | expect(text, `nicht die Werte der Anlagenart (${regelwerk})`).not.toContain( |
| 125 | gedruckt(`Umlaufzeit, zulässig ${regelwerk}`), |
| 126 | ); |
| 127 | // Die eingeengte Zeile steht nicht ohne Kennzeichnung neben "gesichert". |
| 128 | expect(text, 'Kennwert samt Vermerk, dann Wert, Grundlage und Pruefstand').toContain( |
| 129 | zeile('Umlaufzeit, zulässig', grenzen, 'zulaessig'), |
| 130 | ); |
| 131 | }); |
| 132 | |
| 133 | it('druckt den empfohlenen Bereich dieses Plans, auch wenn nur die Obergrenze vorgegeben ist', () => { |
| 134 | // Schon eine eingetragene Obergrenze allein verschiebt den Regelbereich |
| 135 | // ueber clamp() (signalPlan.ts): gemessen 60 bis 60 s bei gedruckten |
| 136 | // 60 bis 90 s. |
| 137 | const { text, grenzen } = ausdruck({ cycleTimeMax: 60 }); |
| 138 | |
| 139 | expect(grenzen.cycleTime.recommendedMax, 'Vorbedingung: der Regelbereich wird eingeengt').toBe( |
| 140 | 60, |
| 141 | ); |
| 142 | |
| 143 | expect(text).toContain(gedruckt('Umlaufzeit, empfohlener Bereich (eigene Vorgabe')); |
| 144 | // Beide Zeilen sind hier verschoben: die zulaessige Obergrenze durch die |
| 145 | // Vorgabe selbst, der Regelbereich durch clamp() - und beide tragen den |
| 146 | // Vermerk vor Wert, Grundlage und Pruefstand. |
| 147 | expect(text).toContain(zeile('Umlaufzeit, empfohlener Bereich', grenzen, 'regelbereich')); |
| 148 | expect(text).toContain(zeile('Umlaufzeit, zulässig', grenzen, 'zulaessig')); |
| 149 | }); |
| 150 | |
| 151 | it('vermerkt keine Abweichung, wo keine ist, und sagt es im Einleitungsabsatz', () => { |
| 152 | const { text, grenzen } = ausdruck({}); |
| 153 | |
| 154 | expect(grenzen.cycleTime, 'Vorbedingung: ohne Vorgabe gelten die Werte der Anlagenart').toEqual( |
| 155 | REGELWERK.cycleTime, |
| 156 | ); |
| 157 | expect(text, 'ohne eigene Vorgabe steht kein Vermerk in der Tabelle').not.toContain( |
| 158 | gedruckt('(eigene Vorgabe, abweichend vom Regelwert'), |
| 159 | ); |
| 160 | expect(text).toContain(zeile('Umlaufzeit, zulässig', grenzen, 'zulaessig')); |
| 161 | expect( |
| 162 | text, |
| 163 | 'der Einleitungsabsatz sagt, wofuer Grundlage und Pruefstand bei eigener Vorgabe stehen', |
| 164 | ).toContain( |
| 165 | gedruckt( |
| 166 | 'Steht unter "Angesetzt" eine eigene Vorgabe aus der Vorgabenverwaltung, ist das an der ' + |
| 167 | 'Zeile vermerkt: Grundlage und Prüfstand nennen dann weiterhin den Regelwert, von dem ' + |
| 168 | 'die Vorgabe abweicht, und nicht die Herkunft des angesetzten Wertes.', |
| 169 | ), |
| 170 | ); |
| 171 | }); |
| 172 | |
| 173 | it('nennt die Sicherheitsgrenze der RiLSA weiter mit ihrem Regelwert und die eigene Vorgabe daneben', () => { |
| 174 | const { text } = ausdruck({ cycleTimeMax: 100 }); |
| 175 | const regelwert = fmt.seconds(ANLAGENART_GRENZEN.knotenpunkt.cycleTime.max); |
| 176 | |
| 177 | expect( |
| 178 | text, |
| 179 | 'die Begruendung "Sicherheitsgrenze" gehoert zum Regelwerkswert, nicht zur eigenen Vorgabe', |
| 180 | ).toContain( |
| 181 | gedruckt(`Die Obergrenze der Umlaufzeit von ${regelwert} ist eine Sicherheitsgrenze`), |
| 182 | ); |
| 183 | expect(text, 'die engere eigene Vorgabe wird als solche benannt').toContain( |
| 184 | gedruckt(`Für diesen Plan ist eine engere Obergrenze von ${fmt.seconds(100)} vorgegeben`), |
| 185 | ); |
| 186 | }); |
| 187 | |
| 188 | it('laesst den Satz ohne eigene Vorgabe unveraendert', () => { |
| 189 | const { text } = ausdruck({}); |
| 190 | expect(text).toContain( |
| 191 | gedruckt( |
| 192 | `Die Obergrenze der Umlaufzeit von ${fmt.seconds(ANLAGENART_GRENZEN.knotenpunkt.cycleTime.max)} ` + |
| 193 | 'ist eine Sicherheitsgrenze', |
| 194 | ), |
| 195 | ); |
| 196 | expect(text).not.toContain(gedruckt('ist eine engere Obergrenze von')); |
| 197 | }); |
| 198 | }); |