lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests domain leerePhase.test.ts
| 1 | import { describe, expect, it } from 'vitest'; |
| 2 | import { buildSignalPlan, intergreenBetween, type GreenInterval } from '@/domain/plan/signalPlan'; |
| 3 | import { createPhase, createStandardIntersectionProject } from '@/domain/model/factory'; |
| 4 | import type { Project } from '@/domain/model/project'; |
| 5 | |
| 6 | /* |
| 7 | * Befund F6: Eine Phase ohne Signalgruppen setzte die Zwischenzeit des |
| 8 | * Phasenwechsels auf 0 s. |
| 9 | * |
| 10 | * Wer in der Phasenansicht auf "Phase hinzufügen" klickt und der neuen Phase |
| 11 | * noch keine Signalgruppe zuordnet, erhielt einen Signalzeitenplan, in dem die |
| 12 | * Freigabe der Folgephase in derselben Sekunde begann, in der die Freigabe der |
| 13 | * Vorphase endete - die gerechnete Zwischenzeit von mehreren Sekunden fiel |
| 14 | * ersatzlos weg, weil beide angrenzenden Uebergaenge auf 0 s standen. |
| 15 | */ |
| 16 | |
| 17 | const FESTES_DATUM = new Date('2026-01-01T00:00:00Z'); |
| 18 | |
| 19 | interface Prueffall { |
| 20 | readonly project: Project; |
| 21 | readonly leerePhaseId: string; |
| 22 | readonly vorphaseId: string; |
| 23 | } |
| 24 | |
| 25 | /** Standardknotenpunkt mit einer zusaetzlich angehaengten Phase ohne Signalgruppe. */ |
| 26 | function mitLeererPhase(): Prueffall { |
| 27 | const basis = createStandardIntersectionProject('Prueffall', FESTES_DATUM); |
| 28 | const leer = createPhase('Phase 3 - ohne Zuordnung'); |
| 29 | return { |
| 30 | project: { |
| 31 | ...basis, |
| 32 | phases: [...basis.phases, leer], |
| 33 | program: { ...basis.program, phaseOrder: [...basis.program.phaseOrder, leer.id] }, |
| 34 | }, |
| 35 | leerePhaseId: leer.id, |
| 36 | vorphaseId: basis.phases[1]!.id, |
| 37 | }; |
| 38 | } |
| 39 | |
| 40 | /** Kleinster Abstand vom Freigabeende der einen zum Freigabebeginn der anderen Gruppe. */ |
| 41 | function kleinsterAbstand( |
| 42 | raeumend: readonly GreenInterval[], |
| 43 | einfahrend: readonly GreenInterval[], |
| 44 | umlauf: number, |
| 45 | ): number { |
| 46 | let kleinster = Number.POSITIVE_INFINITY; |
| 47 | for (const a of raeumend) { |
| 48 | const ende = a.start + a.duration; |
| 49 | for (const b of einfahrend) { |
| 50 | const luecke = (((b.start - ende) % umlauf) + umlauf) % umlauf; |
| 51 | if (luecke < kleinster) kleinster = luecke; |
| 52 | } |
| 53 | } |
| 54 | return kleinster; |
| 55 | } |
| 56 | |
| 57 | describe('Phase ohne Signalgruppen im Signalzeitenplan', () => { |
| 58 | it('haelt jede geforderte Zwischenzeit ein, obwohl eine leere Phase dazwischen steht', () => { |
| 59 | const { project } = mitLeererPhase(); |
| 60 | const plan = buildSignalPlan(project); |
| 61 | |
| 62 | for (const conflict of project.conflicts) { |
| 63 | const gefordert = intergreenBetween(plan.intergreens, conflict.fromId, conflict.toId); |
| 64 | if (gefordert <= 0) continue; |
| 65 | |
| 66 | const raeumend = plan.groups.find((g) => g.groupId === conflict.fromId)!; |
| 67 | const einfahrend = plan.groups.find((g) => g.groupId === conflict.toId)!; |
| 68 | if (raeumend.greens.length === 0 || einfahrend.greens.length === 0) continue; |
| 69 | |
| 70 | const ist = kleinsterAbstand(raeumend.greens, einfahrend.greens, plan.cycleTime); |
| 71 | expect( |
| 72 | ist + 1e-6, |
| 73 | `${raeumend.name} räumt, ${einfahrend.name} fährt ein`, |
| 74 | ).toBeGreaterThanOrEqual(gefordert); |
| 75 | } |
| 76 | }); |
| 77 | |
| 78 | it('bemisst den Uebergang in die leere Phase nach der naechsten belegten Phase', () => { |
| 79 | const { project, leerePhaseId, vorphaseId } = mitLeererPhase(); |
| 80 | const plan = buildSignalPlan(project); |
| 81 | |
| 82 | const hinein = plan.transitions.find((t) => t.toPhaseId === leerePhaseId)!; |
| 83 | const heraus = plan.transitions.find((t) => t.fromPhaseId === leerePhaseId)!; |
| 84 | |
| 85 | // Massgebend ist die groesste Zwischenzeit zwischen den in der Vorphase |
| 86 | // endenden und den in der naechsten belegten Phase beginnenden Gruppen. |
| 87 | const vorphase = project.phases.find((p) => p.id === vorphaseId)!; |
| 88 | const folgephase = project.phases[0]!; |
| 89 | let erwartet = 0; |
| 90 | for (const fromId of vorphase.signalGroupIds) { |
| 91 | if (folgephase.signalGroupIds.includes(fromId)) continue; |
| 92 | for (const toId of folgephase.signalGroupIds) { |
| 93 | if (vorphase.signalGroupIds.includes(toId)) continue; |
| 94 | erwartet = Math.max(erwartet, intergreenBetween(plan.intergreens, fromId, toId)); |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | expect(erwartet).toBeGreaterThan(0); |
| 99 | expect(hinein.duration).toBe(erwartet); |
| 100 | expect(hinein.critical?.intergreen).toBe(erwartet); |
| 101 | // Aus der leeren Phase heraus endet keine Freigabe - dort ist nichts zu sichern. |
| 102 | expect(heraus.duration).toBe(0); |
| 103 | }); |
| 104 | }); |