lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests domain freigabezeitverteilung.test.ts
| 1 | import { describe, expect, it } from 'vitest'; |
| 2 | import { distributeGreenTimes } from '@/domain/rilsa/greenSplit'; |
| 3 | |
| 4 | /** |
| 5 | * Freigabezeitverteilung, behoben in Fassung 5.9.0. |
| 6 | * |
| 7 | * Befund 5: Die Restzeit ging an die Phase mit dem groessten Gewicht - also |
| 8 | * gerade an die Phase, die soeben an ihrer Hoechstfreigabezeit |
| 9 | * gekappt worden war -, waehrend eine Phase mit freiem Spielraum |
| 10 | * an ihrer Mindestfreigabezeit stehen blieb. |
| 11 | * Befund 31: Die Schlussabsicherung der Mindestfreigabezeit hob einen Wert an, |
| 12 | * nachdem die Summe bereits ausgeglichen war; Freigabe- und |
| 13 | * Uebergangszeiten ergaben dann mehr als die Umlaufzeit. |
| 14 | */ |
| 15 | |
| 16 | /** Ausloeser des Befundes 5, Fall A: Knotenpunkt mit fester Freigabezeit. */ |
| 17 | function knotenpunktMitHandvorgabe(): ReturnType<typeof distributeGreenTimes> { |
| 18 | // Feste Umlaufzeit 95 s, zwei Phasenuebergaenge zu je 10 s -> 75 s verfuegbar. |
| 19 | // Phase 1: Kfz-Gruppe, Saettigungsgrad 0,35, "Feste Freigabezeit" 40 s |
| 20 | // (eine Handvorgabe heisst minGreen = maxGreen = 40 s). |
| 21 | // Phase 2: reine Fussgaengerphase. Fussgaenger tragen keine |
| 22 | // Saettigungsverkehrsstaerke, ihr Gewicht ist deshalb 0. |
| 23 | return distributeGreenTimes( |
| 24 | 95, |
| 25 | [ |
| 26 | { id: 'p1', weight: 0.35, minGreen: 40, maxGreen: 40 }, |
| 27 | { id: 'p2', weight: 0, minGreen: 20, maxGreen: 120 }, |
| 28 | ], |
| 29 | [10, 10], |
| 30 | ); |
| 31 | } |
| 32 | |
| 33 | describe('Freigabezeitverteilung: Restzeit und Sekundenraster', () => { |
| 34 | it('gibt die Restzeit der Phase mit Spielraum statt der bereits gekappten Phase', () => { |
| 35 | const result = knotenpunktMitHandvorgabe(); |
| 36 | |
| 37 | expect(result.feasible).toBe(true); |
| 38 | // Bis hierher erhielt der Anwender 55 s und 20 s: seine eingetragenen 40 s |
| 39 | // waren um 15 s ueberschrieben, obwohl die Fussgaengerphase bis 120 s |
| 40 | // haette reichen duerfen. |
| 41 | expect(result.phases[0]!.green).toBe(40); |
| 42 | expect(result.phases[1]!.green).toBe(35); |
| 43 | const sum = result.phases.reduce((a, p) => a + p.green, 0); |
| 44 | expect(sum + result.transitionTime).toBe(95); |
| 45 | }); |
| 46 | |
| 47 | it('behauptet nicht, alle Phasen laegen an ihrer Hoechstfreigabezeit, solange eine an ihrer Mindestfreigabezeit steht', () => { |
| 48 | const result = knotenpunktMitHandvorgabe(); |
| 49 | |
| 50 | for (const note of result.notes) { |
| 51 | expect(note.message).not.toContain('Alle Phasen erreichen ihre Höchstfreigabezeit'); |
| 52 | } |
| 53 | }); |
| 54 | |
| 55 | it('haelt die Hoechstfreigabezeit ein, solange eine andere Phase noch Spielraum hat', () => { |
| 56 | // Einstreifige Verkehrsfuehrung, feste Umlaufzeit 300 s, zwei Uebergaenge |
| 57 | // zu je 10 s -> 280 s verfuegbar; Vorgabe-Hoechstfreigabezeit 120 s. |
| 58 | const result = distributeGreenTimes( |
| 59 | 300, |
| 60 | [ |
| 61 | { id: 'p1', weight: 0.35, minGreen: 5, maxGreen: 120 }, |
| 62 | { id: 'p2', weight: 0, minGreen: 20, maxGreen: 120 }, |
| 63 | ], |
| 64 | [10, 10], |
| 65 | ); |
| 66 | |
| 67 | // Bis hierher: 260 s und 20 s - die Hoechstfreigabezeit war um 140 s |
| 68 | // ueberschritten, und die Fussgaengerphase bekam nichts. |
| 69 | expect(result.phases[1]!.green).toBe(120); |
| 70 | expect(result.phases[0]!.green).toBe(160); |
| 71 | const sum = result.phases.reduce((a, p) => a + p.green, 0); |
| 72 | expect(sum + result.transitionTime).toBe(300); |
| 73 | // Jetzt liegen tatsaechlich alle Phasen an ihrer Hoechstfreigabezeit; erst |
| 74 | // hier ist die Warnung am Platz. |
| 75 | const warnung = result.notes.find( |
| 76 | (n) => n.code === 'freigabezeit-rest' && n.severity === 'warnung', |
| 77 | ); |
| 78 | expect(warnung).toBeDefined(); |
| 79 | expect(warnung!.message).toContain('Alle Phasen erreichen ihre Höchstfreigabezeit'); |
| 80 | }); |
| 81 | |
| 82 | it('nennt die Phase, die die nicht nach Verkehrsstaerken vergebene Zeit erhaelt', () => { |
| 83 | const result = knotenpunktMitHandvorgabe(); |
| 84 | |
| 85 | // Seit der Fassung 5.9.0 traegt die Umlegung eine eigene Kennung: Unter |
| 86 | // 'freigabezeit-rest' standen zwei Sachverhalte, und der Bericht setzte |
| 87 | // ueber die Umlegung den Titel "Freigabezeit konnte nicht vollständig |
| 88 | // verteilt werden". Die Erwartung "Phase 2" bleibt unveraendert. |
| 89 | const hinweis = result.notes.find((n) => n.code === 'freigabezeit-rest-umgelegt'); |
| 90 | expect(hinweis).toBeDefined(); |
| 91 | expect(hinweis!.message).toContain('Phase 2'); |
| 92 | }); |
| 93 | |
| 94 | it('bleibt bei gebrochener Mindestfreigabezeit im Sekundenraster und haelt die Umlaufzeit ein', () => { |
| 95 | // Kfz-Phase mit Verkehrsstaerke, Fussgaengerphase mit eingetragener |
| 96 | // Mindestfreigabezeit 12,5 s; feste Umlaufzeit 60 s, Uebergangszeiten |
| 97 | // 2 s und 7 s -> 51 s verfuegbar. |
| 98 | const result = distributeGreenTimes( |
| 99 | 60, |
| 100 | [ |
| 101 | { id: 'p1', weight: 0.4, minGreen: 5 }, |
| 102 | { id: 'p2', weight: 0, minGreen: 12.5 }, |
| 103 | ], |
| 104 | [2, 7], |
| 105 | ); |
| 106 | |
| 107 | expect(result.feasible).toBe(true); |
| 108 | // Bis hierher: 39 s und 12,5 s - zusammen 51,5 s statt 51 s. Der Plan lief |
| 109 | // 60,5 s statt der eingetragenen 60 s, und 12,5 s sind nicht schaltbar. |
| 110 | for (const phase of result.phases) { |
| 111 | expect(Number.isInteger(phase.green)).toBe(true); |
| 112 | } |
| 113 | expect(result.phases[0]!.green).toBe(38); |
| 114 | expect(result.phases[1]!.green).toBe(13); |
| 115 | const sum = result.phases.reduce((a, p) => a + p.green, 0); |
| 116 | expect(sum).toBe(result.availableGreen); |
| 117 | expect(sum + result.transitionTime).toBe(60); |
| 118 | }); |
| 119 | |
| 120 | it('rundet eine gebrochene Mindestfreigabezeit einmal auf und nicht zweimal', () => { |
| 121 | // Vier Phasen, feste Umlaufzeit 76 s, vier Phasenuebergaenge zu je 5 s |
| 122 | // -> 56 s verfuegbar. Phase 1 ist eine Fussgaengerphase (Gewicht 0) mit |
| 123 | // eingetragener Mindestfreigabezeit 12,5 s, die drei Kfz-Phasen tragen je |
| 124 | // den Saettigungsgrad 0,3. |
| 125 | const result = distributeGreenTimes( |
| 126 | 76, |
| 127 | [ |
| 128 | { id: 'p1', weight: 0, minGreen: 12.5 }, |
| 129 | { id: 'p2', weight: 0.3, minGreen: 5 }, |
| 130 | { id: 'p3', weight: 0.3, minGreen: 5 }, |
| 131 | { id: 'p4', weight: 0.3, minGreen: 5 }, |
| 132 | ], |
| 133 | [5, 5, 5, 5], |
| 134 | ); |
| 135 | |
| 136 | // Die Phase steht nach dem Aufrunden bereits eine halbe Sekunde ueber |
| 137 | // ihrem Ausgangswert. Zaehlt der Summenausgleich sie trotzdem zu den |
| 138 | // Phasen mit dem groessten Rest, bekommt sie eine zweite Sekunde: Der |
| 139 | // Anwender traegt 12,5 s ein und liest 14 s, waehrend einer Kfz-Phase |
| 140 | // ohne Grund eine Sekunde fehlt. |
| 141 | expect(result.phases.map((p) => p.green)).toEqual([13, 15, 14, 14]); |
| 142 | const sum = result.phases.reduce((a, p) => a + p.green, 0); |
| 143 | expect(sum + result.transitionTime).toBe(76); |
| 144 | }); |
| 145 | |
| 146 | it('laesst eine aufgerundete Handvorgabe im Summenausgleich in Ruhe', () => { |
| 147 | // Fuenf Phasen, verfuegbar 47 s. Phase 1 traegt eine "Feste Freigabezeit" |
| 148 | // von 5,5 s (minGreen = maxGreen), die vier uebrigen je 0,3. |
| 149 | const result = distributeGreenTimes( |
| 150 | 47, |
| 151 | [ |
| 152 | { id: 'p1', weight: 0, minGreen: 5.5, maxGreen: 5.5 }, |
| 153 | { id: 'p2', weight: 0.3, minGreen: 5 }, |
| 154 | { id: 'p3', weight: 0.3, minGreen: 5 }, |
| 155 | { id: 'p4', weight: 0.3, minGreen: 5 }, |
| 156 | { id: 'p5', weight: 0.3, minGreen: 5 }, |
| 157 | ], |
| 158 | [], |
| 159 | ); |
| 160 | |
| 161 | // 5,5 s sind nicht schaltbar und werden zu 6 s. Eine zweite Sekunde |
| 162 | // obendrauf - 7 s bei eingetragenen 5,5 s - waere kein Sekundenraster |
| 163 | // mehr, sondern eine Aufstockung. |
| 164 | expect(result.phases.map((p) => p.green)).toEqual([6, 11, 10, 10, 10]); |
| 165 | }); |
| 166 | |
| 167 | it('meldet eine zu kurze Umlaufzeit, wenn gebrochene Vorgaben im Sekundenraster nicht hineinpassen', () => { |
| 168 | // Zwei feste Freigabezeiten 20,5 s und 19,5 s bei 60 s Umlauf und 20 s |
| 169 | // Uebergangszeit. Im Sekundenraster braucht das 21 s und 20 s, also 41 s - |
| 170 | // 1 s mehr, als verfuegbar ist. |
| 171 | const result = distributeGreenTimes( |
| 172 | 60, |
| 173 | [ |
| 174 | { id: 'p1', weight: 0.4, minGreen: 20.5, maxGreen: 20.5 }, |
| 175 | { id: 'p2', weight: 0.2, minGreen: 19.5, maxGreen: 19.5 }, |
| 176 | ], |
| 177 | [10, 10], |
| 178 | ); |
| 179 | |
| 180 | // Bis hierher: 21 s und 19,5 s, zusammen 40,5 s - der Plan lief still 60,5 s. |
| 181 | expect(result.feasible).toBe(false); |
| 182 | expect(result.notes.some((n) => n.code === 'umlaufzeit-zu-kurz')).toBe(true); |
| 183 | for (const phase of result.phases) { |
| 184 | expect(Number.isInteger(phase.green)).toBe(true); |
| 185 | } |
| 186 | }); |
| 187 | }); |