lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests export massgebenderStromUnterlage.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 { |
| 6 | buildSignalPlan, |
| 7 | intergreenKey, |
| 8 | massgebendeRaeumbeziehung, |
| 9 | } from '@/domain/plan/signalPlan'; |
| 10 | import { validateProject } from '@/domain/validation'; |
| 11 | import { raeumbeziehungBefunde } from '@/domain/validation/rules'; |
| 12 | import { createStandardIntersectionProject } from '@/domain/model/factory'; |
| 13 | import { parseProject } from '@/domain/model/schema'; |
| 14 | import { RILSA_DEFAULTS } from '@/domain/rilsa/constants'; |
| 15 | import { toWinAnsi } from '@/render/pdfSurface'; |
| 16 | import type { Conflict, Project } from '@/domain/model/project'; |
| 17 | import * as fmt from '@/ui/format'; |
| 18 | |
| 19 | /* |
| 20 | * Der massgebende Strom in der PLANUNTERLAGE. |
| 21 | * |
| 22 | * Verlangt war ausdruecklich dieser Fall: EINE Beziehung, an der Plan, |
| 23 | * Pruefbericht und die gedruckte Zeile gegeneinander gehalten werden. Bis zu |
| 24 | * diesem Befund liefen sie auseinander - der Plan raeumte mit der Fahrbeziehung |
| 25 | * des massgebenden STROMS, die Aufstellung "Abweichungen von den Regelwerten" |
| 26 | * zaehlte mit der Fahrbeziehung der SIGNALGRUPPE. Gedruckt stand dann: das |
| 27 | * Merkmal "enger Innenradius" ist an einer Beziehung gesetzt und wirkt an |
| 28 | * null - waehrend derselbe Ausdruck zwei Seiten davor 5,0 m/s in die Spalte |
| 29 | * "vr" derselben Zeile schrieb. |
| 30 | */ |
| 31 | |
| 32 | const STICHTAG = new Date('2026-01-01T12:00:00Z'); |
| 33 | const BEISPIEL = 'beispiele/vierarmiger-knotenpunkt.lsap'; |
| 34 | |
| 35 | /** Sichtbaren Text aus dem PDF lesen; die Inhaltsstroeme sind komprimiert. */ |
| 36 | function pdfText(bytes: Uint8Array): string { |
| 37 | const roh = new TextDecoder('latin1').decode(bytes); |
| 38 | const teile: string[] = []; |
| 39 | const suche = /stream\r?\n/g; |
| 40 | let treffer: RegExpExecArray | null; |
| 41 | |
| 42 | while ((treffer = suche.exec(roh)) !== null) { |
| 43 | const start = treffer.index + treffer[0].length; |
| 44 | const ende = roh.indexOf('endstream', start); |
| 45 | if (ende < 0) continue; |
| 46 | let inhalt: string; |
| 47 | try { |
| 48 | inhalt = inflateSync(Buffer.from(roh.slice(start, ende), 'latin1')).toString('latin1'); |
| 49 | } catch { |
| 50 | continue; |
| 51 | } |
| 52 | for (const t of inhalt.matchAll(/\(((?:[^()\\]|\\.)*)\)\s*Tj/g)) { |
| 53 | teile.push(t[1]!.replace(/\\([()\\])/g, '$1')); |
| 54 | } |
| 55 | } |
| 56 | return teile.join(' ').replace(/\s+/g, ' '); |
| 57 | } |
| 58 | |
| 59 | const gedruckt = (text: string): string => toWinAnsi(text).replace(/\s+/g, ' '); |
| 60 | |
| 61 | /** |
| 62 | * Standardknotenpunkt, an dem "K1" geradeaus UND rechts fuehrt: Die Gruppe |
| 63 | * bleibt bei 'geradeaus', die Beziehung K1 -> K3 traegt die Fahrbeziehung |
| 64 | * ihres massgebenden Stroms und das Merkmal "enger Innenradius". |
| 65 | */ |
| 66 | function sammelgruppe(): Project { |
| 67 | const basis = createStandardIntersectionProject('Sammelgruppe', STICHTAG); |
| 68 | const k1 = basis.signalGroups.find((g) => g.name === 'K1')!; |
| 69 | const k3 = basis.signalGroups.find((g) => g.name === 'K3')!; |
| 70 | return { |
| 71 | ...basis, |
| 72 | meta: { ...basis.meta, planner: 'O. Willerding' }, |
| 73 | conflicts: basis.conflicts.map((c) => |
| 74 | c.fromId === k1.id && c.toId === k3.id |
| 75 | ? { ...c, raeumbeziehung: 'rechts' as const, engerRadius: true } |
| 76 | : c, |
| 77 | ), |
| 78 | }; |
| 79 | } |
| 80 | |
| 81 | function beziehung(project: Project): Conflict { |
| 82 | return project.conflicts.find((c) => c.engerRadius === true)!; |
| 83 | } |
| 84 | |
| 85 | function ausdruck(project: Project): string { |
| 86 | const plan = buildSignalPlan(project); |
| 87 | const bericht = validateProject(project, plan, STICHTAG); |
| 88 | return pdfText(buildProjectPdf(project, plan, bericht, DEFAULT_PDF_OPTIONS, STICHTAG)); |
| 89 | } |
| 90 | |
| 91 | describe('N1/3: Plan, Pruefbericht und gedruckte Zeile legen dieselbe Fahrbeziehung zugrunde', () => { |
| 92 | it('haelt die drei an derselben Beziehung gegeneinander', () => { |
| 93 | const project = sammelgruppe(); |
| 94 | const konflikt = beziehung(project); |
| 95 | const from = project.signalGroups.find((g) => g.id === konflikt.fromId)!; |
| 96 | const plan = buildSignalPlan(project); |
| 97 | const bericht = validateProject(project, plan, STICHTAG); |
| 98 | const text = pdfText(buildProjectPdf(project, plan, bericht, DEFAULT_PDF_OPTIONS, STICHTAG)); |
| 99 | |
| 100 | // Die eine Entscheidung, an die sich alle drei zu halten haben. |
| 101 | expect(from.movement).toBe('geradeaus'); |
| 102 | expect(massgebendeRaeumbeziehung(from, konflikt)).toBe('rechts'); |
| 103 | |
| 104 | // PLAN: raeumt als Rechtsabbieger mit engem Innenradius. |
| 105 | const gerechnet = plan.intergreens.get(intergreenKey(konflikt.fromId, konflikt.toId)); |
| 106 | expect(gerechnet?.calculation?.clearingSpeed).toBe( |
| 107 | RILSA_DEFAULTS.clearingSpeed.kfzTurningEngerRadius, |
| 108 | ); |
| 109 | |
| 110 | // PRUEFBERICHT: meldet das Merkmal als wirksam, nicht als wirkungslos. |
| 111 | const zurBeziehung = bericht.findings.filter( |
| 112 | (f) => f.target?.kind === 'konflikt' && f.target.id === konflikt.id, |
| 113 | ); |
| 114 | expect(zurBeziehung.map((f) => f.rule)).toContain('zwischenzeiten.enger-radius'); |
| 115 | expect(zurBeziehung.map((f) => f.rule)).not.toContain( |
| 116 | 'zwischenzeiten.enger-radius-ohne-wirkung', |
| 117 | ); |
| 118 | |
| 119 | // GEDRUCKTE ZEILE: dieselbe Raeumgeschwindigkeit in der Spalte "vr" ... |
| 120 | expect(text).toContain( |
| 121 | gedruckt(fmt.metersPerSecond(RILSA_DEFAULTS.clearingSpeed.kfzTurningEngerRadius, 1)), |
| 122 | ); |
| 123 | // ... und die Abweichungsaufstellung zaehlt die Beziehung als wirksam. |
| 124 | // 16 Beziehungen seit 5.43.0: Die Vorlage erfasst jede Furt auch gegen den |
| 125 | // Strom, der sie beim Ausfahren quert (K2/F2, K4/F1; Gruppe Furt). Bis |
| 126 | // dahin waren es 12. Gezaehlt wird weiter die eine wirksame Beziehung. |
| 127 | expect(text).toContain(gedruckt('an 1 von 16 Konfliktbeziehungen')); |
| 128 | expect(text).toContain(gedruckt('an den gekennzeichneten Beziehungen.')); |
| 129 | expect(text).not.toContain(gedruckt('an den 0 Beziehungen, an denen es wirkt')); |
| 130 | }); |
| 131 | |
| 132 | it('zaehlt weiter nur die Beziehungen, an denen das Merkmal wirkt', () => { |
| 133 | // Gegenprobe: dieselbe Gruppe ohne die Fahrbeziehung des massgebenden |
| 134 | // Stroms. Dann raeumt der Plan geradeaus, das Merkmal wirkt nicht - und |
| 135 | // die Aufstellung muss das sagen, sonst prueft der Fall oben nichts. |
| 136 | const basis = sammelgruppe(); |
| 137 | const project: Project = { |
| 138 | ...basis, |
| 139 | conflicts: basis.conflicts.map((c) => { |
| 140 | const { raeumbeziehung: _weg, ...ohne } = c; |
| 141 | return ohne; |
| 142 | }), |
| 143 | }; |
| 144 | const text = ausdruck(project); |
| 145 | |
| 146 | expect(text).toContain(gedruckt('an den 0 Beziehungen, an denen es wirkt')); |
| 147 | expect(text).not.toContain(gedruckt('an den gekennzeichneten Beziehungen.')); |
| 148 | }); |
| 149 | |
| 150 | it('sagt im Erlaeuterungsabsatz, welche Fahrbeziehung ueber das Merkmal entscheidet', () => { |
| 151 | /* |
| 152 | * Ohne diesen Satz steht in der Unterlage eine Zeile mit 5,0 m/s neben |
| 153 | * einer Signalgruppentabelle, die "geradeaus" fuehrt, und nichts erklaert |
| 154 | * den Unterschied. Der Absatz nennt deshalb den Strom, dem der Raeumweg |
| 155 | * dieser Zeile gehoert - nicht die Signalgruppe. |
| 156 | */ |
| 157 | const text = ausdruck(sammelgruppe()); |
| 158 | |
| 159 | expect(text).toContain( |
| 160 | gedruckt( |
| 161 | 'Ob ein Strom abbiegt, entscheidet dabei die Fahrbeziehung des Stroms, dem der Räumweg ' + |
| 162 | 'dieser Zeile gehört, und nicht die der räumenden Signalgruppe', |
| 163 | ), |
| 164 | ); |
| 165 | }); |
| 166 | |
| 167 | it('nennt im selben Absatz den Rueckfall auf die Fahrbeziehung der Signalgruppe', () => { |
| 168 | /* |
| 169 | * NACHTRAG (Fassung 5.11.0): Festgehalten wird der massgebende |
| 170 | * Strom allein bei der Uebernahme aus dem Lageplan. In der mitgelieferten |
| 171 | * Beispieldatei traegt keine der 72 Beziehungen das Feld; gedruckt stand |
| 172 | * trotzdem unbedingt, ueber das Abbiegen entscheide der Strom und nicht |
| 173 | * die Signalgruppe. Der Pruefer haelt die Zeile damit fuer nachgewiesen - |
| 174 | * die gefaehrliche Richtung des Irrtums. |
| 175 | */ |
| 176 | expect(ausdruck(sammelgruppe())).toContain( |
| 177 | gedruckt( |
| 178 | 'Ist nicht festgehalten, welcher Strom den Räumweg gefahren ist, wird mit der ' + |
| 179 | 'Fahrbeziehung der Signalgruppe gerechnet, und der Prüfbericht meldet das.', |
| 180 | ), |
| 181 | ); |
| 182 | }); |
| 183 | |
| 184 | it('sagt den Rueckfall mit dem Wortlaut des Pruefberichts', () => { |
| 185 | // Derselbe Sachverhalt, dieselben Worte: Der Bericht meldet ihn an der |
| 186 | // Beispieldatei, die Unterlage erklaert ihn im Absatz zur Spalte. |
| 187 | const gemeinsam = 'mit der Fahrbeziehung der Signalgruppe'; |
| 188 | const beispiel = parseProject(JSON.parse(readFileSync(BEISPIEL, 'utf8')), STICHTAG).project; |
| 189 | |
| 190 | expect(raeumbeziehungBefunde(beispiel)[0]?.message).toContain(gemeinsam); |
| 191 | expect(ausdruck(sammelgruppe())).toContain(gedruckt(gemeinsam)); |
| 192 | }); |
| 193 | }); |