lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests export saettigungskette.test.ts
| 1 | import { describe, expect, it } from 'vitest'; |
| 2 | import { schwerverkehrSatz } from '@/services/export/bewertung'; |
| 3 | import { signalGroupsCsv } from '@/services/export/csv'; |
| 4 | import { buildSignalPlan } from '@/domain/plan/signalPlan'; |
| 5 | import { createStandardIntersectionProject } from '@/domain/model/factory'; |
| 6 | import { saturationFlow } from '@/domain/rilsa/capacity'; |
| 7 | import { RILSA_DEFAULTS } from '@/domain/rilsa/constants'; |
| 8 | import { resolveRilsaSettings } from '@/domain/rilsa/settings'; |
| 9 | import type { Project } from '@/domain/model/project'; |
| 10 | import * as fmt from '@/ui/format'; |
| 11 | |
| 12 | /* |
| 13 | * Befund 34 zur Fassung 5.9.0. |
| 14 | * |
| 15 | * Der Erlaeuterungsabsatz zum Schwerverkehr beschrieb die Kette als |
| 16 | * tB = fSV * tB0 und qS = 3600 / tB und behauptete daneben, bei reinem |
| 17 | * Pkw-Verkehr stuenden in der Spalte "Saettigungsverkehrsstaerke" die 2000 Fz/h |
| 18 | * je Fahrstreifen. Die Spalte fuehrt aber saturationFlow(movement, lanes, |
| 19 | * defaults) / fSV, also zusaetzlich die Abminderung fuer abbiegende Stroeme |
| 20 | * (capacity.ts: qS0 * n, bei 'links' und 'rechts' mal turningFactor). Bei einer |
| 21 | * einstreifigen abbiegenden Kfz-Gruppe ohne Schwerverkehr stehen dort 1800 Fz/h |
| 22 | * - der Absatz sagte eine Zahl zu, die die Tabelle daneben widerlegt, und |
| 23 | * schloss zugleich mit dem Anspruch, an dem er scheiterte ("ohne ihn liesse |
| 24 | * sich die Saettigungsverkehrsstaerke aus dem Regelwert je Fahrstreifen nicht |
| 25 | * nachvollziehen"). |
| 26 | * |
| 27 | * Keine Zahl der Rechnung ist betroffen; die Abminderung liegt auf der sicheren |
| 28 | * Seite. Falsch war der Nachweis in der Unterlage, die zur Behoerde geht. |
| 29 | * |
| 30 | * NACHGEBESSERT: Die berichtigte Kette fuehrte das Zeichen qS zweimal - einmal |
| 31 | * als 3600/tB (je Fahrstreifen, einschliesslich fSV), einmal als Wert des |
| 32 | * ganzen Stroms. Der Grundwert je Fahrstreifen heisst jetzt qS0. |
| 33 | * |
| 34 | * MUTATIONSNACHWEIS (gemessen an einer Pruefkopie mit den HEAD-Fassungen von |
| 35 | * bewertung.ts und csv.ts): Gegen den Altstand fallen die vier Satzfaelle; die |
| 36 | * beiden Belegzahlfaelle halten den Ausloeser fest - die 1.800 Fz/h in Rechnung |
| 37 | * und Tabellenzelle - und sind in beiden Staenden gruen. 4 von 6. |
| 38 | */ |
| 39 | |
| 40 | const STICHTAG = new Date('2026-01-01T12:00:00Z'); |
| 41 | const QS0 = RILSA_DEFAULTS.capacity.saturationFlow; |
| 42 | const FA = RILSA_DEFAULTS.capacity.turningFactor; |
| 43 | |
| 44 | /** Knotenpunkt mit einstreifiger, links abbiegender erster Kfz-Gruppe ohne Schwerverkehr. */ |
| 45 | function abbiegerOhneSchwerverkehr(): Project { |
| 46 | const basis = createStandardIntersectionProject('Sättigungskette', STICHTAG); |
| 47 | return { |
| 48 | ...basis, |
| 49 | signalGroups: basis.signalGroups.map((g, index) => |
| 50 | index === 0 ? { ...g, movement: 'links' as const, lanes: 1 } : g, |
| 51 | ), |
| 52 | demands: basis.demands.map((d, index) => (index === 0 ? { ...d, heavyVehicleShare: 0 } : d)), |
| 53 | }; |
| 54 | } |
| 55 | |
| 56 | describe('Befund 34 - der Leistungsabsatz nennt die vollständige Kette der Spalte', () => { |
| 57 | it('rechnet die Belegzahl vor: ein einstreifiger Abbieger steht mit 1800 Fz/h in der Spalte', () => { |
| 58 | const projekt = abbiegerOhneSchwerverkehr(); |
| 59 | const plan = buildSignalPlan(projekt); |
| 60 | const gruppe = plan.groups.find((g) => g.groupId === projekt.signalGroups[0]!.id)!; |
| 61 | |
| 62 | expect(saturationFlow('links', 1, plan.defaults)).toBeCloseTo(QS0 * FA, 9); |
| 63 | expect(gruppe.capacity?.saturationFlow).toBeCloseTo(QS0 * FA, 6); |
| 64 | expect(fmt.vehiclesPerHour(gruppe.capacity!.saturationFlow)).toBe('1.800 Fz/h'); |
| 65 | }); |
| 66 | |
| 67 | it('behauptet nicht mehr, in der Spalte stünde der Regelwert je Fahrstreifen', () => { |
| 68 | const satz = schwerverkehrSatz(); |
| 69 | |
| 70 | expect(satz).not.toContain('je Fahrstreifen der Spalte'); |
| 71 | expect(satz).not.toContain('aus dem Regelwert je Fahrstreifen nicht nachvollziehen'); |
| 72 | }); |
| 73 | |
| 74 | it('nennt Fahrstreifenzahl und Abminderung in derselben Kette', () => { |
| 75 | const satz = schwerverkehrSatz(); |
| 76 | |
| 77 | expect(satz).toContain(`qS0 = ${fmt.vehiclesPerHour(QS0)} je Fahrstreifen`); |
| 78 | expect(satz).toContain('qS0 · n · fA / fSV'); |
| 79 | expect(satz).toContain('Abminderung für abbiegende Ströme'); |
| 80 | expect(satz).toContain(`fA = ${fmt.numShort(FA, 2)}`); |
| 81 | expect(satz).toContain('fA = 1'); |
| 82 | // Und die Zahl, die daneben in der Tabelle steht. |
| 83 | expect(satz).toContain(fmt.vehiclesPerHour(QS0 * FA)); |
| 84 | }); |
| 85 | |
| 86 | it('führt qS nicht zugleich für den Wert des ganzen Stroms', () => { |
| 87 | /* |
| 88 | * Der Absatz nennt zwei Groessen: qS = 3600 / tB ist der Wert JE |
| 89 | * FAHRSTREIFEN einschliesslich fSV, die Spalte der ganze Strom. Stuenden |
| 90 | * beide unter dem Zeichen qS, waere die Kette nur fuer n = 1 und fA = 1 |
| 91 | * richtig, und wer die erste Gleichung in die zweite einsetzt, teilt ein |
| 92 | * zweites Mal durch fSV - in einem Absatz, dessen Zweck die |
| 93 | * Nachrechenbarkeit ist. |
| 94 | */ |
| 95 | const satz = schwerverkehrSatz(); |
| 96 | |
| 97 | // Der Wert je Fahrstreifen - aus dem Zeitbedarfswert. |
| 98 | expect(satz).toContain('tB = fSV · 1,8 s und qS = 3600 / tB'); |
| 99 | // Der Wert des ganzen Stroms - eigene Größe, eigenes Zeichen. |
| 100 | expect(satz).toContain('nennt den Wert des ganzen Stroms'); |
| 101 | expect(satz).not.toContain(`qS = ${fmt.vehiclesPerHour(QS0)}`); |
| 102 | expect(satz).not.toContain('qS = qS0'); |
| 103 | }); |
| 104 | |
| 105 | it('nimmt die Abminderung aus den wirksamen Vorgaben, nicht aus dem Satz', () => { |
| 106 | const { defaults } = resolveRilsaSettings({ turningFactor: 0.8, saturationFlow: 1900 }); |
| 107 | const satz = schwerverkehrSatz(defaults); |
| 108 | |
| 109 | expect(satz).toContain('fA = 0,8'); |
| 110 | expect(satz).toContain(`qS0 = ${fmt.vehiclesPerHour(1900)} je Fahrstreifen`); |
| 111 | expect(satz).toContain('qS0 · n · fA / fSV'); |
| 112 | expect(satz).toContain(fmt.vehiclesPerHour(1900 * 0.8)); |
| 113 | expect(satz).not.toContain('fA = 0,9'); |
| 114 | }); |
| 115 | |
| 116 | it('Belegzahl der Tabellenausgabe: dieselbe Spalte trägt 1.800 Fz/h', () => { |
| 117 | const projekt = abbiegerOhneSchwerverkehr(); |
| 118 | const csv = signalGroupsCsv(projekt, buildSignalPlan(projekt)); |
| 119 | const zerlege = (zeile: string): string[] => |
| 120 | (zeile.match(/("([^"]|"")*"|[^;]*)(;|$)/g) ?? []).map((feld) => |
| 121 | feld.replace(/;$/, '').replace(/^"|"$/g, '').replace(/""/g, '"'), |
| 122 | ); |
| 123 | const alle = csv.split('\r\n'); |
| 124 | const kopf = zerlege((alle[0] ?? '').replace(/^/, '')); |
| 125 | const spalte = kopf.indexOf('Sättigungsverkehrsstärke [Fz/h]'); |
| 126 | const zeile = zerlege(alle.find((z) => z.startsWith(`${projekt.signalGroups[0]!.name};`))!); |
| 127 | |
| 128 | expect(spalte).toBeGreaterThanOrEqual(0); |
| 129 | // Die Zelle traegt die Abminderung; der Kommentar ueber der Spalte fSV |
| 130 | // sagte, mit fSV allein liesse sie sich nachrechnen. |
| 131 | expect(zeile[spalte]).toBe('1.800'); |
| 132 | }); |
| 133 | }); |