lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests domain zufahrtAbleitung.test.ts
| 1 | import { describe, expect, it } from 'vitest'; |
| 2 | import { leiteAb } from '@/domain/geometrie/ableitung'; |
| 3 | import { REGELBREITE } from '@/domain/geometrie/vermessung'; |
| 4 | import type { Haltlinie, Lageplan, Planlinie } from '@/domain/geometrie/lageplan'; |
| 5 | import { createEmptyProject } from '@/domain/model/factory'; |
| 6 | import type { Movement } from '@/domain/rilsa/types'; |
| 7 | |
| 8 | /** |
| 9 | * Vierarmiger Knotenpunkt mit je drei Fahrbeziehungen. |
| 10 | * |
| 11 | * Genau die Zeichnung, nach der der Anwender gefragt hat: aus jeder Zufahrt |
| 12 | * geradeaus, links und rechts. Zwoelf Stroeme, alle an vier Haltlinien. |
| 13 | * |
| 14 | * Der Fall ist der Massstab fuer die ganze Aenderung. Ohne Haltlinien meldet |
| 15 | * das Programm zwoelf Konflikte, die keine sind - je Zufahrt drei Paare, die |
| 16 | * sich nur ihren Anfangspunkt teilen. Mit Haltlinien duerfen es null sein, und |
| 17 | * kein einziger echter Konflikt darf dabei verlorengehen. |
| 18 | * |
| 19 | * Massstab: 1 Bildpunkt = 1 m, damit sich die Zahlen von Hand nachrechnen |
| 20 | * lassen. Der Knotenpunkt liegt im Ursprung, die Zufahrten enden 8 m davor. |
| 21 | */ |
| 22 | |
| 23 | const FIXED_DATE = new Date('2026-01-01T00:00:00Z'); |
| 24 | |
| 25 | interface Arm { |
| 26 | readonly id: string; |
| 27 | readonly name: string; |
| 28 | /** Einheitsvektor von der Haltlinie zum Knotenpunktmittelpunkt. */ |
| 29 | readonly ein: { x: number; y: number }; |
| 30 | } |
| 31 | |
| 32 | const ARME: readonly Arm[] = [ |
| 33 | { id: 'nord', name: 'Nord', ein: { x: 0, y: 1 } }, |
| 34 | { id: 'ost', name: 'Ost', ein: { x: -1, y: 0 } }, |
| 35 | { id: 'sued', name: 'Sued', ein: { x: 0, y: -1 } }, |
| 36 | { id: 'west', name: 'West', ein: { x: 1, y: 0 } }, |
| 37 | ]; |
| 38 | |
| 39 | /** Abstand der Haltlinie vom Mittelpunkt. */ |
| 40 | const HALT = 8; |
| 41 | /** Wie weit die Fahrlinien ueber den Knotenpunkt hinauslaufen. */ |
| 42 | const AUS = 30; |
| 43 | |
| 44 | function drehe(v: { x: number; y: number }, links: boolean): { x: number; y: number } { |
| 45 | // Linksdrehung um 90 Grad im Bildkoordinatensystem. |
| 46 | return links ? { x: v.y, y: -v.x } : { x: -v.y, y: v.x }; |
| 47 | } |
| 48 | |
| 49 | /** |
| 50 | * Baut einen Knotenpunkt. |
| 51 | * `mitHaltlinien` steuert, ob die Stroeme ihrer Zufahrt zugeordnet sind. |
| 52 | */ |
| 53 | function knotenpunkt(mitHaltlinien: boolean): Lageplan { |
| 54 | const haltlinien: Haltlinie[] = []; |
| 55 | const linien: Planlinie[] = []; |
| 56 | |
| 57 | for (const arm of ARME) { |
| 58 | const start = { x: -arm.ein.x * HALT, y: -arm.ein.y * HALT }; |
| 59 | // Die Haltlinie liegt quer zur Fahrtrichtung, 6 m breit. |
| 60 | const quer = drehe(arm.ein, true); |
| 61 | const haltlinie: Haltlinie = { |
| 62 | id: `hl-${arm.id}`, |
| 63 | name: arm.name, |
| 64 | art: 'zufahrt', |
| 65 | armId: arm.id, |
| 66 | von: { x: start.x - quer.x * 3, y: start.y - quer.y * 3 }, |
| 67 | bis: { x: start.x + quer.x * 3, y: start.y + quer.y * 3 }, |
| 68 | }; |
| 69 | haltlinien.push(haltlinie); |
| 70 | |
| 71 | const beziehungen: readonly { movement: Movement; links: boolean | null }[] = [ |
| 72 | { movement: 'geradeaus', links: null }, |
| 73 | { movement: 'links', links: true }, |
| 74 | { movement: 'rechts', links: false }, |
| 75 | ]; |
| 76 | |
| 77 | beziehungen.forEach((b, index) => { |
| 78 | // Alle drei beginnen in der Mitte der Haltlinie - der Regelfall, wenn der |
| 79 | // Anwender sie nacheinander auf dieselbe Stelle setzt. |
| 80 | const punkte = [start, { x: 0, y: 0 }]; |
| 81 | const richtung = b.links === null ? arm.ein : drehe(arm.ein, b.links); |
| 82 | punkte.push({ x: richtung.x * AUS, y: richtung.y * AUS }); |
| 83 | |
| 84 | linien.push({ |
| 85 | id: `${arm.id}-${b.movement}`, |
| 86 | name: `${arm.name} ${b.movement}`, |
| 87 | mode: 'kfz', |
| 88 | movement: b.movement, |
| 89 | breiteMeter: REGELBREITE.kfz, |
| 90 | punkte, |
| 91 | haltlinieId: mitHaltlinien ? haltlinie.id : null, |
| 92 | startT: 0.5 + index * 0.0001, |
| 93 | signalGroupId: null, |
| 94 | }); |
| 95 | }); |
| 96 | } |
| 97 | |
| 98 | return { |
| 99 | arbeitsbereiche: [], |
| 100 | signalgeber: [], |
| 101 | bild: { |
| 102 | datenUrl: 'data:image/png;base64,AAAA', |
| 103 | breite: 200, |
| 104 | hoehe: 200, |
| 105 | herkunft: 'Test', |
| 106 | geladenAm: FIXED_DATE.toISOString(), |
| 107 | }, |
| 108 | // 1 Bildpunkt = 1 m. |
| 109 | kalibrierung: { |
| 110 | von: { x: 0, y: 0 }, |
| 111 | bis: { x: 100, y: 0 }, |
| 112 | laengeMeter: 100, |
| 113 | herkunft: 'georeferenziert', |
| 114 | }, |
| 115 | haltlinien, |
| 116 | linien, |
| 117 | }; |
| 118 | } |
| 119 | |
| 120 | function projektMitArmen() { |
| 121 | const leer = createEmptyProject('Vierarmig', FIXED_DATE); |
| 122 | return { |
| 123 | ...leer, |
| 124 | intersection: { |
| 125 | ...leer.intersection, |
| 126 | arms: ARME.map((a) => ({ id: a.id, name: a.name, direction: a.name, lanes: 1, vZul: 50 })), |
| 127 | }, |
| 128 | }; |
| 129 | } |
| 130 | |
| 131 | /** Paare, die derselben Zufahrt angehoeren. */ |
| 132 | function ausGleicherZufahrt(aId: string, bId: string): boolean { |
| 133 | return (aId.split('-')[0] ?? '') === (bId.split('-')[0] ?? ''); |
| 134 | } |
| 135 | |
| 136 | describe('Vierarmiger Knotenpunkt mit je drei Fahrbeziehungen', () => { |
| 137 | it('meldet ohne Haltlinien zwoelf Konflikte, die keine sind', () => { |
| 138 | // Der Zustand vor der Aenderung - und der Zustand jedes uebernommenen |
| 139 | // Projekts. Er wird hier festgehalten, damit der Gewinn messbar bleibt. |
| 140 | const ableitung = leiteAb(projektMitArmen(), knotenpunkt(false)); |
| 141 | const falsch = ableitung.vertraeglichkeit.filter( |
| 142 | (v) => v.feindlich && ausGleicherZufahrt(v.aLinieId, v.bLinieId), |
| 143 | ); |
| 144 | // Vier Zufahrten mit je drei Stroemen: drei Paare je Zufahrt. |
| 145 | expect(falsch).toHaveLength(12); |
| 146 | }); |
| 147 | |
| 148 | it('meldet mit Haltlinien keinen einzigen davon', () => { |
| 149 | const ableitung = leiteAb(projektMitArmen(), knotenpunkt(true)); |
| 150 | const falsch = ableitung.vertraeglichkeit.filter( |
| 151 | (v) => v.feindlich && ausGleicherZufahrt(v.aLinieId, v.bLinieId), |
| 152 | ); |
| 153 | expect(falsch).toHaveLength(0); |
| 154 | }); |
| 155 | |
| 156 | it('verliert dabei keinen echten Konflikt zwischen den Zufahrten', () => { |
| 157 | const ohne = leiteAb(projektMitArmen(), knotenpunkt(false)); |
| 158 | const mit = leiteAb(projektMitArmen(), knotenpunkt(true)); |
| 159 | |
| 160 | const echt = (a: typeof ohne) => |
| 161 | a.vertraeglichkeit |
| 162 | .filter((v) => v.feindlich && !ausGleicherZufahrt(v.aLinieId, v.bLinieId)) |
| 163 | .map((v) => `${v.aLinieId}|${v.bLinieId}`) |
| 164 | .sort(); |
| 165 | |
| 166 | expect(echt(mit)).toEqual(echt(ohne)); |
| 167 | // Und es sind wirklich welche da - sonst prueft der Vergleich nichts. |
| 168 | expect(echt(mit).length).toBeGreaterThan(10); |
| 169 | }); |
| 170 | |
| 171 | it('weist die unterdrueckten Beruehrungen aus', () => { |
| 172 | const ableitung = leiteAb(projektMitArmen(), knotenpunkt(true)); |
| 173 | expect(ableitung.unterdrueckt).toHaveLength(12); |
| 174 | for (const paar of ableitung.unterdrueckt) { |
| 175 | expect(ausGleicherZufahrt(paar.aLinieId, paar.bLinieId)).toBe(true); |
| 176 | expect(paar.anzahl).toBeGreaterThan(0); |
| 177 | } |
| 178 | }); |
| 179 | |
| 180 | it('erkennt die einstreifige Zufahrt als gemeinsamen Fahrstreifen', () => { |
| 181 | // In dieser Zeichnung laufen alle drei Stroeme einer Zufahrt acht Meter auf |
| 182 | // demselben Fahrstreifen und trennen sich erst im Knotenpunkt - der |
| 183 | // Regelfall einer einstreifigen Zufahrt. Sie sind vertraeglich, muessen aber |
| 184 | // denselben Signalgeber bekommen; getrennt signalisieren laesst sich nur, |
| 185 | // was einen eigenen Fahrstreifen hat. Genau das sagt die Begruendung. |
| 186 | const ableitung = leiteAb(projektMitArmen(), knotenpunkt(true)); |
| 187 | const paar = ableitung.vertraeglichkeit.find( |
| 188 | (v) => v.aLinieId === 'nord-geradeaus' && v.bLinieId === 'nord-links', |
| 189 | ); |
| 190 | expect(paar).toBeDefined(); |
| 191 | expect(paar?.feindlich).toBe(false); |
| 192 | expect(paar?.grund).toBe('gemeinsamer-fahrstreifen'); |
| 193 | expect(paar?.begruendung).toContain('dieselbe Signalgruppe'); |
| 194 | }); |
| 195 | |
| 196 | it('schlaegt keine Zwischenzeit zwischen Stroemen derselben Zufahrt vor', () => { |
| 197 | const ableitung = leiteAb(projektMitArmen(), knotenpunkt(true)); |
| 198 | const falsch = ableitung.wege.filter((w) => ausGleicherZufahrt(w.vonLinieId, w.nachLinieId)); |
| 199 | expect(falsch).toHaveLength(0); |
| 200 | }); |
| 201 | |
| 202 | it('warnt, solange Fahrlinien keiner Haltlinie zugeordnet sind', () => { |
| 203 | const ableitung = leiteAb(projektMitArmen(), knotenpunkt(false)); |
| 204 | const hinweis = ableitung.hinweise.find((h) => h.code === 'haltlinie-fehlt'); |
| 205 | expect(hinweis).toBeDefined(); |
| 206 | // Die Anzeige blendet blosse Hinweise aus - die Schwere muss hoeher sein. |
| 207 | expect(hinweis?.severity).toBe('warnung'); |
| 208 | }); |
| 209 | }); |