lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests domain fahrbeziehungOhneNachweis.test.ts
| 1 | import { readFileSync } from 'node:fs'; |
| 2 | import { describe, expect, it } from 'vitest'; |
| 3 | import { leiteAb, uebernimmWege, wegSchluessel } from '@/domain/geometrie/ableitung'; |
| 4 | import { createStandardIntersectionProject } from '@/domain/model/factory'; |
| 5 | import { parseProject } from '@/domain/model/schema'; |
| 6 | import type { Conflict, Project } from '@/domain/model/project'; |
| 7 | import { buildSignalPlan, intergreenKey } from '@/domain/plan/signalPlan'; |
| 8 | import { RILSA_DEFAULTS } from '@/domain/rilsa/constants'; |
| 9 | import { validateProject } from '@/domain/validation/engine'; |
| 10 | import { raeumbeziehungBefunde } from '@/domain/validation/rules'; |
| 11 | import type { ValidationFinding } from '@/domain/validation/types'; |
| 12 | |
| 13 | /* |
| 14 | * Befund 4, behoben in Fassung 5.11.0 - der Widerspruch zwischen der |
| 15 | * Fahrbeziehung einer Signalgruppe und den Stroemen, die sie im Lageplan |
| 16 | * fuehrt, wurde von keiner Regel gemeldet. |
| 17 | * |
| 18 | * Er steht bis heute in der mitgelieferten `beispiele/vierarmiger- |
| 19 | * knotenpunkt.lsap`: "K Nord" ist als 'geradeaus' erfasst und fuehrt im |
| 20 | * Lageplan die Linien "Nord geradeaus" UND "Nord rechts". Die Beziehung |
| 21 | * sg-k-nord -> sg-k-ost traegt den Raeumweg des Rechtsabbiegers (19,52 m) mit |
| 22 | * dem Vermerk "(6 s)"; gerechnet wird sie mit der Fahrbeziehung der Gruppe und |
| 23 | * damit mit 10,0 m/s - 5 s. Zu kurz ist die gefaehrliche Richtung. |
| 24 | * |
| 25 | * Die Datei ist zugleich Prueffstueck fuer den Altdatei-Pfad und wird deshalb |
| 26 | * NICHT geaendert. Der Bericht muss den Zustand aber benennen. |
| 27 | */ |
| 28 | |
| 29 | const DATEI = 'beispiele/vierarmiger-knotenpunkt.lsap'; |
| 30 | const DATUM = new Date('2026-01-01T00:00:00Z'); |
| 31 | const REGEL = 'zwischenzeiten.raeumbeziehung-nicht-nachgewiesen'; |
| 32 | |
| 33 | /** Die mitgelieferte Altdatei, eingelesen wie beim Oeffnen. */ |
| 34 | function beispiel(): Project { |
| 35 | const ergebnis = parseProject(JSON.parse(readFileSync(DATEI, 'utf8')), DATUM); |
| 36 | expect(ergebnis.issues.filter((i) => i.severity === 'fehler')).toEqual([]); |
| 37 | return ergebnis.project; |
| 38 | } |
| 39 | |
| 40 | /** Dieselbe Datei nach "Ausgewählte übernehmen" ueber alle Beziehungen. */ |
| 41 | function nachUebernahme(basis: Project = beispiel()): Project { |
| 42 | const ableitung = leiteAb(basis, basis.lageplan); |
| 43 | return uebernimmWege(basis, basis.lageplan, ableitung, ableitung.wege.map(wegSchluessel), DATUM) |
| 44 | .project; |
| 45 | } |
| 46 | |
| 47 | function befunde(project: Project): ValidationFinding[] { |
| 48 | return validateProject(project, buildSignalPlan(project), DATUM).findings.filter( |
| 49 | (f) => f.rule === REGEL, |
| 50 | ); |
| 51 | } |
| 52 | |
| 53 | describe('N1/5: Der Bericht meldet die nicht nachgewiesene Fahrbeziehung', () => { |
| 54 | it('trifft die vier Sammelsignalgruppen der Beispieldatei', () => { |
| 55 | const gemeldet = befunde(beispiel()); |
| 56 | |
| 57 | expect(gemeldet.map((f) => (f.target?.kind === 'signalgruppe' ? f.target.id : ''))).toEqual([ |
| 58 | 'sg-k-nord', |
| 59 | 'sg-k-ost', |
| 60 | 'sg-k-sued', |
| 61 | 'sg-k-west', |
| 62 | ]); |
| 63 | // Zu kurz gerechnet ist die gefaehrliche Richtung: Der Rechtsabbieger |
| 64 | // raeumt mit 7,0 m/s, gerechnet wird mit 10,0 m/s. |
| 65 | expect(gemeldet.every((f) => f.severity === 'warnung')).toBe(true); |
| 66 | expect(gemeldet.every((f) => f.category === 'zwischenzeiten')).toBe(true); |
| 67 | }); |
| 68 | |
| 69 | it('nennt beide Fahrbeziehungen, die Zahl der Beziehungen und die Richtung', () => { |
| 70 | const befund = befunde(beispiel())[0]!; |
| 71 | |
| 72 | expect(befund.title).toContain('K Nord'); |
| 73 | expect(befund.message).toContain('rechts abbiegend'); |
| 74 | expect(befund.message).toContain('geradeaus'); |
| 75 | // Alle sieben Beziehungen der Gruppe sind betroffen - keine traegt die |
| 76 | // Fahrbeziehung ihres massgebenden Stroms. |
| 77 | expect(befund.message).toContain('7 von 7'); |
| 78 | expect(befund.message).toContain('zu kurz'); |
| 79 | // Eine Regel traegt Ueberschrift und naechsten Schritt selbst; sie faellt |
| 80 | // nicht auf den Sammeltitel der Plan-Notizen zurueck. |
| 81 | expect(befund.title).not.toBe('Hinweis aus dem Planaufbau'); |
| 82 | expect(befund.suggestion).not.toBe('Prüfen Sie die zugrunde liegenden Eingaben.'); |
| 83 | expect(befund.suggestion).toContain('Lageplan'); |
| 84 | expect(befund.reference).not.toBe(''); |
| 85 | }); |
| 86 | |
| 87 | it('schweigt nach einer Uebernahme aus dem Lageplan', () => { |
| 88 | // Dort steht das Feld, wo es gebraucht wird - und wo es fehlt, ist sein |
| 89 | // Fehlen die Entscheidung derselben Uebernahme. |
| 90 | const project = nachUebernahme(); |
| 91 | // Zwoelf seit 5.24.0; bis dahin acht - siehe massgebenderStrom. |
| 92 | expect(project.conflicts.filter((c) => 'raeumbeziehung' in c)).toHaveLength(12); |
| 93 | expect(befunde(project)).toEqual([]); |
| 94 | }); |
| 95 | |
| 96 | it('schweigt ohne Lageplan', () => { |
| 97 | // Der Standardknotenpunkt fuehrt keine Planlinien; ueber die Stroeme |
| 98 | // seiner Signalgruppen ist nichts bekannt, und eine Meldung waere eine |
| 99 | // Behauptung ins Blaue. |
| 100 | expect(befunde(createStandardIntersectionProject('Ohne Lageplan', DATUM))).toEqual([]); |
| 101 | }); |
| 102 | |
| 103 | it('bleibt ein Hinweis, wo die Zwischenzeit dadurch zu lang ausfaellt', () => { |
| 104 | /* |
| 105 | * Spiegelfall: Traegt die Sammelgruppe 'rechts', so raeumt der Plan ihre |
| 106 | * Beziehungen mit 7,0 m/s, waehrend der gebuendelte Geradeausstrom 10,0 |
| 107 | * m/s faehrt. Die Zwischenzeit faellt dann zu LANG aus - zu klaeren ist |
| 108 | * das auch, gefaehrlich ist es nicht. |
| 109 | */ |
| 110 | const basis = beispiel(); |
| 111 | const gedreht: Project = { |
| 112 | ...basis, |
| 113 | signalGroups: basis.signalGroups.map((g) => |
| 114 | g.id === 'sg-k-nord' ? { ...g, movement: 'rechts' as const } : g, |
| 115 | ), |
| 116 | }; |
| 117 | const nord = befunde(gedreht).find( |
| 118 | (f) => f.target?.kind === 'signalgruppe' && f.target.id === 'sg-k-nord', |
| 119 | ); |
| 120 | |
| 121 | expect(nord?.severity).toBe('hinweis'); |
| 122 | expect(nord?.message).toContain('zu lang'); |
| 123 | // Die uebrigen drei bleiben bei der Warnung. |
| 124 | expect(befunde(gedreht).filter((f) => f.severity === 'warnung')).toHaveLength(3); |
| 125 | }); |
| 126 | |
| 127 | it('sagt den Rueckfall mit dem Wortlaut, den auch die Planunterlage fuehrt', () => { |
| 128 | // Beide Auskunftsstellen beschreiben denselben Sachverhalt; der |
| 129 | // Erlaeuterungsabsatz der Unterlage nennt ihn seit Fassung 5.11.0 ebenfalls. |
| 130 | expect(befunde(beispiel())[0]?.message).toContain('mit der Fahrbeziehung der Signalgruppe'); |
| 131 | }); |
| 132 | |
| 133 | it('zaehlt eine von Hand vorgegebene Zwischenzeit nicht mit', () => { |
| 134 | // Wo die Zwischenzeit von Hand steht, geht keine Fahrbeziehung in eine |
| 135 | // Zahl ein - dort ist nichts zu klaeren. |
| 136 | const basis = beispiel(); |
| 137 | const project: Project = { |
| 138 | ...basis, |
| 139 | conflicts: basis.conflicts.map((c) => |
| 140 | c.fromId === 'sg-k-nord' && c.toId === 'sg-k-ost' ? { ...c, manualIntergreen: 9 } : c, |
| 141 | ), |
| 142 | }; |
| 143 | const nord = befunde(project).find( |
| 144 | (f) => f.target?.kind === 'signalgruppe' && f.target.id === 'sg-k-nord', |
| 145 | ); |
| 146 | |
| 147 | expect(nord?.message).toContain('6 von 7'); |
| 148 | }); |
| 149 | }); |
| 150 | |
| 151 | /* |
| 152 | * NACHTRAG (Fassung 5.11.0): Die Regel loeste die Raeumgeschwindigkeit selbst |
| 153 | * auf (`clearingSpeedOverride ?? clearingSpeedFor`) und liess damit JEDE |
| 154 | * Handeingabe als massgeblich gelten - auch eine, die der Fachkern verwirft. |
| 155 | * Sie haengt nicht an der Fahrbeziehung; der Regelwert, auf den zurueckgefallen |
| 156 | * wird, haengt sehr wohl an ihr. Wo die Regel schwieg, raeumte der Plan weiter |
| 157 | * mit 10,0 m/s als Gruppe und mit 7,0 m/s als Abbieger. Schweigen ist die |
| 158 | * gefaehrliche Richtung. |
| 159 | */ |
| 160 | describe('N1/6: Die Regel prueft die Handeingabe wie der Fachkern', () => { |
| 161 | /** Die Beispieldatei mit einem Zusatz an allen sieben Beziehungen von "K Nord". */ |
| 162 | function mitZusatz(zusatz: Partial<Conflict>, basis: Project = beispiel()): Project { |
| 163 | return { |
| 164 | ...basis, |
| 165 | conflicts: basis.conflicts.map((c) => (c.fromId === 'sg-k-nord' ? { ...c, ...zusatz } : c)), |
| 166 | }; |
| 167 | } |
| 168 | |
| 169 | function nordBefund(project: Project): ValidationFinding | undefined { |
| 170 | return befunde(project).find( |
| 171 | (f) => f.target?.kind === 'signalgruppe' && f.target.id === 'sg-k-nord', |
| 172 | ); |
| 173 | } |
| 174 | |
| 175 | it('meldet weiter, wo der Fachkern die eingetragene Raeumgeschwindigkeit verwirft', () => { |
| 176 | const project = mitZusatz({ clearingSpeedOverride: 0 }); |
| 177 | |
| 178 | // Der Plan setzt die 0 nicht an: Er faellt auf den Regelwert der |
| 179 | // Fahrbeziehung zurueck und raeumt die Gruppe mit 10,0 m/s. |
| 180 | expect( |
| 181 | buildSignalPlan(project).intergreens.get(intergreenKey('sg-k-nord', 'sg-k-ost'))?.calculation |
| 182 | ?.clearingSpeed, |
| 183 | ).toBe(RILSA_DEFAULTS.clearingSpeed.kfzStraight); |
| 184 | |
| 185 | const nord = nordBefund(project); |
| 186 | expect(nord?.severity).toBe('warnung'); |
| 187 | expect(nord?.message).toContain('7 von 7'); |
| 188 | }); |
| 189 | |
| 190 | it('schweigt, wo eine gueltige Handeingabe die Fahrbeziehung gegenstandslos macht', () => { |
| 191 | // Gegenprobe zum Fall darueber: 8,0 m/s setzt der Fachkern an, und zwar |
| 192 | // fuer jede Fahrbeziehung dieselben. Dann ist nichts zu klaeren. |
| 193 | expect(nordBefund(mitZusatz({ clearingSpeedOverride: 8 }))).toBeUndefined(); |
| 194 | }); |
| 195 | |
| 196 | it('misst am Regelwert und damit an der harten Obergrenze der Verkehrsart', () => { |
| 197 | /* |
| 198 | * Ueber der Obergrenze von 10,0 m/s rechnet der Plan geradeaus wie |
| 199 | * abbiegend mit dem Deckel - die Fahrbeziehung faellt aus der Rechnung. |
| 200 | * `clearingSpeedFor` kennt ihn nicht und meldete hier 14,0 gegen 12,0 m/s: |
| 201 | * eine Warnung "zu kurz gerechnet" ueber zwei Zahlen, die im Plan |
| 202 | * dieselben sind. |
| 203 | */ |
| 204 | const defaults = { |
| 205 | ...RILSA_DEFAULTS, |
| 206 | clearingSpeed: { ...RILSA_DEFAULTS.clearingSpeed, kfzStraight: 14, kfzTurning: 12 }, |
| 207 | }; |
| 208 | |
| 209 | expect(raeumbeziehungBefunde(beispiel(), defaults)).toEqual([]); |
| 210 | // Ohne die Kappung waeren es die vier Sammelsignalgruppen - der Fall |
| 211 | // prueft nur etwas, solange die Regel ohne Kappung anschlagen wuerde. |
| 212 | expect(befunde(beispiel())).toHaveLength(4); |
| 213 | }); |
| 214 | |
| 215 | it('meldet weiter, wo der Fachkern die eingetragene Ueberfahrzeit verwirft', () => { |
| 216 | /* |
| 217 | * Beim Ansatz "feste Ueberfahrzeit" haengt auch die Ueberfahrzeit an der |
| 218 | * Fahrbeziehung (geradeaus laenger als abbiegend). Die eingetragene |
| 219 | * Raeumgeschwindigkeit nimmt vr aus dem Vergleich; bleibt eine |
| 220 | * Ueberfahrzeit von -1 s, die der Fachkern verwirft. Ungeprueft |
| 221 | * uebernommen sah die Regel zwei gleiche Werte und schwieg. |
| 222 | */ |
| 223 | const basis = beispiel(); |
| 224 | const festerAnsatz: Project = { |
| 225 | ...basis, |
| 226 | settings: { ...basis.settings, ueberfahrzeitAnsatz: 'fest' }, |
| 227 | }; |
| 228 | const project = mitZusatz({ clearingSpeedOverride: 8, crossingTimeOverride: -1 }, festerAnsatz); |
| 229 | |
| 230 | const nord = nordBefund(project); |
| 231 | expect(nord?.message).toContain('7 von 7'); |
| 232 | // Der abbiegende Strom ueberfaehrt kuerzer: gerechnet wird zu LANG. |
| 233 | expect(nord?.severity).toBe('hinweis'); |
| 234 | }); |
| 235 | |
| 236 | it('behaelt den Regelwert als Untergrenze der Kfz-Ueberfahrzeit', () => { |
| 237 | /* |
| 238 | * 2,5 s liegen zwischen den beiden festen Werten. Der Fachkern setzt sie |
| 239 | * beim Abbieger an (2 s Regelwert) und verwirft sie geradeaus (3 s |
| 240 | * Regelwert) - die Fahrbeziehung entscheidet also weiterhin, obwohl an der |
| 241 | * Beziehung eine gueltige Ueberfahrzeit steht. |
| 242 | */ |
| 243 | const basis = beispiel(); |
| 244 | const festerAnsatz: Project = { |
| 245 | ...basis, |
| 246 | settings: { ...basis.settings, ueberfahrzeitAnsatz: 'fest' }, |
| 247 | }; |
| 248 | const project = mitZusatz( |
| 249 | { clearingSpeedOverride: 8, crossingTimeOverride: 2.5 }, |
| 250 | festerAnsatz, |
| 251 | ); |
| 252 | |
| 253 | // Was der Plan an der Gruppe rechnet: die 3 s des Geradeausstroms. |
| 254 | expect( |
| 255 | buildSignalPlan(project).intergreens.get(intergreenKey('sg-k-nord', 'sg-k-ost'))?.calculation |
| 256 | ?.crossingTime, |
| 257 | ).toBe(RILSA_DEFAULTS.crossingTime.kfzGeradeaus); |
| 258 | expect(nordBefund(project)?.message).toContain('7 von 7'); |
| 259 | }); |
| 260 | }); |