lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests domain verkehrsstaerkeNull.test.ts
| 1 | import { describe, expect, it } from 'vitest'; |
| 2 | import { buildSignalPlan } from '@/domain/plan/signalPlan'; |
| 3 | import { validateProject } from '@/domain/validation/engine'; |
| 4 | import { createStandardIntersectionProject } from '@/domain/model/factory'; |
| 5 | import * as actions from '@/app/actions'; |
| 6 | import type { Project } from '@/domain/model/project'; |
| 7 | |
| 8 | /* |
| 9 | * Befunde 12 und 18, behoben in Fassung 5.10.0 - ein Eintrag ohne |
| 10 | * Verkehrsstaerke galt als gezaehlt. |
| 11 | * |
| 12 | * `setDemand` und `setLastzugAnteil` legen bei fehlendem Datensatz einen mit |
| 13 | * `volume: 0` an - das geschieht schon, wenn in der Signalgruppentabelle nur |
| 14 | * die Spalte "SV-Anteil" oder "Lastzuege am SV" ausgefuellt wird. Der Fachkern |
| 15 | * entschied an zwei Stellen allein am VORHANDENSEIN des Datensatzes: |
| 16 | * - `phaseFlowRatio` setzte `erfasst = true` und lieferte 0 statt |
| 17 | * `undefined`; die Luecke 'saettigungsgrade-unvollstaendig' entstand nicht. |
| 18 | * - `computeCapacity` bekam die 0 als Nachfrage und lieferte einen gesetzten |
| 19 | * `degreeOfSaturation`; damit fiel auch |
| 20 | * 'leistungsfaehigkeit.keine-verkehrsstaerken' weg. |
| 21 | * Y blieb trotzdem zu klein, die Umlaufzeit fiel zu kurz aus - die gefaehrliche |
| 22 | * Richtung, und im teilweise erfassten Fall ohne jede Meldung. |
| 23 | * |
| 24 | * Massstab ist das Kriterium, das der Fachkern sonst durchgaengig fuehrt |
| 25 | * (rules.ts, simulation.ts, naechsterSchritt.ts): erfasst ist eine |
| 26 | * Verkehrsstaerke nur bei `Number.isFinite(volume) && volume > 0`. |
| 27 | * |
| 28 | * GEGEN DEN ALTSTAND schlagen die ersten fuenf Faelle fehl. Der sechste ist die |
| 29 | * Gegenprobe mit einer echten Verkehrsstaerke und besteht auch dort. |
| 30 | */ |
| 31 | |
| 32 | const DATUM = new Date('2026-01-01T00:00:00Z'); |
| 33 | const LUECKE = 'saettigungsgrade-unvollstaendig'; |
| 34 | const REGEL = 'leistungsfaehigkeit.keine-verkehrsstaerken'; |
| 35 | |
| 36 | /** Standardknotenpunkt, dem die Verkehrsstaerken der zweiten Phase fehlen. */ |
| 37 | function ohneVerkehrsstaerkenInPhase2(): Project { |
| 38 | const roh = createStandardIntersectionProject('Teilweise erfasst', DATUM); |
| 39 | const k3 = roh.signalGroups.find((g) => g.name === 'K3')!; |
| 40 | const k4 = roh.signalGroups.find((g) => g.name === 'K4')!; |
| 41 | return { |
| 42 | ...roh, |
| 43 | demands: roh.demands.filter((d) => d.signalGroupId !== k3.id && d.signalGroupId !== k4.id), |
| 44 | }; |
| 45 | } |
| 46 | |
| 47 | /** Derselbe Knotenpunkt ohne jede Verkehrsstaerke. */ |
| 48 | function ohneJedeVerkehrsstaerke(): Project { |
| 49 | return { ...createStandardIntersectionProject('Ohne Zaehlung', DATUM), demands: [] }; |
| 50 | } |
| 51 | |
| 52 | describe('Befunde 12/18 - ein Eintrag mit der Verkehrsstaerke 0 ist keine Zaehlung', () => { |
| 53 | it('meldet die Luecke weiter, wenn nur der Schwerverkehrsanteil eingetragen wird', () => { |
| 54 | const basis = ohneVerkehrsstaerkenInPhase2(); |
| 55 | const k3 = basis.signalGroups.find((g) => g.name === 'K3')!; |
| 56 | |
| 57 | // Genau der Bedienweg: Spalte "SV-Anteil" ausfuellen, Spalte |
| 58 | // "Verkehrsstaerke" leer lassen. |
| 59 | const mitAnteil = actions.setDemand(basis, k3.id, { heavyVehicleShare: 0.05 }); |
| 60 | expect(mitAnteil.demands.some((d) => d.signalGroupId === k3.id && d.volume === 0)).toBe(true); |
| 61 | |
| 62 | const notiz = buildSignalPlan(mitAnteil).notes.find((n) => n.code === LUECKE); |
| 63 | expect(notiz).toBeDefined(); |
| 64 | expect(notiz?.message).toContain('Phase 2'); |
| 65 | }); |
| 66 | |
| 67 | it('meldet die Luecke weiter, wenn nur der Lastzuganteil eingetragen wird', () => { |
| 68 | const basis = ohneVerkehrsstaerkenInPhase2(); |
| 69 | const k3 = basis.signalGroups.find((g) => g.name === 'K3')!; |
| 70 | |
| 71 | const mitAnteil = actions.setLastzugAnteil(basis, k3.id, 0.3); |
| 72 | |
| 73 | expect(buildSignalPlan(mitAnteil).notes.some((n) => n.code === LUECKE)).toBe(true); |
| 74 | }); |
| 75 | |
| 76 | it('bildet ohne Verkehrsstaerke keinen Auslastungsgrad', () => { |
| 77 | const basis = ohneVerkehrsstaerkenInPhase2(); |
| 78 | const k3 = basis.signalGroups.find((g) => g.name === 'K3')!; |
| 79 | const plan = buildSignalPlan(actions.setDemand(basis, k3.id, { heavyVehicleShare: 0.05 })); |
| 80 | |
| 81 | const geplant = plan.groups.find((g) => g.groupId === k3.id)!; |
| 82 | // Ein aus dem Nichts gebildeter Auslastungsgrad 0,00 ist die unsichere |
| 83 | // Aussage: Er stellt die Zufahrt als voellig unbelastet dar. |
| 84 | expect(geplant.capacity?.degreeOfSaturation).toBeUndefined(); |
| 85 | expect(geplant.flowRatio).toBeNull(); |
| 86 | }); |
| 87 | |
| 88 | it('haelt die Warnung "Keine Verkehrsstaerken erfasst" aufrecht', () => { |
| 89 | let projekt = ohneJedeVerkehrsstaerke(); |
| 90 | for (const gruppe of projekt.signalGroups.filter((g) => g.mode === 'kfz')) { |
| 91 | projekt = actions.setDemand(projekt, gruppe.id, { heavyVehicleShare: 0.05 }); |
| 92 | } |
| 93 | |
| 94 | const bericht = validateProject(projekt, buildSignalPlan(projekt)); |
| 95 | expect(bericht.findings.some((f) => f.rule === REGEL)).toBe(true); |
| 96 | }); |
| 97 | |
| 98 | it('behandelt eine von Hand eingetragene 0 genauso', () => { |
| 99 | // Das Programm kann "gezaehlt und 0 Fz/h" von "nicht erfasst" nicht |
| 100 | // unterscheiden - beide Wege erzeugen denselben Datensatz. Eine |
| 101 | // massgebende Kfz-Verkehrsstaerke von 0 ist kein Bemessungsfall; im |
| 102 | // Zweifel nach der Zaehlung zu fragen ist die sichere Richtung. |
| 103 | const basis = ohneVerkehrsstaerkenInPhase2(); |
| 104 | const k3 = basis.signalGroups.find((g) => g.name === 'K3')!; |
| 105 | |
| 106 | const plan = buildSignalPlan(actions.setDemand(basis, k3.id, { volume: 0 })); |
| 107 | |
| 108 | expect(plan.notes.some((n) => n.code === LUECKE)).toBe(true); |
| 109 | }); |
| 110 | |
| 111 | it('schweigt, sobald eine Verkehrsstaerke erfasst ist', () => { |
| 112 | // Gegenprobe: Mit einer echten Zahl entsteht weder die Luecke noch die |
| 113 | // Warnung, und der Auslastungsgrad steht. |
| 114 | const basis = ohneVerkehrsstaerkenInPhase2(); |
| 115 | const k3 = basis.signalGroups.find((g) => g.name === 'K3')!; |
| 116 | const k4 = basis.signalGroups.find((g) => g.name === 'K4')!; |
| 117 | let projekt = actions.setDemand(basis, k3.id, { volume: 410, heavyVehicleShare: 0.03 }); |
| 118 | projekt = actions.setDemand(projekt, k4.id, { volume: 390, heavyVehicleShare: 0.03 }); |
| 119 | |
| 120 | const plan = buildSignalPlan(projekt); |
| 121 | expect(plan.notes.some((n) => n.code === LUECKE)).toBe(false); |
| 122 | expect( |
| 123 | plan.groups.find((g) => g.groupId === k3.id)?.capacity?.degreeOfSaturation, |
| 124 | ).toBeGreaterThan(0); |
| 125 | expect(validateProject(projekt, plan).findings.some((f) => f.rule === REGEL)).toBe(false); |
| 126 | }); |
| 127 | }); |