lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests ui linksabbiegerPhase.test.ts
| 1 | import { describe, expect, it } from 'vitest'; |
| 2 | import { isGreenAt } from '@/domain/plan/aspects'; |
| 3 | import { buildSignalPlan } from '@/domain/plan/signalPlan'; |
| 4 | import { validateProject } from '@/domain/validation'; |
| 5 | import { ASSISTENT_VORGABE, assistentZusammenfassung, projektAusAngaben } from '@/ui/assistent'; |
| 6 | import type { AssistentAngaben } from '@/ui/assistent'; |
| 7 | import type { Project, SignalGroup } from '@/domain/model/project'; |
| 8 | |
| 9 | /** |
| 10 | * Befund 2: das Kaestchen "Eigene Freigabe fuer Linksabbieger" des gefuehrten |
| 11 | * Einstiegs. |
| 12 | * |
| 13 | * Der Assistent legte die neue Linksabbiegergruppe in DIESELBE Phase wie den |
| 14 | * Geradeausstrom ihrer eigenen Zufahrt. Die Konfliktschleife ueberspringt jedes |
| 15 | * Paar derselben Phase, und damit stand der Linksabbieger aus Nord gegenueber |
| 16 | * dem geradeausfahrenden Gegenverkehr aus Sued als vertraeglich im Plan: keine |
| 17 | * Konfliktbeziehung, keine Zwischenzeit, gleichzeitiges Gruen. Dieselbe Phase |
| 18 | * fuehrte ausserdem die Furten der Querachse mit, die der Linksabbieger quert. |
| 19 | * |
| 20 | * Die Zusage des Kaestchens ist die gesicherte Fuehrung ("Noetig, wenn |
| 21 | * Linksabbieger nicht in Luecken des Gegenverkehrs abbiegen koennen"), und das |
| 22 | * Programm nennt sie an zwei weiteren Stellen so: die Hilfe zur Phase ("wird |
| 23 | * daraus eine dritte Phase") und der Pruefbericht ("fuehren Sie ... mit eigener |
| 24 | * Freigabezeit"). Geprueft wird deshalb das Erzeugnis, nicht die Oberflaeche. |
| 25 | */ |
| 26 | |
| 27 | const FESTES_DATUM = new Date('2026-01-01T00:00:00Z'); |
| 28 | |
| 29 | function baue(teil: Partial<AssistentAngaben>): Project { |
| 30 | return projektAusAngaben({ ...ASSISTENT_VORGABE, ...teil }, 'Pruefung', FESTES_DATUM); |
| 31 | } |
| 32 | |
| 33 | /** Kfz-Signalgruppe einer Zufahrt in der gesuchten Fahrtrichtung. */ |
| 34 | function kfz(projekt: Project, zufahrt: string, movement: string): SignalGroup { |
| 35 | const arm = projekt.intersection.arms.find((a) => a.name === zufahrt); |
| 36 | expect(arm, `Zufahrt "${zufahrt}" fehlt`).toBeDefined(); |
| 37 | const treffer = projekt.signalGroups.find( |
| 38 | (g) => g.armId === arm!.id && g.mode === 'kfz' && g.movement === movement, |
| 39 | ); |
| 40 | expect(treffer, `Kfz-Gruppe ${zufahrt}/${movement} fehlt`).toBeDefined(); |
| 41 | return treffer!; |
| 42 | } |
| 43 | |
| 44 | /** Die Phase, in der die Gruppe Freigabe bekommt. */ |
| 45 | function phaseVon(projekt: Project, gruppe: SignalGroup): { name: string; ids: readonly string[] } { |
| 46 | const phase = projekt.phases.find((p) => p.signalGroupIds.includes(gruppe.id)); |
| 47 | expect(phase, `${gruppe.name} steht in keiner Phase`).toBeDefined(); |
| 48 | return { name: phase!.name, ids: phase!.signalGroupIds }; |
| 49 | } |
| 50 | |
| 51 | /** Gibt es zwischen beiden Gruppen eine erfasste Beziehung - in einer Richtung? */ |
| 52 | function beziehung(projekt: Project, a: SignalGroup, b: SignalGroup): boolean { |
| 53 | return projekt.conflicts.some( |
| 54 | (c) => (c.fromId === a.id && c.toId === b.id) || (c.fromId === b.id && c.toId === a.id), |
| 55 | ); |
| 56 | } |
| 57 | |
| 58 | /** Zeigen beide Gruppen zu irgendeinem Zeitpunkt gemeinsam Freigabe? */ |
| 59 | function gemeinsamesGruen(projekt: Project, a: SignalGroup, b: SignalGroup): number { |
| 60 | const plan = buildSignalPlan(projekt); |
| 61 | expect(plan.feasible, 'Der Plan ist nicht schaltbar').toBe(true); |
| 62 | const links = plan.groups.find((g) => g.groupId === a.id)!; |
| 63 | const gegen = plan.groups.find((g) => g.groupId === b.id)!; |
| 64 | let sekunden = 0; |
| 65 | for (let t = 0; t < plan.cycleTime; t += 0.5) { |
| 66 | if (isGreenAt(links, t, plan.cycleTime) && isGreenAt(gegen, t, plan.cycleTime)) sekunden += 0.5; |
| 67 | } |
| 68 | return sekunden; |
| 69 | } |
| 70 | |
| 71 | describe('Eigene Freigabe fuer Linksabbieger: eigene Phase statt gemeinsamer', () => { |
| 72 | it('legt den Linksabbieger nicht in die Phase seines Gegenverkehrs', () => { |
| 73 | const projekt = baue({ form: 'kreuzung', linksabbieger: true }); |
| 74 | const nordLinks = kfz(projekt, 'Nord', 'links'); |
| 75 | const suedGerade = kfz(projekt, 'Süd', 'geradeaus'); |
| 76 | |
| 77 | expect(phaseVon(projekt, nordLinks).name).not.toBe(phaseVon(projekt, suedGerade).name); |
| 78 | }); |
| 79 | |
| 80 | it('fuehrt Linksabbieger und Gegenverkehr als Konfliktbeziehung', () => { |
| 81 | const projekt = baue({ form: 'kreuzung', linksabbieger: true }); |
| 82 | const nordLinks = kfz(projekt, 'Nord', 'links'); |
| 83 | |
| 84 | // Der Gegenverkehr der eigenen Achse - geradeaus wie abbiegend ist der |
| 85 | // Linksabbieger aus Sued unkritisch (beide biegen links ab und kreuzen sich |
| 86 | // nicht), der GERADEAUSSTROM aus Sued kreuzt ihn dagegen. |
| 87 | expect(beziehung(projekt, nordLinks, kfz(projekt, 'Süd', 'geradeaus'))).toBe(true); |
| 88 | expect( |
| 89 | beziehung(projekt, kfz(projekt, 'Süd', 'links'), kfz(projekt, 'Nord', 'geradeaus')), |
| 90 | ).toBe(true); |
| 91 | }); |
| 92 | |
| 93 | it('gibt Linksabbieger und Gegenverkehr nie gleichzeitig frei', () => { |
| 94 | const projekt = baue({ form: 'kreuzung', linksabbieger: true }); |
| 95 | expect( |
| 96 | gemeinsamesGruen(projekt, kfz(projekt, 'Nord', 'links'), kfz(projekt, 'Süd', 'geradeaus')), |
| 97 | ).toBe(0); |
| 98 | }); |
| 99 | |
| 100 | it('fuehrt in der Linksabbiegerphase keine Furt und keinen Geradeausstrom mit', () => { |
| 101 | const projekt = baue({ form: 'kreuzung', linksabbieger: true, fussgaenger: true, rad: true }); |
| 102 | const phase = phaseVon(projekt, kfz(projekt, 'Nord', 'links')); |
| 103 | const mitlaeufer = phase.ids.map((id) => projekt.signalGroups.find((g) => g.id === id)!); |
| 104 | |
| 105 | for (const g of mitlaeufer) { |
| 106 | expect(`${g.name}: ${g.mode}/${g.movement}`).toBe(`${g.name}: kfz/links`); |
| 107 | } |
| 108 | }); |
| 109 | |
| 110 | it('gibt die gegenueberliegenden Linksabbieger gemeinsam frei', () => { |
| 111 | const projekt = baue({ form: 'kreuzung', linksabbieger: true }); |
| 112 | // Sie kreuzen einander nicht - eine eigene Phase je Zufahrt waere ein |
| 113 | // Umlauf, den kein Knotenpunkt braucht. |
| 114 | expect(phaseVon(projekt, kfz(projekt, 'Nord', 'links')).name).toBe( |
| 115 | phaseVon(projekt, kfz(projekt, 'Süd', 'links')).name, |
| 116 | ); |
| 117 | expect(beziehung(projekt, kfz(projekt, 'Nord', 'links'), kfz(projekt, 'Süd', 'links'))).toBe( |
| 118 | false, |
| 119 | ); |
| 120 | }); |
| 121 | |
| 122 | it('legt ohne das Kaestchen keine zusaetzliche Phase an', () => { |
| 123 | // Die Gegenprobe: Wer nicht ankreuzt, bekommt den Umlauf von vorher. |
| 124 | expect(baue({ form: 'kreuzung' }).phases.map((p) => p.name)).toEqual([ |
| 125 | 'Phase 1 – Nord/Süd', |
| 126 | 'Phase 2 – Ost/West', |
| 127 | ]); |
| 128 | expect(baue({ form: 'fuenfarmig' }).phases).toHaveLength(3); |
| 129 | }); |
| 130 | |
| 131 | it('nummeriert die Phasen des Umlaufs lueckenlos durch', () => { |
| 132 | for (const teil of [ |
| 133 | { form: 'kreuzung' as const, linksabbieger: true }, |
| 134 | { form: 'einmuendung' as const, linksabbieger: true }, |
| 135 | { form: 'fuenfarmig' as const, linksabbieger: true }, |
| 136 | ]) { |
| 137 | const projekt = baue(teil); |
| 138 | projekt.phases.forEach((p, i) => { |
| 139 | expect(p.name.startsWith(`Phase ${i + 1} – `), `${teil.form}: ${p.name}`).toBe(true); |
| 140 | expect(p.signalGroupIds.length, `${teil.form}: ${p.name} ist leer`).toBeGreaterThan(0); |
| 141 | }); |
| 142 | expect(projekt.program.phaseOrder).toHaveLength(projekt.phases.length); |
| 143 | } |
| 144 | }); |
| 145 | |
| 146 | it('gibt dem Ostarm der Einmuendung keinen Linksabbieger und keine leere Phase', () => { |
| 147 | const projekt = baue({ form: 'einmuendung', linksabbieger: true }); |
| 148 | const arm = projekt.intersection.arms.find((a) => a.name === 'Ost')!; |
| 149 | expect( |
| 150 | projekt.signalGroups.filter((g) => g.armId === arm.id && g.movement === 'links'), |
| 151 | ).toHaveLength(0); |
| 152 | // Nord und Sued haben Gegenverkehr, Ost nicht: drei Phasen, nicht vier. |
| 153 | expect(projekt.phases).toHaveLength(3); |
| 154 | }); |
| 155 | |
| 156 | it('bleibt mit eigener Linksabbiegerphase schaltbar und ohne Fachfehler', () => { |
| 157 | for (const teil of [ |
| 158 | { form: 'kreuzung' as const, linksabbieger: true, rad: true }, |
| 159 | { form: 'einmuendung' as const, linksabbieger: true, rad: true }, |
| 160 | { form: 'fuenfarmig' as const, linksabbieger: true, rad: true }, |
| 161 | ]) { |
| 162 | const projekt = baue(teil); |
| 163 | const plan = buildSignalPlan(projekt); |
| 164 | expect(plan.feasible, teil.form).toBe(true); |
| 165 | const fehler = validateProject(projekt, plan, FESTES_DATUM) |
| 166 | .findings.filter((f) => f.severity === 'fehler' && f.category !== 'projektdaten') |
| 167 | .map((f) => `${f.rule}: ${f.message}`); |
| 168 | expect(fehler, `${teil.form}: ${fehler.join('\n')}`).toHaveLength(0); |
| 169 | } |
| 170 | }); |
| 171 | |
| 172 | it('nennt die eigene Phase in der Zusammenfassung der Annahmen', () => { |
| 173 | // Was der Assistent an Fuehrungsform festlegt, steht im letzten Schritt - |
| 174 | // dieselbe Regel wie fuer die Furtwege (Befund C12). |
| 175 | const mit = assistentZusammenfassung({ ...ASSISTENT_VORGABE, linksabbieger: true }).join(' '); |
| 176 | expect(mit).toContain('Linksabbieger'); |
| 177 | expect(mit).toContain('eigene Phase'); |
| 178 | expect(mit).toContain('Gegenverkehr'); |
| 179 | |
| 180 | const ohne = assistentZusammenfassung({ ...ASSISTENT_VORGABE, linksabbieger: false }).join(' '); |
| 181 | expect(ohne).not.toContain('Linksabbieger'); |
| 182 | }); |
| 183 | }); |