lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests export csvZwischenzeitenmatrix.test.ts
| 1 | import { describe, expect, it } from 'vitest'; |
| 2 | import { intergreenCsv } from '@/services/export/csv'; |
| 3 | import { buildSignalPlan } from '@/domain/plan/signalPlan'; |
| 4 | import { createStandardIntersectionProject } from '@/domain/model/factory'; |
| 5 | import { intergreenKey } from '@/domain/plan/signalPlan'; |
| 6 | import type { Project } from '@/domain/model/project'; |
| 7 | |
| 8 | /** |
| 9 | * Die Zwischenzeitenmatrix als Tabellenausgabe fuehrt denselben Nachweis wie |
| 10 | * der Ausdruck. |
| 11 | * |
| 12 | * BEFUND (Fassung 5.8.0): Die Kreuztabelle verlor zwei Unterscheidungen, die |
| 13 | * `drawIntergreenMatrix` macht. |
| 14 | * |
| 15 | * 1. Eine von Hand vorgegebene Zwischenzeit traegt im Ausdruck einen Stern. |
| 16 | * Hier stand dieselbe Zahl ohne Kennzeichnung - wer die Datei auswertete, |
| 17 | * sah nicht, welche Werte die Berechnung ersetzt hatten. |
| 18 | * 2. Die leere Zelle stand fuer ZWEI Sachverhalte: die Diagonale und ein |
| 19 | * vertraegliches Paar. Im Ausdruck ist das erste leer, das zweite ein |
| 20 | * Punkt. |
| 21 | * |
| 22 | * Das wiegt schwer, weil diese Ausgabe die maschinell verwertbare Fassung |
| 23 | * derselben Tabelle ist, und weil die Oberflaeche Matrix und Rechenweg als |
| 24 | * getrennte Schaltflaechen anbietet: Wer nur die Matrix laedt, haette den |
| 25 | * Nachweis sonst nirgends. Sie ist zugleich die einzige maschinell lesbare |
| 26 | * Quelle dafuer, WELCHE Paare vertraeglich sind - der Rechenweg fuehrt nur die |
| 27 | * erfassten Beziehungen. |
| 28 | * |
| 29 | * GEPRUEFT WIRD BEIDES: dass der Nachweis da ist UND dass die Wertetabelle |
| 30 | * darueber rein numerisch bleibt. Ein "7*" waere in einer Tabellenkalkulation |
| 31 | * keine Zahl mehr, und in einer Spalte mit gemischten Zellen schluegen Summe, |
| 32 | * Hoechstwert und Vergleich fehl. |
| 33 | */ |
| 34 | |
| 35 | const STICHTAG = new Date('2026-01-01T00:00:00Z'); |
| 36 | |
| 37 | /** Die Ausgabe ohne Steuerzeichen, in Zeilen und Felder zerlegt. */ |
| 38 | function tabelle(csv: string): string[][] { |
| 39 | return csv |
| 40 | .replace(/^/, '') |
| 41 | .split('\r\n') |
| 42 | .map((zeile) => zeile.split(';')); |
| 43 | } |
| 44 | |
| 45 | /** Der Block ab der Zeile, die mit `titel` beginnt - bis zur naechsten Leerzeile. */ |
| 46 | function block(zeilen: string[][], kopfIndex: number): string[][] { |
| 47 | const raus: string[][] = []; |
| 48 | for (let i = kopfIndex; i < zeilen.length; i += 1) { |
| 49 | const z = zeilen[i]!; |
| 50 | if (z.length <= 1 && (z[0] ?? '') === '') break; |
| 51 | if (z.length <= 1) break; |
| 52 | raus.push(z); |
| 53 | } |
| 54 | return raus; |
| 55 | } |
| 56 | |
| 57 | function bloecke(csv: string): { werte: string[][]; herkunft: string[][]; alle: string[][] } { |
| 58 | const zeilen = tabelle(csv); |
| 59 | const titel = zeilen.findIndex((z) => (z[0] ?? '').startsWith('Herkunft der Zwischenzeit')); |
| 60 | expect(titel, 'Titelzeile des Herkunftsblocks').toBeGreaterThan(0); |
| 61 | return { werte: block(zeilen, 0), herkunft: block(zeilen, titel + 1), alle: zeilen }; |
| 62 | } |
| 63 | |
| 64 | /** Standardknotenpunkt; `vorgabe` setzt an der ersten Konfliktbeziehung eine feste Zwischenzeit. */ |
| 65 | function projekt(vorgabe: number | null = null): Project { |
| 66 | const p = createStandardIntersectionProject('Prüfstück', STICHTAG); |
| 67 | if (vorgabe === null) return p; |
| 68 | const [erster, ...rest] = p.conflicts; |
| 69 | expect(erster, 'Vorbedingung: die Vorlage hat eine Konfliktbeziehung').toBeDefined(); |
| 70 | return { ...p, conflicts: [{ ...erster!, manualIntergreen: vorgabe }, ...rest] }; |
| 71 | } |
| 72 | |
| 73 | describe('Zwischenzeitenmatrix als Tabellenausgabe', () => { |
| 74 | it('weist eine von Hand vorgegebene Zwischenzeit als solche aus', () => { |
| 75 | const p = projekt(9); |
| 76 | const plan = buildSignalPlan(p); |
| 77 | const konflikt = p.conflicts[0]!; |
| 78 | const gruppen = p.signalGroups; |
| 79 | const zeile = gruppen.findIndex((g) => g.id === konflikt.fromId); |
| 80 | const spalte = gruppen.findIndex((g) => g.id === konflikt.toId); |
| 81 | |
| 82 | const { werte, herkunft } = bloecke(intergreenCsv(p, plan)); |
| 83 | |
| 84 | // Der Wert steht in der Wertetabelle - als blosse Zahl, ohne Stern. |
| 85 | expect(werte[zeile + 1]![spalte + 1]).toBe('9'); |
| 86 | // Und die Herkunft steht an derselben Stelle des zweiten Blocks. |
| 87 | expect(herkunft[zeile + 1]![spalte + 1]).toBe('vorgegeben'); |
| 88 | |
| 89 | /* |
| 90 | * Gegenprobe: ohne Vorgabe ist dieselbe Zelle berechnet. Das Projekt wird |
| 91 | * EINMAL erzeugt und fuer Plan und Ausgabe wiederverwendet - die Vorlage |
| 92 | * vergibt je Aufruf neue Kennungen, und ein Plan aus einem zweiten Aufruf |
| 93 | * fuende keine einzige Zwischenzeit wieder. |
| 94 | */ |
| 95 | const ohneVorgabe = projekt(); |
| 96 | const ohne = bloecke(intergreenCsv(ohneVorgabe, buildSignalPlan(ohneVorgabe))); |
| 97 | expect(ohne.herkunft[zeile + 1]![spalte + 1]).toBe('berechnet'); |
| 98 | }); |
| 99 | |
| 100 | it('unterscheidet die Diagonale von einem vertraeglichen Paar', () => { |
| 101 | const p = projekt(); |
| 102 | const plan = buildSignalPlan(p); |
| 103 | const gruppen = p.signalGroups; |
| 104 | const { werte, herkunft } = bloecke(intergreenCsv(p, plan)); |
| 105 | |
| 106 | // Ein vertraegliches Paar suchen: keine Zwischenzeit hinterlegt, i != j. |
| 107 | let paar: [number, number] | null = null; |
| 108 | for (let i = 0; i < gruppen.length && !paar; i += 1) { |
| 109 | for (let j = 0; j < gruppen.length; j += 1) { |
| 110 | if (i === j) continue; |
| 111 | if (!plan.intergreens.get(intergreenKey(gruppen[i]!.id, gruppen[j]!.id))) { |
| 112 | paar = [i, j]; |
| 113 | break; |
| 114 | } |
| 115 | } |
| 116 | } |
| 117 | expect(paar, 'Vorbedingung: die Vorlage hat ein vertraegliches Paar').not.toBeNull(); |
| 118 | const [vi, vj] = paar!; |
| 119 | |
| 120 | // In der Wertetabelle sehen beide gleich aus - dort steht keine Zahl. |
| 121 | expect(werte[vi + 1]![vj + 1]).toBe(''); |
| 122 | expect(werte[vi + 1]![vi + 1]).toBe(''); |
| 123 | |
| 124 | // Die Herkunft trennt sie: benannt gegen leer. |
| 125 | expect(herkunft[vi + 1]![vj + 1]).toBe('verträglich'); |
| 126 | expect(herkunft[vi + 1]![vi + 1]).toBe(''); |
| 127 | }); |
| 128 | |
| 129 | it('haelt die Wertetabelle rein numerisch', () => { |
| 130 | const p = projekt(9); |
| 131 | const { werte } = bloecke(intergreenCsv(p, buildSignalPlan(p))); |
| 132 | for (let z = 1; z < werte.length; z += 1) { |
| 133 | for (let s = 1; s < werte[z]!.length; s += 1) { |
| 134 | const zelle = werte[z]![s]!; |
| 135 | if (zelle === '') continue; |
| 136 | expect(zelle, `Zelle ${z}/${s} ist keine Zahl`).toMatch(/^\d+(,\d+)?$/); |
| 137 | } |
| 138 | } |
| 139 | }); |
| 140 | |
| 141 | it('legt beide Bloecke Zelle auf Zelle - gleicher Kopf, gleiche Zeilenfolge', () => { |
| 142 | const p = projekt(); |
| 143 | const { werte, herkunft } = bloecke(intergreenCsv(p, buildSignalPlan(p))); |
| 144 | expect(herkunft[0]).toEqual(werte[0]); |
| 145 | expect(herkunft.length).toBe(werte.length); |
| 146 | for (let z = 1; z < werte.length; z += 1) { |
| 147 | expect(herkunft[z]![0], `Zeilenschluessel ${z}`).toBe(werte[z]![0]); |
| 148 | expect(herkunft[z]!.length).toBe(werte[z]!.length); |
| 149 | } |
| 150 | }); |
| 151 | |
| 152 | it('nennt fuer jedes Paar genau einen der drei Sachverhalte', () => { |
| 153 | const p = projekt(9); |
| 154 | const { herkunft } = bloecke(intergreenCsv(p, buildSignalPlan(p))); |
| 155 | const zulaessig = new Set(['berechnet', 'vorgegeben', 'verträglich']); |
| 156 | let gezaehlt = 0; |
| 157 | for (let z = 1; z < herkunft.length; z += 1) { |
| 158 | for (let s = 1; s < herkunft[z]!.length; s += 1) { |
| 159 | const zelle = herkunft[z]![s]!; |
| 160 | if (z === s) { |
| 161 | expect(zelle, 'Diagonale').toBe(''); |
| 162 | continue; |
| 163 | } |
| 164 | expect(zulaessig.has(zelle), `Zelle ${z}/${s}: "${zelle}"`).toBe(true); |
| 165 | gezaehlt += 1; |
| 166 | } |
| 167 | } |
| 168 | const n = p.signalGroups.length; |
| 169 | expect(gezaehlt, 'jedes geordnete Paar genau einmal').toBe(n * (n - 1)); |
| 170 | }); |
| 171 | |
| 172 | it('laesst die Vermerke die letzten Zeilen bleiben (Befund C7)', () => { |
| 173 | const p = projekt(); |
| 174 | const plan = buildSignalPlan(p); |
| 175 | const zeilen = tabelle( |
| 176 | intergreenCsv(p, plan, { errorCount: 0, warningCount: 2, hintCount: 0 } as never), |
| 177 | ); |
| 178 | const letzte = zeilen[zeilen.length - 1]!.join(';'); |
| 179 | expect(letzte).toContain('Hinweis:'); |
| 180 | // Und der Herkunftsblock steht davor, nicht dahinter. |
| 181 | const titel = zeilen.findIndex((z) => (z[0] ?? '').startsWith('Herkunft der Zwischenzeit')); |
| 182 | expect(titel).toBeLessThan(zeilen.length - 1); |
| 183 | }); |
| 184 | }); |