lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests domain pruefberichtQualitaetsstufen.test.ts
| 1 | import { readFileSync } from 'node:fs'; |
| 2 | import { describe, expect, it } from 'vitest'; |
| 3 | import { validateProject } from '@/domain/validation/engine'; |
| 4 | import { buildSignalPlan } from '@/domain/plan/signalPlan'; |
| 5 | import { |
| 6 | createConflict, |
| 7 | createEmptyProject, |
| 8 | createPhase, |
| 9 | createSignalGroup, |
| 10 | createStandardIntersectionProject, |
| 11 | } from '@/domain/model/factory'; |
| 12 | import type { Anlagenart, Project } from '@/domain/model/project'; |
| 13 | |
| 14 | /* |
| 15 | * Pruefbericht zur Qualitaetsstufe seit Fassung 5.4.0 (Befunde |
| 16 | * B1, B2, B4). |
| 17 | * |
| 18 | * Bis dahin lief jede Furt ueber die Kfz-Rechnung mit Kapazitaet 0, erhielt |
| 19 | * formal die Stufe F, und der Bericht erklaerte die eigene Stufe fuer |
| 20 | * aussagelos ("Wartezeit nicht bestimmbar - bei Fussgaengerfurten ist das der |
| 21 | * Regelfall"). Die Kfz-Meldung schrieb "nach HBS", waehrend immer die |
| 22 | * HCM-Formel gerechnet wurde, und nannte nicht, woran die Stufe haengt. |
| 23 | * |
| 24 | * Jetzt: Fussgaenger und Radverkehr werden nach der maximalen Wartezeit |
| 25 | * (laengste Sperrzeit) bewertet - ohne Verkehrsstaerke, auch an der |
| 26 | * Fussgaengerschutzanlage; Kfz und OePNV nennen Verfahren und Kriterium; nach |
| 27 | * HCM sagt der Bericht, dass Fussgaenger und Radverkehr nicht bewertet werden. |
| 28 | */ |
| 29 | |
| 30 | const STICHTAG = new Date('2026-01-01T00:00:00Z'); |
| 31 | |
| 32 | function vollstaendig(): Project { |
| 33 | const base = createStandardIntersectionProject('Prueffall', STICHTAG); |
| 34 | return { |
| 35 | ...base, |
| 36 | meta: { |
| 37 | ...base.meta, |
| 38 | projectNumber: 'VP-2026-001', |
| 39 | client: 'Stadt Musterhausen', |
| 40 | planner: 'O. Willerding', |
| 41 | location: 'Hauptstrasse / Bahnhofstrasse', |
| 42 | }, |
| 43 | }; |
| 44 | } |
| 45 | |
| 46 | /** |
| 47 | * Eine Fahrbahn, eine Furt, feste Umlaufzeit und feste Freigabezeit der Furt. |
| 48 | * Die Sperrzeit der Furt ist Umlaufzeit abzueglich Freigabezeit und |
| 49 | * Uebergaengen - bei 120 s Umlauf und 10 s Freigabe rund 110 s. |
| 50 | */ |
| 51 | function furtprojekt( |
| 52 | umlaufzeit: number, |
| 53 | gruenFurt: number, |
| 54 | anlagenart: Anlagenart, |
| 55 | furtart: 'fuss' | 'rad' = 'fuss', |
| 56 | ): Project { |
| 57 | const basis = createEmptyProject('Furt', STICHTAG, anlagenart); |
| 58 | const kfz = createSignalGroup({ name: 'K1', mode: 'kfz', index: 0 }); |
| 59 | const furt = createSignalGroup({ name: 'F1', mode: furtart, index: 1 }); |
| 60 | const phase1 = createPhase('Fahrverkehr', [kfz.id]); |
| 61 | const phase2 = createPhase('Furt', [furt.id]); |
| 62 | const konflikte = [ |
| 63 | { ...createConflict(kfz.id, furt.id), clearingDistance: 25, enteringDistance: 2 }, |
| 64 | { ...createConflict(furt.id, kfz.id), clearingDistance: 12, enteringDistance: 5 }, |
| 65 | ]; |
| 66 | return { |
| 67 | ...basis, |
| 68 | meta: { |
| 69 | ...basis.meta, |
| 70 | projectNumber: 'VP-2026-002', |
| 71 | client: 'Stadt Musterhausen', |
| 72 | planner: 'O. Willerding', |
| 73 | location: 'Furt', |
| 74 | }, |
| 75 | signalGroups: [kfz, furt], |
| 76 | conflicts: konflikte, |
| 77 | phases: [phase1, { ...phase2, manualGreen: gruenFurt }], |
| 78 | program: { |
| 79 | ...basis.program, |
| 80 | phaseOrder: [phase1.id, phase2.id], |
| 81 | method: 'manuell', |
| 82 | manualCycleTime: umlaufzeit, |
| 83 | }, |
| 84 | }; |
| 85 | } |
| 86 | |
| 87 | function qualitaetsstufen(project: Project) { |
| 88 | const plan = buildSignalPlan(project); |
| 89 | const bericht = validateProject(project, plan, STICHTAG); |
| 90 | return { |
| 91 | plan, |
| 92 | bericht, |
| 93 | stufen: bericht.findings.filter((f) => f.rule === 'leistungsfaehigkeit.qualitaetsstufe'), |
| 94 | }; |
| 95 | } |
| 96 | |
| 97 | describe('Fussgaenger und Radverkehr: maximale Wartezeit statt "nicht bestimmbar"', () => { |
| 98 | it('meldet fuer eine Furt mit langer Sperrzeit die Stufe F nach der maximalen Wartezeit', () => { |
| 99 | // Umlauf 120 s, Furt 10 s Freigabe: Sperrzeit rund 110 s, Tafel F (> 85 s). |
| 100 | const { plan, stufen } = qualitaetsstufen(furtprojekt(120, 10, 'knotenpunkt')); |
| 101 | const furt = plan.groups.find((g) => g.name === 'F1')!; |
| 102 | expect(furt.delay?.serviceLevel?.grade).toBe('F'); |
| 103 | |
| 104 | const befund = stufen.find((f) => f.target?.label === 'F1'); |
| 105 | expect(befund, 'Die Furt muss eine Stufenmeldung erhalten').toBeDefined(); |
| 106 | expect(befund!.severity).toBe('warnung'); |
| 107 | expect(befund!.title).toBe('Qualitätsstufe F für "F1"'); |
| 108 | expect(befund!.message).toContain('maximale Wartezeit'); |
| 109 | expect(befund!.message).toContain('längste Sperrzeit im Umlauf'); |
| 110 | expect(befund!.message).toContain('nach HBS 2015'); |
| 111 | expect(befund!.message).toContain('Fußgänger und Radverkehr: maximale Wartezeit'); |
| 112 | expect(befund!.message).not.toContain('mittlere Wartezeit'); |
| 113 | expect(befund!.message).not.toContain('nicht bestimmbar'); |
| 114 | expect(befund!.reference).toBe('HBS 2015 - Qualitätsstufen des Verkehrsablaufs'); |
| 115 | expect(befund!.suggestion).toContain('Sperrzeit'); |
| 116 | |
| 117 | // Der genannte Wert ist der Wert des Plans - nachrechenbar aus Umlauf und |
| 118 | // Freigabezeit, nicht eine zweite Zahl. |
| 119 | const maximal = furt.delay!.maximumDelay; |
| 120 | expect(maximal).toBeGreaterThan(85); |
| 121 | expect(befund!.message).toContain(`${String(maximal).replace('.', ',')} s`); |
| 122 | }); |
| 123 | |
| 124 | it('bewertet auch ohne erfasste Verkehrsstaerken - die Furt braucht keine', () => { |
| 125 | const projekt = furtprojekt(120, 10, 'knotenpunkt'); |
| 126 | expect(projekt.demands).toHaveLength(0); |
| 127 | const { bericht, stufen } = qualitaetsstufen(projekt); |
| 128 | // Fuer den Kfz-Strom fehlt die Verkehrsstaerke - das bleibt eine Warnung ... |
| 129 | expect( |
| 130 | bericht.findings.some((f) => f.rule === 'leistungsfaehigkeit.keine-verkehrsstaerken'), |
| 131 | ).toBe(true); |
| 132 | // ... die Furt wird trotzdem bewertet. |
| 133 | expect(stufen.some((f) => f.target?.label === 'F1')).toBe(true); |
| 134 | }); |
| 135 | |
| 136 | it('bewertet den Radverkehr genauso wie die Fussgaengerfurt', () => { |
| 137 | const { stufen } = qualitaetsstufen(furtprojekt(120, 10, 'knotenpunkt', 'rad')); |
| 138 | const befund = stufen.find((f) => f.target?.label === 'F1'); |
| 139 | expect(befund).toBeDefined(); |
| 140 | expect(befund!.message).toContain('maximale Wartezeit'); |
| 141 | expect(befund!.message).toContain('Fußgänger und Radverkehr: maximale Wartezeit'); |
| 142 | }); |
| 143 | |
| 144 | it('prueft die Furt auch an der Fussgaengerschutzanlage - dort ist die Wartezeit das Mass', () => { |
| 145 | const { bericht, stufen } = qualitaetsstufen(furtprojekt(120, 10, 'fussgaengerschutzanlage')); |
| 146 | const befund = stufen.find((f) => f.target?.label === 'F1'); |
| 147 | expect(befund).toBeDefined(); |
| 148 | expect(befund!.message).toContain('maximale Wartezeit'); |
| 149 | // Die Kfz-Leistungsfaehigkeit bleibt dort ausser Betracht: kein |
| 150 | // Auslastungsgrad, keine Forderung nach Verkehrsstaerken. |
| 151 | expect( |
| 152 | bericht.findings.some( |
| 153 | (f) => |
| 154 | f.rule === 'leistungsfaehigkeit.keine-verkehrsstaerken' || |
| 155 | f.rule === 'leistungsfaehigkeit.ueberlastet' || |
| 156 | (f.rule === 'leistungsfaehigkeit.qualitaetsstufe' && f.target?.label === 'K1'), |
| 157 | ), |
| 158 | ).toBe(false); |
| 159 | }); |
| 160 | |
| 161 | it('stuft E als Hinweis ein und schweigt bei A bis D', () => { |
| 162 | // 100 s Umlauf, 20 s Freigabe: Sperrzeit 80 s -> E (70 < tS <= 85). |
| 163 | const e = qualitaetsstufen(furtprojekt(100, 20, 'knotenpunkt')); |
| 164 | const furtE = e.plan.groups.find((g) => g.name === 'F1')!; |
| 165 | expect(furtE.delay?.maximumDelay).toBe(80); |
| 166 | expect(furtE.delay?.serviceLevel?.grade).toBe('E'); |
| 167 | const befundE = e.stufen.find((f) => f.target?.label === 'F1'); |
| 168 | expect(befundE?.severity).toBe('hinweis'); |
| 169 | |
| 170 | // 60 s Umlauf, 15 s Freigabe: Sperrzeit 45 s -> C, keine Meldung. |
| 171 | const b = qualitaetsstufen(furtprojekt(60, 15, 'knotenpunkt')); |
| 172 | const furtB = b.plan.groups.find((g) => g.name === 'F1')!; |
| 173 | expect(['A', 'B', 'C', 'D']).toContain(furtB.delay?.serviceLevel?.grade); |
| 174 | expect(b.stufen.some((f) => f.target?.label === 'F1')).toBe(false); |
| 175 | }); |
| 176 | |
| 177 | it('kennt die alte Meldung "nicht bestimmbar" nicht mehr', () => { |
| 178 | const quelltext = readFileSync( |
| 179 | new URL('../../src/domain/validation/engine.ts', import.meta.url), |
| 180 | 'utf8', |
| 181 | ); |
| 182 | expect(quelltext).not.toContain('nicht bestimmbar'); |
| 183 | expect(quelltext).not.toContain('Bei Fußgängerfurten ist das der Regelfall'); |
| 184 | }); |
| 185 | }); |
| 186 | |
| 187 | describe('Kfz und OePNV: Verfahren und Kriterium stehen an der Stufe', () => { |
| 188 | function ueberlastet(scale: 'HBS' | 'HCM'): Project { |
| 189 | const base = vollstaendig(); |
| 190 | return { |
| 191 | ...base, |
| 192 | settings: { ...base.settings, serviceLevelScale: scale }, |
| 193 | demands: base.demands.map((d) => ({ ...d, volume: 2500 })), |
| 194 | }; |
| 195 | } |
| 196 | |
| 197 | it('nennt bei Ueberlastung das Kriterium q > C und die Stufe F nach HBS 2015', () => { |
| 198 | const { plan, bericht, stufen } = qualitaetsstufen(ueberlastet('HBS')); |
| 199 | const k1 = plan.groups.find((g) => g.name === 'K1')!; |
| 200 | expect(k1.delay?.serviceLevel?.kriterium).toBe('ueberlastung'); |
| 201 | |
| 202 | const befund = stufen.find((f) => f.target?.label === 'K1'); |
| 203 | expect(befund).toBeDefined(); |
| 204 | expect(befund!.severity).toBe('warnung'); |
| 205 | expect(befund!.message).toContain('ist überlastet (q > C)'); |
| 206 | expect(befund!.message).toContain( |
| 207 | 'Nach HBS 2015 ist das die Stufe F unabhängig von der Wartezeit', |
| 208 | ); |
| 209 | // q und C stehen in der Meldung - sonst liesse sich q > C nicht nachrechnen. |
| 210 | expect(befund!.message).toContain('2500 Fz/h'); |
| 211 | expect(befund!.message).toContain(`${k1.capacity!.capacity} Fz/h`); |
| 212 | expect(befund!.reference).toBe('HBS 2015 - Qualitätsstufen des Verkehrsablaufs'); |
| 213 | |
| 214 | // Die Auslastungsgrad-Regel selbst bleibt und traegt die HBS-Fundstelle. |
| 215 | const ueberlastung = bericht.findings.find( |
| 216 | (f) => f.rule === 'leistungsfaehigkeit.ueberlastet' && f.target?.label === 'K1', |
| 217 | ); |
| 218 | expect(ueberlastung?.reference).toBe('HBS 2015 - Kapazität und Auslastungsgrad'); |
| 219 | }); |
| 220 | |
| 221 | it('nennt bei einer Wartezeitstufe das Kriterium mittlere Wartezeit', () => { |
| 222 | // K1 auf 1200 Fz/h: x knapp unter 1, mittlere Wartezeit weit ueber 70 s -> |
| 223 | // Stufe E (HBS 2015 kennt fuer Kfz keine Wartezeitstufe F). |
| 224 | const base = vollstaendig(); |
| 225 | const projekt: Project = { |
| 226 | ...base, |
| 227 | demands: base.demands.map((d, i) => (i === 0 ? { ...d, volume: 1200 } : d)), |
| 228 | }; |
| 229 | const { plan, stufen } = qualitaetsstufen(projekt); |
| 230 | const k1 = plan.groups.find((g) => g.name === 'K1')!; |
| 231 | expect(k1.delay?.serviceLevel?.grade).toBe('E'); |
| 232 | expect(k1.delay?.serviceLevel?.kriterium).toBe('mittlere-wartezeit'); |
| 233 | |
| 234 | const befund = stufen.find((f) => f.target?.label === 'K1'); |
| 235 | expect(befund).toBeDefined(); |
| 236 | expect(befund!.severity).toBe('hinweis'); |
| 237 | expect(befund!.message).toContain('mittlere Wartezeit'); |
| 238 | expect(befund!.message).toContain('Kriterium: mittlere Wartezeit'); |
| 239 | expect(befund!.message).toContain('nach HBS 2015'); |
| 240 | expect(befund!.message).toContain(`${String(k1.delay!.averageDelay).replace('.', ',')} s`); |
| 241 | expect(befund!.message).not.toContain('überlastet'); |
| 242 | }); |
| 243 | |
| 244 | it('sagt "HCM" statt "HBS 2015", wenn nach HCM gerechnet wurde', () => { |
| 245 | const { plan, stufen } = qualitaetsstufen(ueberlastet('HCM')); |
| 246 | const k1 = plan.groups.find((g) => g.name === 'K1')!; |
| 247 | expect(k1.delay?.serviceLevel?.scale).toBe('HCM'); |
| 248 | |
| 249 | const befund = stufen.find((f) => f.target?.label === 'K1'); |
| 250 | expect(befund).toBeDefined(); |
| 251 | expect(befund!.message).toContain('Nach HCM ist das die Stufe F unabhängig von der Wartezeit'); |
| 252 | expect(befund!.message).not.toContain('HBS'); |
| 253 | expect(befund!.reference).toBe('HCM - Level of Service'); |
| 254 | }); |
| 255 | |
| 256 | it('weist die Schwellen 0,85 und 0,95 als Praxiswerte aus, nicht als HBS-Zahlen', () => { |
| 257 | // K1 auf 1300 Fz/h: K1 ueberlastet, K4 ueber dem Praxiswert, aber nicht |
| 258 | // ueberlastet (die Umlaufzeit steigt bis an ihre Obergrenze). |
| 259 | // |
| 260 | // NACHGEZOGEN MIT FASSUNG 5.43.0 (Gruppe Furt): Hier standen 1500 Fz/h und |
| 261 | // K2 als zweite Gruppe. Die Vorlage erfasst seither jede Furt auch gegen |
| 262 | // den Strom, der sie beim Ausfahren quert (K2/F2, K4/F1), und K4 beginnt |
| 263 | // erst 10 s nach dem Ende von F1. Mit 1500 Fz/h sind dadurch K1, K3 und K4 |
| 264 | // ueberlastet, und keine Meldung nach Praxiswert bleibt uebrig; mit |
| 265 | // 1300 Fz/h liegt K4 bei 0,961 - der Fall, den diese Wache braucht. |
| 266 | const base = vollstaendig(); |
| 267 | const projekt: Project = { |
| 268 | ...base, |
| 269 | demands: base.demands.map((d, i) => (i === 0 ? { ...d, volume: 1300 } : d)), |
| 270 | }; |
| 271 | const { bericht } = qualitaetsstufen(projekt); |
| 272 | const praxis = bericht.findings.filter( |
| 273 | (f) => f.rule === 'leistungsfaehigkeit.hoch' || f.rule === 'leistungsfaehigkeit.kritisch', |
| 274 | ); |
| 275 | expect(praxis.length).toBeGreaterThan(0); |
| 276 | for (const f of praxis) { |
| 277 | expect(f.reference).toContain('Praxiswerte'); |
| 278 | expect(f.reference).not.toContain('HBS'); |
| 279 | } |
| 280 | }); |
| 281 | }); |
| 282 | |
| 283 | describe('Nach HCM: Fussgaenger und Radverkehr werden nicht bewertet - und der Bericht sagt es', () => { |
| 284 | it('gibt einen Hinweis je Projekt, nicht je Gruppe', () => { |
| 285 | const base = furtprojekt(120, 10, 'knotenpunkt'); |
| 286 | const rad = createSignalGroup({ name: 'R1', mode: 'rad', index: 2 }); |
| 287 | const projekt: Project = { |
| 288 | ...base, |
| 289 | settings: { ...base.settings, serviceLevelScale: 'HCM' }, |
| 290 | signalGroups: [...base.signalGroups, rad], |
| 291 | phases: base.phases.map((p, i) => |
| 292 | i === 1 ? { ...p, signalGroupIds: [...p.signalGroupIds, rad.id] } : p, |
| 293 | ), |
| 294 | }; |
| 295 | const { plan, bericht, stufen } = qualitaetsstufen(projekt); |
| 296 | // Voraussetzung: keine Stufe fuer Furt und Radfurt. |
| 297 | expect(plan.groups.find((g) => g.name === 'F1')!.delay?.serviceLevel).toBeNull(); |
| 298 | expect(plan.groups.find((g) => g.name === 'R1')!.delay?.serviceLevel).toBeNull(); |
| 299 | |
| 300 | const hinweise = bericht.findings.filter( |
| 301 | (f) => f.rule === 'leistungsfaehigkeit.fuss-rad-nicht-bewertet', |
| 302 | ); |
| 303 | expect(hinweise, 'genau ein Hinweis fuer beide Gruppen').toHaveLength(1); |
| 304 | expect(hinweise[0]!.severity).toBe('hinweis'); |
| 305 | expect(hinweise[0]!.title).toContain('nicht bewertet'); |
| 306 | expect(hinweise[0]!.message).toContain('"F1"'); |
| 307 | expect(hinweise[0]!.message).toContain('"R1"'); |
| 308 | expect(hinweise[0]!.message).toContain('maximale Wartezeit'); |
| 309 | expect(hinweise[0]!.reference).toBe('HCM - Level of Service'); |
| 310 | // Keine Stufenmeldung fuer die Furten - es gibt keine Stufe. |
| 311 | expect(stufen.some((f) => f.target?.label === 'F1' || f.target?.label === 'R1')).toBe(false); |
| 312 | }); |
| 313 | |
| 314 | it('schweigt nach HBS und ohne Fuss- oder Radgruppen', () => { |
| 315 | const hbs = qualitaetsstufen(furtprojekt(120, 10, 'knotenpunkt')); |
| 316 | expect( |
| 317 | hbs.bericht.findings.some((f) => f.rule === 'leistungsfaehigkeit.fuss-rad-nicht-bewertet'), |
| 318 | ).toBe(false); |
| 319 | |
| 320 | const base = vollstaendig(); |
| 321 | const nurKfz: Project = { |
| 322 | ...base, |
| 323 | settings: { ...base.settings, serviceLevelScale: 'HCM' }, |
| 324 | signalGroups: base.signalGroups.filter((g) => g.mode === 'kfz'), |
| 325 | conflicts: base.conflicts.filter((c) => |
| 326 | base.signalGroups |
| 327 | .filter((g) => g.mode === 'kfz') |
| 328 | .some((g) => g.id === c.fromId || g.id === c.toId), |
| 329 | ), |
| 330 | phases: base.phases.map((p) => ({ |
| 331 | ...p, |
| 332 | signalGroupIds: p.signalGroupIds.filter((id) => |
| 333 | base.signalGroups.some((g) => g.id === id && g.mode === 'kfz'), |
| 334 | ), |
| 335 | })), |
| 336 | }; |
| 337 | expect( |
| 338 | qualitaetsstufen(nurKfz).bericht.findings.some( |
| 339 | (f) => f.rule === 'leistungsfaehigkeit.fuss-rad-nicht-bewertet', |
| 340 | ), |
| 341 | ).toBe(false); |
| 342 | }); |
| 343 | }); |
| 344 | |
| 345 | describe('Verkehrsstaerken werden nur verlangt, wo eine Saettigungsverkehrsstaerke gilt', () => { |
| 346 | it('fordert bei einem Projekt nur aus Furt und Radfurt keine Verkehrsstaerken', () => { |
| 347 | // Vor Fassung 5.4.0 loeste jede Gruppe mit mode !== 'fuss' |
| 348 | // die Forderung aus - also auch der Radverkehr, der nach HBS 2015 keine |
| 349 | // Saettigungsverkehrsstaerke hat. |
| 350 | const basis = createEmptyProject('Rad und Fuss', STICHTAG, 'knotenpunkt'); |
| 351 | const rad = createSignalGroup({ name: 'R1', mode: 'rad', index: 0 }); |
| 352 | const fuss = createSignalGroup({ name: 'F1', mode: 'fuss', index: 1 }); |
| 353 | const phase1 = createPhase('Rad', [rad.id]); |
| 354 | const phase2 = createPhase('Fuss', [fuss.id]); |
| 355 | const projekt: Project = { |
| 356 | ...basis, |
| 357 | signalGroups: [rad, fuss], |
| 358 | conflicts: [ |
| 359 | { ...createConflict(rad.id, fuss.id), clearingDistance: 10, enteringDistance: 3 }, |
| 360 | { ...createConflict(fuss.id, rad.id), clearingDistance: 6, enteringDistance: 3 }, |
| 361 | ], |
| 362 | phases: [phase1, phase2], |
| 363 | program: { ...basis.program, phaseOrder: [phase1.id, phase2.id] }, |
| 364 | }; |
| 365 | const bericht = validateProject(projekt, undefined, STICHTAG); |
| 366 | expect( |
| 367 | bericht.findings.some((f) => f.rule === 'leistungsfaehigkeit.keine-verkehrsstaerken'), |
| 368 | ).toBe(false); |
| 369 | }); |
| 370 | |
| 371 | it('fordert sie weiterhin, sobald ein Kfz-Strom ohne Verkehrsstaerke dabei ist', () => { |
| 372 | const base = vollstaendig(); |
| 373 | const bericht = validateProject({ ...base, demands: [] }, undefined, STICHTAG); |
| 374 | expect( |
| 375 | bericht.findings.some((f) => f.rule === 'leistungsfaehigkeit.keine-verkehrsstaerken'), |
| 376 | ).toBe(true); |
| 377 | }); |
| 378 | }); |
| 379 | |
| 380 | describe('Gespeicherte Verkehrsstaerken ohne Rechenwirkung', () => { |
| 381 | it('nennt eine Radverkehrsstaerke aus einer aelteren Datei, die in keine Rechnung mehr eingeht', () => { |
| 382 | // Bis Fassung 5.3.5 ging die Verkehrsstaerke einer Radgruppe mit einer |
| 383 | // Kfz-Saettigungsverkehrsstaerke in Kapazitaet und Umlaufzeit ein. Seit |
| 384 | // Fassung 5.4.0 (Befund B4) nicht mehr - das darf beim |
| 385 | // Oeffnen einer Altdatei nicht still geschehen. |
| 386 | const base = vollstaendig(); |
| 387 | const rad = createSignalGroup({ name: 'R1', mode: 'rad', index: base.signalGroups.length }); |
| 388 | const projekt: Project = { |
| 389 | ...base, |
| 390 | signalGroups: [...base.signalGroups, rad], |
| 391 | phases: base.phases.map((p, i) => |
| 392 | i === 0 ? { ...p, signalGroupIds: [...p.signalGroupIds, rad.id] } : p, |
| 393 | ), |
| 394 | demands: [...base.demands, { signalGroupId: rad.id, volume: 300, heavyVehicleShare: 0 }], |
| 395 | }; |
| 396 | const bericht = validateProject(projekt, undefined, STICHTAG); |
| 397 | const hinweis = bericht.findings.find( |
| 398 | (f) => f.rule === 'leistungsfaehigkeit.verkehrsstaerke-ohne-wirkung', |
| 399 | ); |
| 400 | expect(hinweis).toBeDefined(); |
| 401 | expect(hinweis?.severity).toBe('hinweis'); |
| 402 | expect(hinweis?.message).toContain('"R1"'); |
| 403 | expect(hinweis?.message).toContain('Umlaufzeit'); |
| 404 | // Ohne solche Verkehrsstaerke schweigt der Bericht. |
| 405 | const ohne = validateProject(base, undefined, STICHTAG); |
| 406 | expect( |
| 407 | ohne.findings.some((f) => f.rule === 'leistungsfaehigkeit.verkehrsstaerke-ohne-wirkung'), |
| 408 | ).toBe(false); |
| 409 | }); |
| 410 | }); |
| 411 | |
| 412 | describe('Verfahrensabhaengige Meldungen im HCM-Projekt', () => { |
| 413 | it('nennt bei einer Fuss/Rad-Verkehrsstaerke nach HCM kein HBS-Etikett', () => { |
| 414 | const base = vollstaendig(); |
| 415 | const rad = createSignalGroup({ name: 'R1', mode: 'rad', index: base.signalGroups.length }); |
| 416 | const projekt: Project = { |
| 417 | ...base, |
| 418 | signalGroups: [...base.signalGroups, rad], |
| 419 | phases: base.phases.map((p, i) => |
| 420 | i === 0 ? { ...p, signalGroupIds: [...p.signalGroupIds, rad.id] } : p, |
| 421 | ), |
| 422 | demands: [...base.demands, { signalGroupId: rad.id, volume: 300, heavyVehicleShare: 0 }], |
| 423 | settings: { ...base.settings, serviceLevelScale: 'HCM' }, |
| 424 | }; |
| 425 | const bericht = validateProject(projekt, undefined, STICHTAG); |
| 426 | const hinweis = bericht.findings.find( |
| 427 | (f) => f.rule === 'leistungsfaehigkeit.verkehrsstaerke-ohne-wirkung', |
| 428 | ); |
| 429 | expect(hinweis).toBeDefined(); |
| 430 | expect(hinweis?.message).not.toContain('HBS'); |
| 431 | expect(hinweis?.message).toContain('nach HCM erhalten sie keine Stufe'); |
| 432 | expect(hinweis?.reference).toBe('HCM - Level of Service'); |
| 433 | }); |
| 434 | |
| 435 | it('meldet eine Abweichung des Instationaritaetsfaktors nach HCM als wirkungslos, nach HBS mit HBS-Fundstelle', () => { |
| 436 | const base = vollstaendig(); |
| 437 | const mitAbweichung = (verfahren: 'HBS' | 'HCM'): Project => ({ |
| 438 | ...base, |
| 439 | settings: { |
| 440 | ...base.settings, |
| 441 | serviceLevelScale: verfahren, |
| 442 | rilsa: { instationaritaetsfaktor: 1.2 }, |
| 443 | }, |
| 444 | }); |
| 445 | const hcm = validateProject(mitAbweichung('HCM'), undefined, STICHTAG).findings.find( |
| 446 | (f) => f.rule === 'vorgaben.abweichung.instationaritaetsfaktor', |
| 447 | ); |
| 448 | expect(hcm?.message).toContain('geht in keine Zahl ein'); |
| 449 | expect(hcm?.reference).toContain('HBS 2015'); |
| 450 | const hbs = validateProject(mitAbweichung('HBS'), undefined, STICHTAG).findings.find( |
| 451 | (f) => f.rule === 'vorgaben.abweichung.instationaritaetsfaktor', |
| 452 | ); |
| 453 | expect(hbs?.message).not.toContain('geht in keine Zahl ein'); |
| 454 | expect(hbs?.reference).toContain('mittlere Wartezeit'); |
| 455 | // Die Fundstelle einer Abweichung ist die Quelle des Kennwerts - nicht |
| 456 | // pauschal "RiLSA 2015 - Kennwerte". |
| 457 | const rast = validateProject( |
| 458 | { ...base, settings: { ...base.settings, rilsa: { cycleTimeStep: 2 } } }, |
| 459 | undefined, |
| 460 | STICHTAG, |
| 461 | ).findings.find((f) => f.rule === 'vorgaben.abweichung.cycleTimeStep'); |
| 462 | expect(rast?.reference).toContain('Planungspraxis'); |
| 463 | }); |
| 464 | }); |