lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests domain kennwertquellen.test.ts
| 1 | import { describe, expect, it } from 'vitest'; |
| 2 | import { RILSA_DEFAULTS } from '@/domain/rilsa/constants'; |
| 3 | import { computeCycleTime } from '@/domain/rilsa/cycle'; |
| 4 | import { distributeGreenTimes } from '@/domain/rilsa/greenSplit'; |
| 5 | import { QUELLEN, type QuellenSchluessel } from '@/domain/rilsa/quellen'; |
| 6 | import { settingFields } from '@/domain/rilsa/settings'; |
| 7 | import type { RilsaOverrides } from '@/domain/rilsa/settings'; |
| 8 | import { |
| 9 | createPhase, |
| 10 | createSignalGroup, |
| 11 | createStandardIntersectionProject, |
| 12 | } from '@/domain/model/factory'; |
| 13 | import { buildSignalPlan } from '@/domain/plan/signalPlan'; |
| 14 | import type { SignalPlan } from '@/domain/plan/signalPlan'; |
| 15 | import type { Project } from '@/domain/model/project'; |
| 16 | |
| 17 | /** |
| 18 | * Nachbarfaelle zu Befund 19, behoben in Fassung 5.12.0. |
| 19 | * |
| 20 | * Befund 19 hat Ziel- und Hoechst-Auslastungsgrad aus der Sammelquelle |
| 21 | * 'praxis' herausgeloest, weil deren gedruckter Hinweis - "Frei wählbar; wirkt |
| 22 | * sich nicht auf die Einhaltung des Regelwerks aus" - fuer sie nicht zutrifft. |
| 23 | * Der Satz steht als "Zu beachten" im Fundstellenverzeichnis der |
| 24 | * Planunterlage. Drei Vorgabenfelder blieben an der Sammelquelle haengen; fuer |
| 25 | * alle drei ist er nachgemessen falsch: |
| 26 | * |
| 27 | * A `minGreenOepnv` - geht ueber die Mindestfreigabezeit der Phase in die |
| 28 | * Mindestumlaufzeit ein; die Umlaufzeit ist an |
| 29 | * Regelwerksgrenzen gebunden. |
| 30 | * B `cycleTimeStep` - `ceilToStep` laeuft vor der Kappung auf den |
| 31 | * Hoechstwert; die Rasterung entscheidet damit allein |
| 32 | * darueber, ob die Ueberschreitung gemeldet wird. |
| 33 | * C `maxGreen` - kappt eine Phase, verschiebt deren Zeit auf die |
| 34 | * uebrigen und veraendert darueber Kapazitaet, |
| 35 | * Auslastungsgrad und Qualitaetsstufe nach HBS 2015. |
| 36 | * |
| 37 | * C war als "kein Treffer" gemeldet, mit der Begruendung, die |
| 38 | * Mindestfreigabezeit gehe immer vor und der Wert gehe nicht in die |
| 39 | * Umlaufzeit ein. Beides trifft zu und ist unten mitgemessen; der Schluss |
| 40 | * daraus traegt nicht. |
| 41 | * |
| 42 | * Der letzte Fall der Datei bewacht, dass der neue Hinweis zu C nicht seinerseits |
| 43 | * eine Meldung zusagt, die es fuer das Vorgabenfeld nicht gibt. |
| 44 | */ |
| 45 | |
| 46 | const SATZ_DER_WIRKUNGSLOSIGKEIT = 'wirkt sich nicht auf die Einhaltung des Regelwerks aus'; |
| 47 | |
| 48 | /** Die drei Felder, um die es geht - mit der Fundstelle, die sie tragen sollen. */ |
| 49 | const FELDER = [ |
| 50 | { key: 'minGreenOepnv', schluessel: 'praxis-mindestfreigabezeit-oepnv' }, |
| 51 | { key: 'maxGreen', schluessel: 'praxis-hoechstfreigabezeit' }, |
| 52 | { key: 'cycleTimeStep', schluessel: 'praxis-umlaufzeit-rasterung' }, |
| 53 | ] as const satisfies readonly { key: keyof RilsaOverrides; schluessel: QuellenSchluessel }[]; |
| 54 | |
| 55 | function feld(key: keyof RilsaOverrides) { |
| 56 | const gefunden = settingFields().find((f) => f.key === key); |
| 57 | expect(gefunden, `Vorgabenfeld "${key}"`).toBeDefined(); |
| 58 | return gefunden!; |
| 59 | } |
| 60 | |
| 61 | describe('Die drei letzten Felder der Sammelquelle "praxis"', () => { |
| 62 | it('haengt keines der drei Felder mehr an der Sammelquelle', () => { |
| 63 | for (const { key } of FELDER) { |
| 64 | expect(feld(key).quellenSchluessel, key).not.toBe('praxis'); |
| 65 | } |
| 66 | }); |
| 67 | |
| 68 | it('gibt jedem der drei eine eigene Fundstelle - der Hinweissatz trifft nicht auf alle drei zu', () => { |
| 69 | for (const { key, schluessel } of FELDER) { |
| 70 | expect(feld(key).quellenSchluessel, key).toBe(schluessel); |
| 71 | } |
| 72 | const schluessel = new Set(FELDER.map((f) => feld(f.key).quellenSchluessel)); |
| 73 | expect(schluessel.size, 'drei verschiedene Fundstellen').toBe(FELDER.length); |
| 74 | }); |
| 75 | |
| 76 | it('druckt zu keinem der drei Felder den Satz von der Wirkungslosigkeit', () => { |
| 77 | for (const { key } of FELDER) { |
| 78 | expect(feld(key).quelle.hinweis ?? '', key).not.toContain(SATZ_DER_WIRKUNGSLOSIGKEIT); |
| 79 | expect(feld(key).quelle.verwendung, key).not.toContain(SATZ_DER_WIRKUNGSLOSIGKEIT); |
| 80 | } |
| 81 | }); |
| 82 | |
| 83 | it('nennt in jeder Fundstelle, worauf der Wert wirkt', () => { |
| 84 | // A und B wirken auf die Umlaufzeit, C auf die Bewertung nach HBS 2015. |
| 85 | expect(feld('minGreenOepnv').quelle.verwendung).toContain('Mindestumlaufzeit'); |
| 86 | expect(feld('cycleTimeStep').quelle.verwendung).toContain('Umlaufzeit'); |
| 87 | expect(feld('maxGreen').quelle.verwendung).toContain('HBS 2015'); |
| 88 | for (const { key } of FELDER) { |
| 89 | expect(feld(key).quelle.hinweis ?? '', `${key}: Beleglage`).toContain('Kein Regelwerkswert'); |
| 90 | } |
| 91 | }); |
| 92 | |
| 93 | it('fuehrt alle drei als selbstgesetzte Schwelle, nie als gesichert', () => { |
| 94 | for (const { key } of FELDER) { |
| 95 | // Grundsatz: selbstgesetzte Schwellen tragen nie "gesichert". |
| 96 | expect(feld(key).quelle.pruefstand, key).not.toBe('gesichert'); |
| 97 | expect(feld(key).quelle.regelwerk, key).not.toContain('RiLSA'); |
| 98 | expect(feld(key).quelle.regelwerk, key).not.toContain('HBS'); |
| 99 | } |
| 100 | }); |
| 101 | |
| 102 | it('bleibt in Kurzform bei der Planungspraxis - die Spalte des Verzeichnisses aendert sich nicht', () => { |
| 103 | for (const { key } of FELDER) { |
| 104 | expect(feld(key).quelle.kurz, key).toBe('Planungspraxis'); |
| 105 | expect(feld(key).quelle.regelwerk, key).toBe('Planungspraxis'); |
| 106 | } |
| 107 | }); |
| 108 | |
| 109 | /* |
| 110 | * Die Sammelquelle bleibt stehen - sie benennt den Massstab, an dem die drei |
| 111 | * Trennungen gemessen wurden. Sie traegt jetzt aber kein Vorgabenfeld mehr |
| 112 | * und erscheint damit in keinem Verzeichnis; gedruckt wird ihr Satz |
| 113 | * nirgends. |
| 114 | */ |
| 115 | it('laesst den Satz an der Sammelquelle stehen, die aber kein Vorgabenfeld mehr traegt', () => { |
| 116 | expect(QUELLEN.praxis.hinweis ?? '').toContain(SATZ_DER_WIRKUNGSLOSIGKEIT); |
| 117 | expect(settingFields().filter((f) => f.quellenSchluessel === 'praxis')).toHaveLength(0); |
| 118 | }); |
| 119 | }); |
| 120 | |
| 121 | describe('Die Wirkung, die der Satz der Sammelquelle bestritt', () => { |
| 122 | /* |
| 123 | * A - die Mindestfreigabezeit des OePNV. Zweimal gemessen: einmal am |
| 124 | * fertigen Plan, damit die Kette Vorgabe -> Phase -> Mindestumlaufzeit |
| 125 | * belegt ist, und einmal an der Umlaufzeitrechnung selbst, damit die |
| 126 | * Regelwerksgrenze erreicht wird. |
| 127 | */ |
| 128 | const DATUM = new Date('2026-01-01T00:00:00Z'); |
| 129 | |
| 130 | function projektMitOepnv(minGreenOepnv?: number): Project { |
| 131 | const basis = createStandardIntersectionProject('Prüffall', DATUM); |
| 132 | const bahn = createSignalGroup({ |
| 133 | name: 'B1', |
| 134 | mode: 'oepnv', |
| 135 | armId: basis.intersection.arms[0]?.id ?? null, |
| 136 | index: 6, |
| 137 | }); |
| 138 | return { |
| 139 | ...basis, |
| 140 | signalGroups: [...basis.signalGroups, bahn], |
| 141 | phases: [...basis.phases, createPhase('Phase 3 - ÖPNV', [bahn.id])], |
| 142 | settings: { |
| 143 | ...basis.settings, |
| 144 | rilsa: |
| 145 | minGreenOepnv === undefined |
| 146 | ? basis.settings.rilsa |
| 147 | : { ...basis.settings.rilsa, minGreenOepnv }, |
| 148 | }, |
| 149 | }; |
| 150 | } |
| 151 | |
| 152 | it('verlaengert die Mindestfreigabezeit des OePNV die Umlaufzeit des fertigen Plans', () => { |
| 153 | expect(RILSA_DEFAULTS.minGreen.oepnv, 'Regelwert').toBe(3); |
| 154 | const mitRegelwert = buildSignalPlan(projektMitOepnv()); |
| 155 | const mitVorgabe = buildSignalPlan(projektMitOepnv(60)); |
| 156 | expect(mitRegelwert.cycleTime, 'Umlaufzeit mit 3 s').toBe(65); |
| 157 | expect(mitVorgabe.cycleTime, 'Umlaufzeit mit 60 s').toBe(80); |
| 158 | expect(mitRegelwert.notes.map((n) => n.code)).not.toContain('mindestumlauf-massgebend'); |
| 159 | expect(mitVorgabe.notes.map((n) => n.code)).toContain('mindestumlauf-massgebend'); |
| 160 | }); |
| 161 | |
| 162 | it('treibt sie die Mindestumlaufzeit bis ueber den Hoechstwert der RiLSA', () => { |
| 163 | // Phasenfolge mit 5 + 25 s Mindestfreigabe und 35 s Uebergangszeiten; |
| 164 | // dazwischen die Phase des Sondersignals. |
| 165 | const eingang = (minGreenOepnv: number) => ({ |
| 166 | lostTime: 20, |
| 167 | criticalFlowRatios: [0.2, 0.2], |
| 168 | minimumCycle: 5 + 25 + minGreenOepnv + 35, |
| 169 | step: RILSA_DEFAULTS.cycleTime.step, |
| 170 | }); |
| 171 | const klein = computeCycleTime('webster', eingang(3)); |
| 172 | const gross = computeCycleTime('webster', eingang(60)); |
| 173 | expect(klein.cycleTime, 'Umlaufzeit bei 3 s').toBe(70); |
| 174 | expect(klein.notes.map((n) => n.code)).not.toContain('umlaufzeit-ueber-hoechstwert'); |
| 175 | expect(gross.bounded, 'Zustand bei 60 s').toBe('maximum'); |
| 176 | expect(gross.cycleTime).toBe(RILSA_DEFAULTS.cycleTime.max); |
| 177 | expect(gross.notes.map((n) => n.code)).toContain('umlaufzeit-ueber-hoechstwert'); |
| 178 | }); |
| 179 | |
| 180 | /* |
| 181 | * B - die Rasterung. Derselbe rechnerische Wert, dieselbe ausgegebene |
| 182 | * Umlaufzeit - und seit Fassung 5.13.0 auch dieselbe Beurteilung. |
| 183 | * |
| 184 | * GEAENDERT (Fassung 5.13.0). Dieser Fall hielt bis dahin fest, dass die |
| 185 | * Rasterung allein ueber die Beanstandung entscheidet: bei 7 und 9 s der |
| 186 | * Fehler 'umlaufzeit-ueber-hoechstwert', bei den uebrigen acht Rasterungen |
| 187 | * keiner. Das war eine Messung des Zustands, nicht seine Billigung - der |
| 188 | * Kopfkommentar der Fundstelle sagte ausdruecklich, die Reihenfolge in |
| 189 | * cycle.ts sei damit nicht entschieden. |
| 190 | * |
| 191 | * Sie ist es jetzt: Beurteilt wird der erforderliche Umlauf VOR der |
| 192 | * Rasterung. Der Fall misst weiterhin, was die Fundstelle behauptet, nur |
| 193 | * eben den entschiedenen Zustand - die Wirkung der Rasterung auf die |
| 194 | * Umlaufzeit selbst steht im Fall darunter. Kein Abschwaechen: Die |
| 195 | * Messreihe ueber alle zehn Rasterungen bleibt, und die Gegenprobe mit |
| 196 | * einem wirklich zu langen Umlauf kommt hinzu. Die ausfuehrliche Wache |
| 197 | * dazu steht in `umlaufzeitRasterung.test.ts`. |
| 198 | */ |
| 199 | it('entscheidet die Rasterung nicht mehr, ob die Ueberschreitung des Hoechstwerts gemeldet wird', () => { |
| 200 | // (1,5 * L + 5) / (1 - Y) = 119,48 s bei Y = 0,5. |
| 201 | const Y = 0.5; |
| 202 | const eingang = (step: number, raw = 119.48) => ({ |
| 203 | lostTime: (raw * (1 - Y) - 5) / 1.5, |
| 204 | criticalFlowRatios: [Y], |
| 205 | minimumCycle: 30, |
| 206 | step, |
| 207 | }); |
| 208 | const teilend = computeCycleTime('webster', eingang(5)); |
| 209 | const nichtTeilend = computeCycleTime('webster', eingang(7)); |
| 210 | |
| 211 | expect(teilend.raw, 'rechnerischer Wert').toBeCloseTo(119.48, 2); |
| 212 | expect(nichtTeilend.raw, 'derselbe rechnerische Wert').toBeCloseTo(119.48, 2); |
| 213 | // Dieselbe Umlaufzeit - und in beiden Faellen kein Fehler im Bericht. |
| 214 | expect(teilend.cycleTime).toBe(120); |
| 215 | expect(nichtTeilend.cycleTime).toBe(120); |
| 216 | expect(teilend.bounded).toBe('keine'); |
| 217 | expect(nichtTeilend.bounded).toBe('keine'); |
| 218 | const meldend = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].filter((step) => |
| 219 | computeCycleTime('webster', eingang(step)) |
| 220 | .notes.map((n) => n.code) |
| 221 | .includes('umlaufzeit-ueber-hoechstwert'), |
| 222 | ); |
| 223 | expect(meldend, 'keine Rasterung des Feldbereichs meldet').toEqual([]); |
| 224 | |
| 225 | // Gegenprobe: Ein erforderlicher Umlauf ueber dem Hoechstwert wird bei |
| 226 | // jeder Rasterung gemeldet - auch bei einer, die ihn teilt. |
| 227 | const zuLang = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].filter((step) => |
| 228 | computeCycleTime('webster', eingang(step, 121)) |
| 229 | .notes.map((n) => n.code) |
| 230 | .includes('umlaufzeit-ueber-hoechstwert'), |
| 231 | ); |
| 232 | expect(zuLang).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); |
| 233 | }); |
| 234 | |
| 235 | it('wirkt die Rasterung weiterhin auf die Umlaufzeit selbst', () => { |
| 236 | /* |
| 237 | * Der Grund, aus dem dieses Feld eine eigene Fundstelle hat, bleibt |
| 238 | * bestehen: Die Rasterung ist keine wirkungslose Anzeigegroesse. An der |
| 239 | * Standardvorlage (rechnerisch 88,07 s) ergibt sie bei 5 s eine |
| 240 | * Umlaufzeit von 90 s mit den Freigabezeiten 42/28 s, bei 1 s eine von |
| 241 | * 89 s mit 42/27 s. |
| 242 | */ |
| 243 | const basis = createStandardIntersectionProject('Prüffall', DATUM); |
| 244 | const mitRaster = (cycleTimeStep: number): Project => ({ |
| 245 | ...basis, |
| 246 | settings: { ...basis.settings, rilsa: { ...basis.settings.rilsa, cycleTimeStep } }, |
| 247 | }); |
| 248 | const fuenf = buildSignalPlan(mitRaster(5)); |
| 249 | const eins = buildSignalPlan(mitRaster(1)); |
| 250 | expect(fuenf.cycleResult?.raw, 'derselbe rechnerische Wert').toBeCloseTo(88.07, 2); |
| 251 | expect(eins.cycleResult?.raw, 'derselbe rechnerische Wert').toBeCloseTo(88.07, 2); |
| 252 | expect(fuenf.cycleTime).toBe(90); |
| 253 | expect(eins.cycleTime).toBe(89); |
| 254 | expect(fuenf.phases.map((p) => p.duration)).toEqual([42, 28]); |
| 255 | expect(eins.phases.map((p) => p.duration)).toEqual([42, 27]); |
| 256 | }); |
| 257 | |
| 258 | /* |
| 259 | * C - die Hoechstfreigabezeit. Zuerst die beiden Feststellungen, die der |
| 260 | * gemeldete Nachbarfall richtig getroffen hat, dann der Schluss, der nicht |
| 261 | * traegt. |
| 262 | */ |
| 263 | it('geht die Mindestfreigabezeit der Hoechstfreigabezeit vor', () => { |
| 264 | const ergebnis = distributeGreenTimes( |
| 265 | 90, |
| 266 | [ |
| 267 | { id: 'furt', weight: 0.4, minGreen: 25, maxGreen: 10 }, |
| 268 | { id: 'kfz', weight: 0.4, minGreen: 5, maxGreen: 120 }, |
| 269 | ], |
| 270 | [10, 10], |
| 271 | ); |
| 272 | expect(ergebnis.phases.find((p) => p.id === 'furt')?.green, 'Furtphase').toBe(25); |
| 273 | }); |
| 274 | |
| 275 | it('aendert die Hoechstfreigabezeit die Bewertung nach HBS 2015', () => { |
| 276 | const basis = createStandardIntersectionProject('Prüffall', DATUM); |
| 277 | const mit = (maxGreen: number): Project => ({ |
| 278 | ...basis, |
| 279 | settings: { ...basis.settings, rilsa: { ...basis.settings.rilsa, maxGreen } }, |
| 280 | }); |
| 281 | |
| 282 | expect(RILSA_DEFAULTS.maxGreen, 'Regelwert').toBe(120); |
| 283 | const regelwert = buildSignalPlan(basis); |
| 284 | const gekappt = buildSignalPlan(mit(10)); |
| 285 | expect(regelwert.cycleTime, 'Umlaufzeit unveraendert').toBe(90); |
| 286 | expect(gekappt.cycleTime, 'Umlaufzeit unveraendert').toBe(90); |
| 287 | expect(regelwert.phases.map((p) => p.duration)).toEqual([42, 28]); |
| 288 | /* |
| 289 | * NACHGEZOGEN AM 10.09.2026 (Fassung 5.23.0): Hier stand [60, 10]. Der |
| 290 | * Rest, den die gekappte Hoechstfreigabezeit uebrig laesst, ging bis dahin |
| 291 | * ganz an die Phase mit dem groessten Gewicht; jetzt wird er nach |
| 292 | * Gewichten auf die Phasen mit Spielraum verteilt. Die Aussage dieses |
| 293 | * Falls - die Hoechstfreigabezeit aendert die Freigabezeiten und damit die |
| 294 | * Bewertung - traegt unveraendert, nur die Aufteilung ist eine andere. |
| 295 | */ |
| 296 | expect(gekappt.phases.map((p) => p.duration)).toEqual([40, 30]); |
| 297 | |
| 298 | /* |
| 299 | * NACHGEZOGEN AM 10.09.2026 (Fassung 5.23.0). Hier stand die Ueberlastung: |
| 300 | * Mit gekappter Hoechstfreigabezeit fiel die zweite Phase auf 10 s, und |
| 301 | * `leistungsfaehigkeit.ueberlastet` stand als Fehler im Bericht. Seit der |
| 302 | * Rest nach Gewichten auf die Phasen mit Spielraum verteilt wird, bekommt |
| 303 | * sie 30 s statt 10 s, und diese Ueberlastung entsteht nicht mehr - das |
| 304 | * ist die Absicht der Aenderung und kein Verlust. |
| 305 | * |
| 306 | * PUNKT C BRAUCHT SIE AUCH NICHT. Behauptet ist, die Hoechstfreigabezeit |
| 307 | * veraendere Kapazitaet, Auslastungsgrad und Qualitaetsstufe nach HBS |
| 308 | * 2015; alle drei sind unmittelbar messbar und haengen an keiner Schwelle. |
| 309 | * Das ist die staerkere Wache: Ein Fehler im Bericht setzt voraus, dass |
| 310 | * die Wirkung gerade eine Grenze ueberschreitet, die Wirkung selbst nicht. |
| 311 | */ |
| 312 | const kfz = (plan: SignalPlan, name: string) => { |
| 313 | const gruppe = plan.groups.find((g) => g.name === name); |
| 314 | expect(gruppe, `Signalgruppe ${name}`).toBeDefined(); |
| 315 | return gruppe!; |
| 316 | }; |
| 317 | |
| 318 | expect(kfz(regelwert, 'K1').capacity?.capacity, 'Kapazität mit dem Regelwert').toBe(914); |
| 319 | expect(kfz(gekappt, 'K1').capacity?.capacity, 'Kapazität mit 10 s').toBe(872); |
| 320 | expect( |
| 321 | kfz(regelwert, 'K1').capacity?.degreeOfSaturation, |
| 322 | 'Auslastungsgrad mit dem Regelwert', |
| 323 | ).toBeCloseTo(0.678, 3); |
| 324 | expect(kfz(gekappt, 'K1').capacity?.degreeOfSaturation, 'Auslastungsgrad mit 10 s').toBeCloseTo( |
| 325 | 0.711, |
| 326 | 3, |
| 327 | ); |
| 328 | /* |
| 329 | * Und die Bewertung: die mittlere Wartezeit der ersten Fussgaengerfurt, |
| 330 | * aus der die Qualitaetsstufe folgt. |
| 331 | * |
| 332 | * NACHGEZOGEN MIT FASSUNG 5.43.0: Hier stand der Wechsel ihrer Stufe von B |
| 333 | * auf C. Die Vorlage erfasst seither F1 auch gegen K4, der die Furt beim |
| 334 | * Ausfahren quert, und rechnet den einfahrenden Fussgaenger mit te = 0 s. |
| 335 | * F1 wartet dadurch schon mit dem Regelwert 10,8 s und steht auf C; die |
| 336 | * gekappte Hoechstfreigabezeit verlaengert die Wartezeit weiter, |
| 337 | * ueberschreitet aber keine Stufengrenze mehr. Gemessen wird deshalb die |
| 338 | * Groesse, die an keiner Schwelle haengt. |
| 339 | */ |
| 340 | expect(kfz(regelwert, 'F1').delay?.averageDelay, 'Wartezeit mit dem Regelwert').toBe(10.8); |
| 341 | expect(kfz(gekappt, 'F1').delay?.averageDelay, 'Wartezeit mit 10 s').toBe(11.8); |
| 342 | }); |
| 343 | |
| 344 | /* |
| 345 | * NACHTRAG (Fassung 5.12.0): Der gedruckte Hinweis dieser Fundstelle |
| 346 | * schloss zunaechst mit dem pauschalen Satz, eine Hoechstfreigabezeit unter |
| 347 | * der Mindestfreigabezeit sei "nicht schaltbar und wird als wirkungslos |
| 348 | * gemeldet". Gedruckt wird er im Fundstellenverzeichnis unter dem |
| 349 | * VORGABENFELD - und fuer das galt die zweite Haelfte nicht: phaseMaxGreen |
| 350 | * kehrte ohne Meldung zurueck, wenn keine Signalgruppe eine kleinere |
| 351 | * Schranke trug. Damit stand unter einem Kennwert wieder ein Satz, der ihn |
| 352 | * falsch beschreibt: genau der Fehlertyp, gegen den diese |
| 353 | * Datei steht. |
| 354 | * |
| 355 | * GEAENDERT (Fassung 5.13.0): Der Hinweis beschrieb damit einen Zustand, der |
| 356 | * als gemeldet, aber unentschieden festgehalten war. Entschieden ist er jetzt |
| 357 | * zugunsten der Meldung - eine wirkungslose Eingabe, von der niemand |
| 358 | * erfaehrt, bleibt sonst stehen. Der Fall misst weiterhin beide Lagen und |
| 359 | * bindet den Wortlaut daran; neu ist, dass die Vorgabe gemeldet WIRD. Die |
| 360 | * ausfuehrliche Wache dazu steht in |
| 361 | * `hoechstfreigabezeitUnterMindestfreigabezeit.test.ts`. |
| 362 | */ |
| 363 | it('meldet auch eine Projektvorgabe unter der Mindestfreigabezeit als wirkungslos - einmal fuer die Anlage', () => { |
| 364 | const basis = createStandardIntersectionProject('Prüffall', DATUM); |
| 365 | // Beide Werte liegen im Feldbereich der Vorgabenverwaltung |
| 366 | // (Mindestfreigabezeiten bis 60 s, Hoechstfreigabezeit ab 10 s). |
| 367 | const plan = buildSignalPlan({ |
| 368 | ...basis, |
| 369 | settings: { |
| 370 | ...basis.settings, |
| 371 | rilsa: { ...basis.settings.rilsa, minGreenKfz: 40, maxGreen: 10 }, |
| 372 | }, |
| 373 | }); |
| 374 | expect(plan.cycleTime, 'Umlaufzeit').toBe(100); |
| 375 | expect( |
| 376 | plan.phases.map((p) => p.duration), |
| 377 | 'Freigabezeiten', |
| 378 | ).toEqual([40, 40]); |
| 379 | expect( |
| 380 | plan.notes.map((n) => n.code), |
| 381 | 'Meldungen des Plans', |
| 382 | ).toEqual([ |
| 383 | 'hoechstfreigabezeit-vorgabe-unter-mindestfreigabezeit', |
| 384 | 'mindestumlauf-massgebend', |
| 385 | ]); |
| 386 | expect( |
| 387 | plan.notes.find((n) => n.code === 'hoechstfreigabezeit-vorgabe-unter-mindestfreigabezeit') |
| 388 | ?.signalGroupId, |
| 389 | 'die Zahl steht an keiner Signalgruppe', |
| 390 | ).toBeUndefined(); |
| 391 | |
| 392 | // Dieselbe Schranke je Signalgruppe eingetragen: dort haengt die Meldung |
| 393 | // an der Gruppe. |
| 394 | const jeGruppe = buildSignalPlan({ |
| 395 | ...basis, |
| 396 | signalGroups: basis.signalGroups.map((g, i) => (i === 0 ? { ...g, maxGreenOverride: 2 } : g)), |
| 397 | }); |
| 398 | expect(jeGruppe.notes.map((n) => n.code)).toContain( |
| 399 | 'hoechstfreigabezeit-unter-mindestfreigabezeit', |
| 400 | ); |
| 401 | |
| 402 | // Der gedruckte Hinweis der Fundstelle sagt genau diese Lage - und nicht mehr. |
| 403 | const hinweis = feld('maxGreen').quelle.hinweis ?? ''; |
| 404 | expect(hinweis).toContain('eine je Signalgruppe eingetragene Höchstfreigabezeit'); |
| 405 | expect(hinweis).toContain('einmal für die Anlage'); |
| 406 | expect(hinweis, 'der alte, unentschiedene Zustand').not.toContain('ohne Beanstandung'); |
| 407 | }); |
| 408 | }); |