lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests domain leerePhaseAmUmlaufende.test.ts
| 1 | import { describe, expect, it } from 'vitest'; |
| 2 | import { buildSignalPlan } from '@/domain/plan/signalPlan'; |
| 3 | import { createStandardIntersectionProject } from '@/domain/model/factory'; |
| 4 | import * as actions from '@/app/actions'; |
| 5 | |
| 6 | /* |
| 7 | * Befund 13, behoben in Fassung 5.10.0 - Freigabefenster am Umlaufende. |
| 8 | * |
| 9 | * `buildGroupWindows` bildete den Fensterbeginn `rawStart` und mass die Dauer |
| 10 | * gegen den UNGEFALTETEN Wert, waehrend die Zeile darunter den Beginn bereits |
| 11 | * in den Umlauf zurueckfaltete. Endet der Lauf einer Gruppe genau an der |
| 12 | * Umlaufgrenze - alle folgenden Phasen sind leer (0 s Dauer), und der |
| 13 | * Uebergang aus der letzten belegten Phase betraegt 0 s -, gilt |
| 14 | * rawStart == end == tU: Der Nicht-Ueberlauf-Zweig rechnete dann 0 s statt des |
| 15 | * ganzen Umlaufs. |
| 16 | * |
| 17 | * Folge: Die Phasentabelle wies 30 s aus, die Signalgruppenzeile 0 s; dazu |
| 18 | * kamen Scheinbefunde (Mindestfreigabezeit unterschritten, Auslastungsgrad |
| 19 | * unendlich), die es fachlich nicht gibt. |
| 20 | * |
| 21 | * GEGEN DEN ALTSTAND schlagen die ersten drei Faelle fehl (Freigabezeit 0 s |
| 22 | * statt des ganzen Umlaufs, dazu die Scheinmeldung). Der vierte Fall ist die |
| 23 | * Gegenprobe und besteht auch dort: Bei zwei belegten und zwei leeren Phasen |
| 24 | * endet kein Lauf an der Umlaufgrenze, die Faltung aendert dort nichts. |
| 25 | */ |
| 26 | |
| 27 | const DATUM = new Date('2026-01-01T00:00:00Z'); |
| 28 | |
| 29 | /** Standardknotenpunkt, dessen zweite Phase durch eine leere ersetzt ist. */ |
| 30 | function eineBelegtePhaseUndEineLeere() { |
| 31 | const roh = createStandardIntersectionProject('Leere Phase', DATUM); |
| 32 | const ohne = actions.removePhase(roh, roh.phases[1]!.id); |
| 33 | return actions.addPhase(ohne); |
| 34 | } |
| 35 | |
| 36 | describe('Befund 13 - Freigabefenster, dessen Lauf an der Umlaufgrenze endet', () => { |
| 37 | it('gibt der Gruppe der einzigen belegten Phase den ganzen Umlauf', () => { |
| 38 | const plan = buildSignalPlan(eineBelegtePhaseUndEineLeere()); |
| 39 | const k1 = plan.groups.find((g) => g.name === 'K1')!; |
| 40 | |
| 41 | // Die leere Phase hat 0 s Dauer und beide Uebergaenge 0 s: Phase 1 traegt |
| 42 | // den gesamten Umlauf, ihre Gruppen sind durchgehend freigegeben. |
| 43 | expect(plan.phases[0]?.duration).toBe(plan.cycleTime); |
| 44 | expect(k1.greens).toHaveLength(1); |
| 45 | expect(k1.greens[0]?.start).toBe(0); |
| 46 | expect(k1.greens[0]?.duration).toBe(plan.cycleTime); |
| 47 | expect(k1.totalGreen).toBe(plan.cycleTime); |
| 48 | }); |
| 49 | |
| 50 | it('erzeugt dabei keinen Scheinbefund zur Mindestfreigabezeit', () => { |
| 51 | const plan = buildSignalPlan(eineBelegtePhaseUndEineLeere()); |
| 52 | |
| 53 | // Eine Gruppe mit 30 s Freigabezeit unterschreitet keine |
| 54 | // Mindestfreigabezeit - die Meldung entstand allein aus dem Fenster 0 s. |
| 55 | const betroffen = plan.notes.filter( |
| 56 | (n) => n.code === 'mindestfreigabezeit-unterschritten' && n.signalGroupId !== undefined, |
| 57 | ); |
| 58 | expect(betroffen).toEqual([]); |
| 59 | // Und der Auslastungsgrad ist endlich, weil die Kapazitaet nicht 0 ist. |
| 60 | const k1 = plan.groups.find((g) => g.name === 'K1')!; |
| 61 | expect(k1.capacity?.capacity ?? 0).toBeGreaterThan(0); |
| 62 | }); |
| 63 | |
| 64 | it('faltet auch einen Lauf ueber mehrere belegte Phasen', () => { |
| 65 | // K1 laeuft durch Phase 1 UND Phase 2; erst danach steht die leere Phase. |
| 66 | const roh = createStandardIntersectionProject('Zwei Laeufe', DATUM); |
| 67 | const k1 = roh.signalGroups.find((g) => g.name === 'K1')!; |
| 68 | const mitK1 = actions.updatePhase(roh, roh.phases[1]!.id, { signalGroupIds: [k1.id] }); |
| 69 | const plan = buildSignalPlan(actions.addPhase(mitK1)); |
| 70 | |
| 71 | const geplant = plan.groups.find((g) => g.name === 'K1')!; |
| 72 | expect(geplant.totalGreen).toBe(plan.cycleTime); |
| 73 | // Die uebrigen Gruppen behalten ihr gewoehnliches Fenster. |
| 74 | expect(plan.groups.find((g) => g.name === 'K2')?.totalGreen).toBe(plan.phases[0]?.duration); |
| 75 | }); |
| 76 | |
| 77 | it('aendert nichts, wenn kein Lauf an der Umlaufgrenze endet', () => { |
| 78 | // Gegenprobe: zwei belegte Phasen, danach zwei leere. Der Uebergang aus |
| 79 | // Phase 2 wird gegen Phase 1 bemessen und ist groesser als 0; kein Lauf |
| 80 | // endet an der Umlaufgrenze. |
| 81 | const roh = createStandardIntersectionProject('Gegenprobe', DATUM); |
| 82 | const plan = buildSignalPlan(actions.addPhase(actions.addPhase(roh))); |
| 83 | |
| 84 | for (const gruppe of plan.groups) { |
| 85 | expect(gruppe.totalGreen).toBeGreaterThan(0); |
| 86 | expect(gruppe.totalGreen).toBeLessThan(plan.cycleTime); |
| 87 | } |
| 88 | }); |
| 89 | }); |