lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests domain wegeschluessel.test.ts
| 1 | import { describe, expect, it } from 'vitest'; |
| 2 | import { leiteAb, uebernimmWege, wegSchluessel } from '@/domain/geometrie/ableitung'; |
| 3 | import type { Haltlinie, Lageplan, Planlinie } from '@/domain/geometrie/lageplan'; |
| 4 | import { REGELBREITE } from '@/domain/geometrie/vermessung'; |
| 5 | import { createEmptyProject } from '@/domain/model/factory'; |
| 6 | import { WEGNACHWEIS_HANDEINGABE, type Conflict, type Project } from '@/domain/model/project'; |
| 7 | import type { SignalGroup } from '@/domain/model/project'; |
| 8 | |
| 9 | /* |
| 10 | * Befund 43, behoben in Fassung 5.11.0. |
| 11 | * |
| 12 | * `wegSchluessel` klebte den Schluessel eines Wegevorschlags aus den beiden |
| 13 | * PLANLINIENKENNUNGEN zusammen: `${vonLinieId}->${nachLinieId}`. Dieselbe |
| 14 | * Bauart hat Befund 61 (Fassung 5.10.0) an drei Stellen beseitigt; die |
| 15 | * Folge dazu (schluesselAusKennungen.test.ts) sah dabei nur die |
| 16 | * Signalgruppenkennungen an. |
| 17 | * |
| 18 | * Planlinienkennungen sind noch schwaecher geprueft als Signalgruppenkennungen: |
| 19 | * `parseLageplan` vergibt eine doppelte zwar neu, prueft aber kein |
| 20 | * Zeichenrepertoire - eine Kennung mit "->" ueberlebt das Einlesen unveraendert. |
| 21 | * Damit fielen ("a->b", "c") und ("a", "b->c") auf denselben Schluessel |
| 22 | * "a->b->c". |
| 23 | * |
| 24 | * WAS DARAN SCHADET: `uebernimmWege` filtert mit |
| 25 | * `if (!schluessel.has(wegSchluessel(weg))) continue;`. Der Anwender hakt EIN |
| 26 | * Paar an, uebernommen werden ZWEI. Fuer das nicht angehakte Paar werden Raeum- |
| 27 | * und Einfahrweg ueberschrieben - ein von Hand eingetragener, bewusst laengerer |
| 28 | * Raeumweg wird durch den gemessenen ersetzt, und die Zwischenzeit kann dadurch |
| 29 | * KUERZER werden. Dazu bekommt es den Herkunftsnachweis "aus dem Lageplan" mit |
| 30 | * dem heutigen Datum, obwohl der Kopfkommentar von `uebernimmWege` zusagt, das |
| 31 | * sei die einzige Stelle, die ihn vergibt, "und sie vergibt ihn nur fuer die |
| 32 | * Beziehungen, deren Wege sie im selben Zug schreibt". |
| 33 | * |
| 34 | * GEGEN DEN ALTSTAND schlagen die beiden Faelle mit "->" in der Kennung fehl; |
| 35 | * die Gegenprobe mit gewoehnlichen Kennungen besteht dort ebenso. |
| 36 | */ |
| 37 | |
| 38 | const DATUM = new Date('2026-01-01T00:00:00Z'); |
| 39 | |
| 40 | /** 1 Bildpunkt = 1 m, damit sich die Masse unmittelbar ablesen lassen. */ |
| 41 | const KALIBRIERUNG = { |
| 42 | von: { x: 0, y: 0 }, |
| 43 | bis: { x: 400, y: 0 }, |
| 44 | laengeMeter: 400, |
| 45 | herkunft: 'gemessen', |
| 46 | } as const; |
| 47 | |
| 48 | /** Zufahrt und Furt einer Kreuzung, um `versatz` Meter nach Osten geschoben. */ |
| 49 | function haltlinienPaar(nummer: number, versatz: number): Haltlinie[] { |
| 50 | return [ |
| 51 | { |
| 52 | id: `hl-zufahrt-${nummer}`, |
| 53 | name: `Zufahrt ${nummer}`, |
| 54 | art: 'zufahrt', |
| 55 | armId: null, |
| 56 | von: { x: versatz, y: 0 }, |
| 57 | bis: { x: versatz + 20, y: 0 }, |
| 58 | }, |
| 59 | { |
| 60 | id: `hl-furt-${nummer}`, |
| 61 | name: `Furt ${nummer}`, |
| 62 | art: 'querung', |
| 63 | armId: null, |
| 64 | von: { x: versatz, y: 30 }, |
| 65 | bis: { x: versatz + 30, y: 30 }, |
| 66 | }, |
| 67 | ]; |
| 68 | } |
| 69 | |
| 70 | /** Geradeausstrom und kreuzende Furt; sie schneiden sich bei (versatz+10 | 30). */ |
| 71 | function linienPaar( |
| 72 | nummer: number, |
| 73 | versatz: number, |
| 74 | kennungGerade: string, |
| 75 | kennungFurt: string, |
| 76 | ): Planlinie[] { |
| 77 | return [ |
| 78 | { |
| 79 | id: kennungGerade, |
| 80 | name: `L${nummer} geradeaus`, |
| 81 | mode: 'kfz', |
| 82 | movement: 'geradeaus', |
| 83 | breiteMeter: REGELBREITE.kfz, |
| 84 | punkte: [ |
| 85 | { x: versatz + 10, y: 0 }, |
| 86 | { x: versatz + 10, y: 60 }, |
| 87 | ], |
| 88 | haltlinieId: `hl-zufahrt-${nummer}`, |
| 89 | startT: 0.5, |
| 90 | signalGroupId: `sg-k${nummer}`, |
| 91 | }, |
| 92 | { |
| 93 | id: kennungFurt, |
| 94 | name: `L${nummer} Furt`, |
| 95 | mode: 'fuss', |
| 96 | movement: 'querung', |
| 97 | breiteMeter: REGELBREITE.fuss, |
| 98 | punkte: [ |
| 99 | { x: versatz, y: 30 }, |
| 100 | { x: versatz + 30, y: 30 }, |
| 101 | ], |
| 102 | haltlinieId: `hl-furt-${nummer}`, |
| 103 | startT: 0, |
| 104 | signalGroupId: `sg-f${nummer}`, |
| 105 | }, |
| 106 | ]; |
| 107 | } |
| 108 | |
| 109 | const gruppe = (id: string, patch: Partial<SignalGroup> = {}): SignalGroup => ({ |
| 110 | id, |
| 111 | name: id, |
| 112 | mode: 'kfz', |
| 113 | movement: 'geradeaus', |
| 114 | armId: null, |
| 115 | vZul: 50, |
| 116 | lanes: 1, |
| 117 | vehicleClass: 'pkw', |
| 118 | minGreenOverride: null, |
| 119 | maxGreenOverride: null, |
| 120 | reducedMobility: false, |
| 121 | color: '#2f6fd0', |
| 122 | ...patch, |
| 123 | }); |
| 124 | |
| 125 | /** Die von Hand gepflegte Beziehung der ZWEITEN Kreuzung - 99 m Raeumweg. */ |
| 126 | const VON_HAND: Conflict = { |
| 127 | id: 'cf-hand', |
| 128 | fromId: 'sg-k2', |
| 129 | toId: 'sg-f2', |
| 130 | clearingDistance: 99, |
| 131 | enteringDistance: 4, |
| 132 | clearingSpeedOverride: null, |
| 133 | enteringSpeedOverride: null, |
| 134 | crossingTimeOverride: null, |
| 135 | manualIntergreen: null, |
| 136 | wegnachweis: WEGNACHWEIS_HANDEINGABE, |
| 137 | note: 'Von Hand nachgemessen.', |
| 138 | }; |
| 139 | |
| 140 | /** |
| 141 | * Zwei getrennte Kreuzungen in EINEM Lageplan, mit frei waehlbaren |
| 142 | * Planlinienkennungen. |
| 143 | */ |
| 144 | function aufbau(kennungen: readonly (readonly [string, string])[]): { |
| 145 | project: Project; |
| 146 | lageplan: Lageplan; |
| 147 | } { |
| 148 | const haltlinien = kennungen.flatMap((_, i) => haltlinienPaar(i + 1, i * 100)); |
| 149 | const linien = kennungen.flatMap(([gerade, furt], i) => linienPaar(i + 1, i * 100, gerade, furt)); |
| 150 | const lageplan: Lageplan = { |
| 151 | bild: null, |
| 152 | kalibrierung: KALIBRIERUNG, |
| 153 | haltlinien, |
| 154 | linien, |
| 155 | arbeitsbereiche: [], |
| 156 | signalgeber: [], |
| 157 | }; |
| 158 | const leer = createEmptyProject('Planlinienkennungen mit Pfeil', DATUM); |
| 159 | return { |
| 160 | project: { |
| 161 | ...leer, |
| 162 | signalGroups: kennungen.flatMap((_, i) => [ |
| 163 | gruppe(`sg-k${i + 1}`), |
| 164 | gruppe(`sg-f${i + 1}`, { mode: 'fuss', movement: 'querung', vehicleClass: 'keine' }), |
| 165 | ]), |
| 166 | conflicts: [VON_HAND], |
| 167 | lageplan, |
| 168 | }, |
| 169 | lageplan, |
| 170 | }; |
| 171 | } |
| 172 | |
| 173 | /** Der Weg der ERSTEN Kreuzung, vom Geradeausstrom auf seine Furt. */ |
| 174 | function wegDerErstenKreuzung(lageplan: Lageplan, ableitung: ReturnType<typeof leiteAb>): string[] { |
| 175 | const erste = lageplan.linien[0]!.id; |
| 176 | const zweite = lageplan.linien[1]!.id; |
| 177 | const weg = ableitung.wege.find((w) => w.vonLinieId === erste && w.nachLinieId === zweite); |
| 178 | // Vorbedingung jedes Falls: Ohne diesen Weg prueft nichts darunter etwas. |
| 179 | expect(weg, 'die Ströme der ersten Kreuzung schneiden sich im Plan nicht').toBeDefined(); |
| 180 | return [wegSchluessel(weg!)]; |
| 181 | } |
| 182 | |
| 183 | describe('Befund 43 - Wegeuebernahme bei "->" in der Planlinienkennung', () => { |
| 184 | it('haelt zwei Wege auseinander, die denselben zusammengeklebten Schluessel ergaeben', () => { |
| 185 | const { project, lageplan } = aufbau([ |
| 186 | ['a->b', 'c'], |
| 187 | ['a', 'b->c'], |
| 188 | ]); |
| 189 | const ableitung = leiteAb(project, lageplan); |
| 190 | |
| 191 | const erster = ableitung.wege.find((w) => w.vonLinieId === 'a->b' && w.nachLinieId === 'c'); |
| 192 | const zweiter = ableitung.wege.find((w) => w.vonLinieId === 'a' && w.nachLinieId === 'b->c'); |
| 193 | expect(erster, 'Weg der ersten Kreuzung').toBeDefined(); |
| 194 | expect(zweiter, 'Weg der zweiten Kreuzung').toBeDefined(); |
| 195 | |
| 196 | // Der Kern: zwei sachlich verschiedene Wege, zwei verschiedene Schluessel. |
| 197 | expect(wegSchluessel(erster!)).not.toBe(wegSchluessel(zweiter!)); |
| 198 | }); |
| 199 | |
| 200 | it('uebernimmt nur die angehakte Beziehung und laesst die andere unangetastet', () => { |
| 201 | const { project, lageplan } = aufbau([ |
| 202 | ['a->b', 'c'], |
| 203 | ['a', 'b->c'], |
| 204 | ]); |
| 205 | const ableitung = leiteAb(project, lageplan); |
| 206 | const ergebnis = uebernimmWege( |
| 207 | project, |
| 208 | lageplan, |
| 209 | ableitung, |
| 210 | wegDerErstenKreuzung(lageplan, ableitung), |
| 211 | DATUM, |
| 212 | ); |
| 213 | |
| 214 | expect(ergebnis.uebernommen).toBe(1); |
| 215 | |
| 216 | // Die nicht angehakte Beziehung behaelt ihren von Hand gepflegten, |
| 217 | // laengeren Raeumweg - und ihren Herkunftsnachweis. |
| 218 | const zweite = ergebnis.project.conflicts.find( |
| 219 | (c) => c.fromId === 'sg-k2' && c.toId === 'sg-f2', |
| 220 | ); |
| 221 | expect(zweite?.clearingDistance).toBe(99); |
| 222 | expect(zweite?.wegnachweis.herkunft).toBe('handeingabe'); |
| 223 | expect(zweite?.note).toBe('Von Hand nachgemessen.'); |
| 224 | |
| 225 | // Und die angehakte ist wirklich geschrieben worden. |
| 226 | const erste = ergebnis.project.conflicts.find( |
| 227 | (c) => c.fromId === 'sg-k1' && c.toId === 'sg-f1', |
| 228 | ); |
| 229 | expect(erste?.wegnachweis.herkunft).toBe('lageplan'); |
| 230 | }); |
| 231 | |
| 232 | it('uebernimmt bei gewoehnlichen Kennungen ebenfalls nur die angehakte (Gegenprobe)', () => { |
| 233 | const { project, lageplan } = aufbau([ |
| 234 | ['l-gerade-1', 'l-furt-1'], |
| 235 | ['l-gerade-2', 'l-furt-2'], |
| 236 | ]); |
| 237 | const ableitung = leiteAb(project, lageplan); |
| 238 | const ergebnis = uebernimmWege( |
| 239 | project, |
| 240 | lageplan, |
| 241 | ableitung, |
| 242 | wegDerErstenKreuzung(lageplan, ableitung), |
| 243 | DATUM, |
| 244 | ); |
| 245 | |
| 246 | expect(ergebnis.uebernommen).toBe(1); |
| 247 | const zweite = ergebnis.project.conflicts.find( |
| 248 | (c) => c.fromId === 'sg-k2' && c.toId === 'sg-f2', |
| 249 | ); |
| 250 | expect(zweite?.clearingDistance).toBe(99); |
| 251 | expect(zweite?.wegnachweis.herkunft).toBe('handeingabe'); |
| 252 | }); |
| 253 | }); |