lsa-planer

LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.

/ tests export schwerverkehrsaufteilung.test.ts

23,4 KB Rohdatei
tests/export/schwerverkehrsaufteilung.test.ts — 541 Zeilen
1 import { inflateSync } from 'node:zlib';
2 import { describe, expect, it } from 'vitest';
3 import { buildProjectPdf, DEFAULT_PDF_OPTIONS } from '@/services/export/pdf';
4 import { signalGroupsCsv } from '@/services/export/csv';
5 import {
6 aufteilungslage,
7 schwerverkehrSatz,
8 schwerverkehrsangabe,
9 } from '@/services/export/bewertung';
10 import { buildSignalPlan } from '@/domain/plan/signalPlan';
11 import { validateProject } from '@/domain/validation';
12 import { createStandardIntersectionProject } from '@/domain/model/factory';
13 import { saturationFlow, schwerverkehrsfaktor } from '@/domain/rilsa/capacity';
14 import {
15 HBS_LASTZUGANTEIL_BEI_UNBEKANNTER_AUFTEILUNG,
16 HBS_PKW_GLEICHWERTE,
17 RILSA_DEFAULTS,
18 } from '@/domain/rilsa/constants';
19 import { toWinAnsi } from '@/render/pdfSurface';
20 import type { Project, TrafficDemand } from '@/domain/model/project';
21 import * as fmt from '@/ui/format';
22
23 /*
24 * Was AUSDRUCK und TABELLENAUSGABE von der Aufteilung des Schwerverkehrs sagen
25 * muessen.
26 *
27 * Jede Verkehrsstaerke kennt einen optionalen Lastzuganteil am
28 * Schwerverkehr (`TrafficDemand.lastzugAnteil`), und der Plan rechnet danach
29 * nach HBS 2015 Gl. 2-5 statt Gl. 2-6. Fuer die Ausgabe folgt daraus dreierlei:
30 *
31 * 1. Der gedruckte Faktor fSV muss MIT der Aufteilung gebildet werden. Zuvor
32 * rief jede der beiden Ausgaben `schwerverkehrsfaktor(anteil)` selbst auf -
33 * also ohne sie. Bei 20 % Schwerverkehr aus lauter Lastzuegen stand dann
34 * fSV = 1,18 neben einer Saettigungsverkehrsstaerke von 1.538 Fz/h, die zu
35 * 1,30 gehoert: zwei Spalten derselben Zeile, die einander widersprechen.
36 * 2. Die Zeile muss sagen, WAS erfasst ist und nach welcher Gleichung
37 * gerechnet wurde. Zwei Stroeme mit denselben 20 % tragen jetzt
38 * verschiedene Faktoren, und ohne diese Angabe liesse sich die Unterlage
39 * nicht nachrechnen.
40 * 3. Der Erlaeuterungsabsatz darf nicht mehr behaupten, dieses Programm
41 * erfasse keine Aufteilung. Die Fehlerrichtung des Pauschalwerts gilt nur
42 * noch fuer Stroeme ohne erfasste Aufteilung.
43 *
44 * Die Zahlen dieses Tests sind die Belegzahlen der Umstellung (BASt V 400,
45 * S. 10, Gl. 2-5/2-6) und bei einem Grundwert von 2000 Kfz/h je Fahrstreifen
46 * und 20 % Schwerverkehr nachgerechnet:
47 *
48 * lauter Lastzuege fSV = 1 + 1,50 · 0,20 = 1,30 -> 1.538 Fz/h
49 * keine Aufteilung fSV = 1 + 0,90 · 0,20 = 1,18 -> 1.695 Fz/h
50 * keine Lastzuege fSV = 1 + 0,75 · 0,20 = 1,15 -> 1.739 Fz/h
51 * 40 % Lastzuege fSV = 1 + 0,75·0,12 + 1,50·0,08 = 1,21 -> 1.653 Fz/h
52 */
53
54 const STICHTAG = new Date('2026-01-01T12:00:00Z');
55
56 /** Sichtbaren Text aus dem PDF lesen; die Inhaltsstroeme sind komprimiert. */
57 function pdfText(bytes: Uint8Array): string {
58 const roh = new TextDecoder('latin1').decode(bytes);
59 const teile: string[] = [];
60 const suche = /stream\r?\n/g;
61 let treffer: RegExpExecArray | null;
62
63 while ((treffer = suche.exec(roh)) !== null) {
64 const start = treffer.index + treffer[0].length;
65 const ende = roh.indexOf('endstream', start);
66 if (ende < 0) continue;
67 let inhalt: string;
68 try {
69 inhalt = inflateSync(Buffer.from(roh.slice(start, ende), 'latin1')).toString('latin1');
70 } catch {
71 continue;
72 }
73 for (const t of inhalt.matchAll(/\(((?:[^()\\]|\\.)*)\)\s*Tj/g)) {
74 teile.push(t[1]!.replace(/\\([()\\])/g, '$1'));
75 }
76 }
77 return teile.join(' ').replace(/\s+/g, ' ');
78 }
79
80 const gedruckt = (text: string): string => toWinAnsi(text).replace(/\s+/g, ' ');
81
82 /**
83 * Vier Kfz-Stroeme mit demselben Schwerverkehrsanteil von 20 % und den vier
84 * Datenlagen, die es gibt: lauter Lastzuege, keine Angabe, gezaehlte 0 % und
85 * ein Zwischenwert. Derselbe Anteil bei jedem - so ist jeder Unterschied in
86 * der Ausgabe ein Unterschied der AUFTEILUNG und nichts anderes.
87 */
88 function vierDatenlagen(): Project {
89 const basis = createStandardIntersectionProject('Aufteilung', STICHTAG);
90 const anteile: readonly (number | undefined)[] = [1, undefined, 0, 0.4];
91 return {
92 ...basis,
93 demands: basis.demands.map((d, index): TrafficDemand => {
94 const lastzugAnteil = anteile[index];
95 const gemeinsam = { ...d, heavyVehicleShare: 0.2 };
96 // Kein `lastzugAnteil: undefined`: Das Feld fehlt, wo nichts erfasst ist
97 // - genau das ist der Fall, fuer den Gl. 2-6 gilt.
98 return lastzugAnteil === undefined ? gemeinsam : { ...gemeinsam, lastzugAnteil };
99 }),
100 };
101 }
102
103 /** Dasselbe Projekt, aber an JEDEM Strom eine erfasste Aufteilung. */
104 function ueberallErfasst(): Project {
105 const basis = createStandardIntersectionProject('Aufteilung', STICHTAG);
106 return {
107 ...basis,
108 demands: basis.demands.map((d) => ({ ...d, heavyVehicleShare: 0.2, lastzugAnteil: 0.4 })),
109 };
110 }
111
112 /** Dasselbe Projekt mit einem Lastzuganteil, den es nicht geben kann. */
113 function unbrauchbarerEintrag(): Project {
114 const basis = createStandardIntersectionProject('Aufteilung', STICHTAG);
115 return {
116 ...basis,
117 demands: basis.demands.map((d, index) =>
118 index === 0 ? { ...d, heavyVehicleShare: 0.2, lastzugAnteil: 1.5 } : d,
119 ),
120 };
121 }
122
123 function ausdruck(project: Project): string {
124 const plan = buildSignalPlan(project);
125 const bericht = validateProject(project, plan, STICHTAG);
126 return pdfText(buildProjectPdf(project, plan, bericht, DEFAULT_PDF_OPTIONS, STICHTAG));
127 }
128
129 /**
130 * Die Tabellenausgabe als Felder je Zeile.
131 *
132 * Mit Anfuehrungszeichen gelesen und nicht mit `split(';')`: Die Spalte
133 * "Gleichung für fSV" nennt die Annahme hinter Gl. 2-6 und traegt dafuer ein
134 * Semikolon, das die Ausgabe ordnungsgemaess in Anfuehrungszeichen setzt. Ein
135 * naives Trennen zerschnitte genau die Zelle, um die es hier geht.
136 */
137 function tabelle(project: Project): string[][] {
138 const text = signalGroupsCsv(project, buildSignalPlan(project)).replace('', '');
139 const zeilen: string[][] = [];
140 let zeile: string[] = [];
141 let feld = '';
142 let inAnfuehrung = false;
143
144 for (let i = 0; i < text.length; i += 1) {
145 const z = text[i]!;
146 if (inAnfuehrung) {
147 if (z === '"') {
148 if (text[i + 1] === '"') {
149 feld += '"';
150 i += 1;
151 } else inAnfuehrung = false;
152 } else feld += z;
153 continue;
154 }
155 if (z === '"') inAnfuehrung = true;
156 else if (z === ';') {
157 zeile.push(feld);
158 feld = '';
159 } else if (z === '\r') continue;
160 else if (z === '\n') {
161 zeile.push(feld);
162 zeilen.push(zeile);
163 zeile = [];
164 feld = '';
165 } else feld += z;
166 }
167 if (feld !== '' || zeile.length > 0) {
168 zeile.push(feld);
169 zeilen.push(zeile);
170 }
171 return zeilen;
172 }
173
174 const spalte = (zeilen: string[][], name: string): string[] => {
175 const index = zeilen[0]!.indexOf(name);
176 expect(index, `Spalte "${name}" muss es geben`).toBeGreaterThanOrEqual(0);
177 return zeilen.slice(1).map((z) => z[index] ?? '');
178 };
179
180 // --- Die Rechnung selbst, als Grundlage der Erwartungen ---------------------
181
182 describe('Belegzahlen der vier Datenlagen', () => {
183 it('rechnet die vier Faktoren, auf die sich Ausdruck und Tabelle berufen', () => {
184 const pSV = 0.2;
185 // Das fehlende Feld und ein Feld mit dem Wert `undefined` sind hier nicht
186 // dasselbe (exactOptionalPropertyTypes): "nicht erfasst" heisst, dass es
187 // den Schluessel nicht gibt.
188 const fsv = (lastzugAnteil?: number): number =>
189 schwerverkehrsangabe(
190 lastzugAnteil === undefined
191 ? { heavyVehicleShare: pSV }
192 : { heavyVehicleShare: pSV, lastzugAnteil },
193 ).fsv;
194
195 expect(fsv(1)).toBeCloseTo(1.3, 12);
196 expect(fsv(undefined)).toBeCloseTo(1.18, 12);
197 expect(fsv(0)).toBeCloseTo(1.15, 12);
198 expect(fsv(0.4)).toBeCloseTo(1.21, 12);
199
200 // Der Pauschalwert IST Gl. 2-5 mit 20 % Lastzuegen (V 400, S. 10) - die
201 // Erfassung ist eine Verfeinerung derselben Gleichung, kein anderes
202 // Verfahren.
203 expect(fsv(HBS_LASTZUGANTEIL_BEI_UNBEKANNTER_AUFTEILUNG)).toBeCloseTo(fsv(undefined), 12);
204
205 // Mutationsnachweis: Ohne die Aufteilung waeren alle vier gleich - dann
206 // pruefte dieser Test nichts.
207 expect(schwerverkehrsfaktor(pSV)).toBeCloseTo(1.18, 12);
208 expect(fsv(1)).not.toBeCloseTo(schwerverkehrsfaktor(pSV), 6);
209 expect(fsv(0)).not.toBeCloseTo(schwerverkehrsfaktor(pSV), 6);
210 });
211
212 it('unterscheidet "0 % gezaehlt" von "nicht erfasst"', () => {
213 // Die eine Angabe fuehrt auf Gl. 2-5, die andere auf Gl. 2-6, und beide
214 // ergeben verschiedene Saettigungsverkehrsstaerken. Eine Ausgabe, die die
215 // leere Zelle als 0 schreibt, macht aus einer fehlenden Zaehlung eine
216 // guenstige Behauptung.
217 const gezaehlt = schwerverkehrsangabe({ heavyVehicleShare: 0.2, lastzugAnteil: 0 });
218 const ohne = schwerverkehrsangabe({ heavyVehicleShare: 0.2 });
219
220 expect(gezaehlt.mitAufteilung).toBe(true);
221 expect(gezaehlt.lastzuganteilProzent).toBe('0');
222 expect(ohne.mitAufteilung).toBe(false);
223 expect(ohne.lastzuganteilProzent).toBe('');
224 expect(gezaehlt.fsv).not.toBeCloseTo(ohne.fsv, 6);
225 });
226
227 it('verwirft einen unbrauchbaren Eintrag und sagt es - kein stiller Rueckfall', () => {
228 const verworfen = schwerverkehrsangabe({ heavyVehicleShare: 0.2, lastzugAnteil: 1.5 });
229
230 expect(verworfen.mitAufteilung).toBe(false);
231 expect(verworfen.gleichungKurz).toBe('Gl. 2-6');
232 expect(verworfen.fsv).toBeCloseTo(schwerverkehrsfaktor(0.2), 12);
233 // Der Unterschied zum Fall "nichts eingetragen" steht in den Worten - der
234 // Bearbeiter hat etwas eingetragen, und das darf die Unterlage nicht
235 // verschweigen.
236 expect(verworfen.datenlage).toContain('verworfen');
237 expect(verworfen.gleichungSpalte).toContain('unbrauchbar');
238 expect(schwerverkehrsangabe({ heavyVehicleShare: 0.2 }).datenlage).not.toContain('verworfen');
239 });
240 });
241
242 // --- Ausdruck: die Leistungstabelle -----------------------------------------
243
244 describe('Leistungstabelle nennt Anteil, Aufteilung und die Gleichung', () => {
245 it('druckt je Strom den Anteil und - wo erfasst - den Lastzuganteil', () => {
246 const text = ausdruck(vierDatenlagen());
247
248 expect(text).toContain(gedruckt('20 % Lastzüge 100 % des Schwerverkehrs fSV = 1,3 (Gl. 2-5)'));
249 expect(text).toContain(gedruckt('20 % Aufteilung nicht erfasst fSV = 1,18 (Gl. 2-6)'));
250 expect(text).toContain(gedruckt('20 % Lastzüge 0 % des Schwerverkehrs fSV = 1,15 (Gl. 2-5)'));
251 expect(text).toContain(gedruckt('20 % Lastzüge 40 % des Schwerverkehrs fSV = 1,21 (Gl. 2-5)'));
252
253 // Die Spaltenueberschrift sagt, was die drei Zeilen der Zelle sind.
254 expect(text).toContain(gedruckt('Schwerverkehr (Anteil, Aufteilung, fSV)'));
255 });
256
257 it('druckt einen Faktor, der zur Saettigungsverkehrsstaerke derselben Zeile gehoert', () => {
258 /*
259 * Der Befund, um den es geht: Der Plan rechnete mit Gl. 2-5, der Ausdruck
260 * druckte den Pauschalwert. Die Gegenprobe ist qS · fSV = Grundwert je
261 * Fahrstreifen - sie geht nur auf, wenn beide Spalten aus derselben
262 * Datenlage stammen.
263 */
264 const projekt = vierDatenlagen();
265 const plan = buildSignalPlan(projekt);
266 const text = ausdruck(projekt);
267
268 let geprueft = 0;
269 for (const gruppe of plan.groups) {
270 const nachfrage = projekt.demands.find((d) => d.signalGroupId === gruppe.groupId);
271 const stammdaten = projekt.signalGroups.find((g) => g.id === gruppe.groupId);
272 if (!nachfrage || !stammdaten || gruppe.capacity === null) continue;
273 const angabe = schwerverkehrsangabe(nachfrage);
274 const grundwert = saturationFlow(stammdaten.movement, stammdaten.lanes, plan.defaults);
275
276 expect(gruppe.capacity.saturationFlow * angabe.fsv, gruppe.name).toBeCloseTo(grundwert, 6);
277 // Beides steht gedruckt da: der Faktor und die Zahl, die zu ihm gehoert.
278 expect(text, gruppe.name).toContain(gedruckt(`fSV = ${fmt.numShort(angabe.fsv, 3)}`));
279 expect(text, gruppe.name).toContain(
280 gedruckt(fmt.vehiclesPerHour(gruppe.capacity.saturationFlow)),
281 );
282 geprueft += 1;
283 }
284 expect(geprueft, 'Vorbedingung: es gibt Gruppen mit Kapazität').toBe(4);
285
286 // Die vier Zahlen ausgeschrieben - und der Altstand daneben: Er haette
287 // ueberall 1,18 gedruckt, auch neben 1.538 und 1.739 Fz/h.
288 for (const zahl of ['1.538 Fz/h', '1.695 Fz/h', '1.739 Fz/h', '1.653 Fz/h']) {
289 expect(text).toContain(gedruckt(zahl));
290 }
291 expect(text).not.toContain(gedruckt('fSV = 1,18 (Gl. 2-5)'));
292 });
293
294 it('nennt den verworfenen Eintrag als solchen', () => {
295 const text = ausdruck(unbrauchbarerEintrag());
296
297 expect(text).toContain(gedruckt('20 % Aufteilung unbrauchbar, verworfen fSV = 1,18 (Gl. 2-6)'));
298 // Ein Verwerfen, das die Unterlage als "nicht erfasst" ausgibt, waere
299 // gegenueber dem Bearbeiter schlicht falsch.
300 expect(text).not.toContain(gedruckt('20 % Aufteilung nicht erfasst fSV = 1,18'));
301 });
302 });
303
304 // --- Ausdruck: der Rechenweg unter der Tabelle ------------------------------
305
306 describe('Rechenweg nennt beide Gleichungen und die Annahme dahinter', () => {
307 it('nennt Gl. 2-5 und Gl. 2-6 und sagt, was ueber sie entscheidet', () => {
308 const text = ausdruck(vierDatenlagen());
309
310 expect(text).toContain(
311 gedruckt('fSV = (qLV + 1,75 · qLkw+Bus + 2,5 · qLkwK) / qKfz (Gl. 2-5)'),
312 );
313 expect(text).toContain(
314 gedruckt('fSV = (qLV + 1,9 · qSV) / qKfz, also fSV = 1 + 0,9 · pSV (Gl. 2-6)'),
315 );
316 expect(text).toContain(gedruckt('entscheidet die Datenlage DES EINZELNEN STROMS'));
317 // Die Behauptung des Altstands - sie ist seit P14 falsch.
318 expect(text).not.toContain(gedruckt('erfasst keine Aufteilung des Schwerverkehrs'));
319 });
320
321 it('rechnet die Annahme hinter dem Pauschalwert vor', () => {
322 // V 400, S. 10: "Bei einem Anteil der Lkw mit Anhänger und Sattel-Kfz
323 // (Fahrzeugklasse LkwK) am Schwerverkehr von 20 % ergibt sich der
324 // Funktionsverlauf der Gleichung (2-6)." Nachgerechnet ohne Rundung.
325 const text = ausdruck(vierDatenlagen());
326
327 expect(text).toContain(gedruckt('1,75 · 0,80 + 2,50 · 0,20 = 1,90'));
328 expect(text).toContain(gedruckt('Gl. 2-6 ist damit Gl. 2-5 mit 20 % Lastzügen'));
329 expect(
330 HBS_PKW_GLEICHWERTE.lkwUndBus * (1 - HBS_LASTZUGANTEIL_BEI_UNBEKANNTER_AUFTEILUNG) +
331 HBS_PKW_GLEICHWERTE.lkwMitAnhaenger * HBS_LASTZUGANTEIL_BEI_UNBEKANNTER_AUFTEILUNG,
332 ).toBeCloseTo(HBS_PKW_GLEICHWERTE.schwerverkehrPauschal, 12);
333 });
334
335 it('zaehlt, welcher Weg in DIESEM Plan gegriffen hat', () => {
336 const text = ausdruck(vierDatenlagen());
337
338 expect(aufteilungslage(vierDatenlagen())).toEqual({ mitAufteilung: 3, ohneAufteilung: 1 });
339 expect(text).toContain(
340 gedruckt('In diesem Plan rechnen 3 Ströme nach Gl. 2-5 und 1 Strom nach Gl. 2-6.'),
341 );
342 });
343
344 it('nennt die Fehlerrichtung, solange ein Strom ohne Aufteilung dabei ist', () => {
345 const text = ausdruck(vierDatenlagen());
346
347 /*
348 * Anker ist der SCHLUSSSATZ des Absatzes und nicht die Ueberschrift
349 * "FEHLERRICHTUNG, WO DIE AUFTEILUNG FEHLT": Dieselbe Ueberschrift steht
350 * auch im Fundstellenverzeichnis (quellen.ts, 'hbs-kapazitaet') und wuerde
351 * die Probe unten unbrauchbar machen.
352 */
353 expect(text).toContain(gedruckt('Für Ströme MIT erfasster Aufteilung entfällt dieser Posten.'));
354 expect(text).toContain(gedruckt('ZU GROSS'));
355 // Beide Richtungen, beziffert aus derselben Funktion, mit der gerechnet
356 // wird: 2000/1,180 = 1.695 statt 2000/1,300 = 1.538 Fz/h.
357 expect(text).toContain(gedruckt('um 10,2 % (1.695 Fz/h statt 1.538 Fz/h)'));
358 expect(text).toContain(gedruckt('bis zu 31,6 %'));
359 expect(text).toContain(
360 gedruckt('rechnet das Programm 2,5 % zu klein, also auf der sicheren Seite'),
361 );
362 });
363
364 it('laesst den Vorbehalt weg, wo jeder Strom eine erfasste Aufteilung hat', () => {
365 const text = ausdruck(ueberallErfasst());
366
367 expect(aufteilungslage(ueberallErfasst())).toEqual({ mitAufteilung: 4, ohneAufteilung: 0 });
368 expect(text).toContain(gedruckt('gerechnet wurde durchgehend nach Gl. 2-5'));
369 expect(text).toContain(gedruckt('geht damit in keine Zahl dieses Plans ein'));
370 // Mutationsnachweis: Genau diese Saetze stehen im Plan mit einem Strom
371 // ohne Aufteilung.
372 expect(text).not.toContain(
373 gedruckt('Für Ströme MIT erfasster Aufteilung entfällt dieser Posten.'),
374 );
375 expect(text).not.toContain(gedruckt('um 10,2 % (1.695 Fz/h statt 1.538 Fz/h)'));
376 });
377
378 it('behaelt den Vorbehalt, wo die Datenlage nicht durchgereicht wird', () => {
379 // Der Vorbehalt darf nur entfallen, wenn NACHGEWIESEN ist, dass jeder
380 // Strom gezaehlt ist - nicht schon dann, wenn der Aufrufer nichts weiss.
381 const ohneLage = schwerverkehrSatz(RILSA_DEFAULTS);
382
383 expect(ohneLage).toContain('FEHLERRICHTUNG, WO DIE AUFTEILUNG FEHLT');
384 expect(ohneLage).not.toContain('In diesem Plan');
385 expect(
386 schwerverkehrSatz(RILSA_DEFAULTS, { mitAufteilung: 4, ohneAufteilung: 0 }),
387 ).not.toContain('FEHLERRICHTUNG, WO DIE AUFTEILUNG FEHLT');
388 });
389 });
390
391 // --- Tabellenausgabe --------------------------------------------------------
392
393 describe('Tabellenausgabe fuehrt Lastzuganteil, fSV und die Gleichung', () => {
394 it('fuehrt die drei Spalten in der Reihenfolge des Rechenwegs', () => {
395 const zeilen = tabelle(vierDatenlagen());
396 const kopf = zeilen[0]!;
397 const anteil = kopf.indexOf('Schwerverkehrsanteil [%]');
398
399 expect(anteil).toBeGreaterThanOrEqual(0);
400 expect(kopf[anteil + 1]).toBe('Lastzuganteil am Schwerverkehr [%] (leer = nicht erfasst)');
401 expect(kopf[anteil + 2]).toBe('Schwerverkehrsfaktor fSV');
402 expect(kopf[anteil + 3]).toBe('Gleichung für fSV (HBS 2015)');
403 });
404
405 it('schreibt den Anteil, mit dem gerechnet wurde - nicht den eingetragenen', () => {
406 /*
407 * Derselbe Befund wie im Pruefbericht, eine Ausgabe weiter: Tabelle und
408 * Ausdruck bildeten die Prozentzahl je selbst aus
409 * `demand.heavyVehicleShare`, waehrend fSV daneben aus dem auf 0 bis 1
410 * begrenzten Anteil entsteht. Bei 150 % Schwerverkehr - ueber Datei und
411 * Oberflaeche nicht erreichbar, ueber Fremdwerkzeuge sehr wohl - stand
412 * "150,0 %" neben "1,900", also neben dem Faktor fuer 100 %.
413 */
414 const basis = vierDatenlagen();
415 const projekt: Project = {
416 ...basis,
417 demands: basis.demands.map((d, index) =>
418 index === 1 ? { ...d, heavyVehicleShare: 1.5 } : d,
419 ),
420 };
421 const zeilen = tabelle(projekt);
422 const namen = spalte(zeilen, 'Signalgruppe');
423 const anteil = spalte(zeilen, 'Schwerverkehrsanteil [%]');
424 const faktor = spalte(zeilen, 'Schwerverkehrsfaktor fSV');
425 const i = namen.indexOf(projekt.signalGroups[1]!.name);
426
427 expect(anteil[i]).toBe('100');
428 expect(faktor[i]).toBe('1,9');
429
430 // Und der Ausdruck nimmt dieselbe Angabe.
431 const zelle = schwerverkehrsangabe({ heavyVehicleShare: 1.5 });
432 expect(zelle.anteilProzent).toBe('100');
433 expect(zelle.fsv).toBeCloseTo(1.9, 9);
434 });
435
436 it('schreibt je Zeile Aufteilung, Faktor und Gleichung', () => {
437 const projekt = vierDatenlagen();
438 const zeilen = tabelle(projekt);
439 const namen = spalte(zeilen, 'Signalgruppe');
440 const lastzug = spalte(zeilen, 'Lastzuganteil am Schwerverkehr [%] (leer = nicht erfasst)');
441 const faktor = spalte(zeilen, 'Schwerverkehrsfaktor fSV');
442 const gleichung = spalte(zeilen, 'Gleichung für fSV (HBS 2015)');
443
444 const zeile = (index: number): number => namen.indexOf(projekt.signalGroups[index]!.name);
445
446 // Lauter Lastzuege.
447 expect(lastzug[zeile(0)]).toBe('100');
448 expect(faktor[zeile(0)]).toBe('1,3');
449 expect(gleichung[zeile(0)]).toBe('2-5 (Aufteilung erfasst)');
450
451 // Keine Angabe: leere Zelle, Pauschalwert, und die Annahme steht dabei.
452 expect(lastzug[zeile(1)]).toBe('');
453 expect(faktor[zeile(1)]).toBe('1,18');
454 expect(gleichung[zeile(1)]).toBe(
455 '2-6 (keine Aufteilung erfasst; Annahme 20 % Lastzüge am Schwerverkehr)',
456 );
457
458 // Gezaehlte 0 % - andere Zelle, andere Gleichung, anderer Faktor als oben.
459 expect(lastzug[zeile(2)]).toBe('0');
460 expect(faktor[zeile(2)]).toBe('1,15');
461 expect(gleichung[zeile(2)]).toBe('2-5 (Aufteilung erfasst)');
462 expect(faktor[zeile(2)]).not.toBe(faktor[zeile(1)]);
463
464 expect(lastzug[zeile(3)]).toBe('40');
465 expect(faktor[zeile(3)]).toBe('1,21');
466 });
467
468 it('haelt fSV und Saettigungsverkehrsstaerke derselben Zeile zusammen', () => {
469 /*
470 * Derselbe Befund wie im Ausdruck: Die Tabelle bildete den Faktor ohne die
471 * Aufteilung und schrieb ihn neben eine Saettigungsverkehrsstaerke, die
472 * aus einem anderen Faktor entstanden ist. Gegenprobe qS · fSV = Grundwert.
473 */
474 const projekt = vierDatenlagen();
475 const plan = buildSignalPlan(projekt);
476 const zeilen = tabelle(projekt);
477 const namen = spalte(zeilen, 'Signalgruppe');
478 const faktor = spalte(zeilen, 'Schwerverkehrsfaktor fSV');
479 const qS = spalte(zeilen, 'Sättigungsverkehrsstärke [Fz/h]');
480
481 let geprueft = 0;
482 for (const gruppe of projekt.signalGroups) {
483 const i = namen.indexOf(gruppe.name);
484 if (i < 0 || faktor[i] === '') continue;
485 const zahl = (text: string): number => Number(text.replace(/\./g, '').replace(',', '.'));
486 const grundwert = saturationFlow(gruppe.movement, gruppe.lanes, plan.defaults);
487
488 // Die Tabelle rundet die Saettigungsverkehrsstaerke auf ganze Fz/h; der
489 // Faktor traegt drei Stellen. Die Toleranz prueft die Zuordnung, nicht
490 // die Rundung.
491 //
492 // WAS DIESER FALL FAENGT (Fassung 5.6.0): die HALBE Umstellung - Plan
493 // nach Gl. 2-5, Ausfuhr weiter nach Gl. 2-6 (oder umgekehrt). Dann
494 // stuende neben einer Saettigungsverkehrsstaerke aus 1,300 ein Faktor
495 // 1,180, und das Produkt verfehlte den Grundwert um 10 %. Hier stand "der
496 // Altstand lag hier um bis zu 10 % daneben" - das trifft nicht zu: Auf
497 // dem Altstand rechnen Plan UND Tabelle nach Gl. 2-6, das Produkt ergibt
498 // weiterhin den Grundwert, und der Fall besteht. Er ist eine
499 // Zusammenhangs- und keine Altstandspruefung.
500 expect(zahl(qS[i]!) * zahl(faktor[i]!), gruppe.name).toBeCloseTo(grundwert, -1);
501 geprueft += 1;
502 }
503 expect(geprueft, 'Vorbedingung: es gibt Zeilen mit Faktor').toBe(4);
504 });
505
506 it('laesst die drei Spalten fuer Fussgaenger und Radverkehr leer', () => {
507 // Fuer sie wird keine Saettigungsverkehrsstaerke gebildet; ein Faktor
508 // daneben laese sich als Eingangsgroesse (Befund B4).
509 const projekt = vierDatenlagen();
510 const zeilen = tabelle(projekt);
511 const namen = spalte(zeilen, 'Signalgruppe');
512 const lastzug = spalte(zeilen, 'Lastzuganteil am Schwerverkehr [%] (leer = nicht erfasst)');
513 const faktor = spalte(zeilen, 'Schwerverkehrsfaktor fSV');
514 const gleichung = spalte(zeilen, 'Gleichung für fSV (HBS 2015)');
515
516 const fussRad = projekt.signalGroups.filter((g) => g.mode === 'fuss' || g.mode === 'rad');
517 expect(fussRad.length).toBeGreaterThan(0);
518 for (const gruppe of fussRad) {
519 const i = namen.indexOf(gruppe.name);
520 expect(lastzug[i], gruppe.name).toBe('');
521 expect(faktor[i], gruppe.name).toBe('');
522 expect(gleichung[i], gruppe.name).toBe('');
523 }
524 });
525
526 it('nennt den verworfenen Eintrag in der Gleichungsspalte', () => {
527 const projekt = unbrauchbarerEintrag();
528 const zeilen = tabelle(projekt);
529 const namen = spalte(zeilen, 'Signalgruppe');
530 const lastzug = spalte(zeilen, 'Lastzuganteil am Schwerverkehr [%] (leer = nicht erfasst)');
531 const gleichung = spalte(zeilen, 'Gleichung für fSV (HBS 2015)');
532 const i = namen.indexOf(projekt.signalGroups[0]!.name);
533
534 // Die Zahlenspalte bleibt leer - 150 neben "Gl. 2-6" waere eine Angabe,
535 // die in keine Rechnung eingegangen ist. Der Grund steht in der Spalte
536 // daneben und als Warnung im Pruefbericht.
537 expect(lastzug[i]).toBe('');
538 expect(gleichung[i]).toContain('unbrauchbar und verworfen');
539 expect(gleichung[i]).toContain('Annahme 20 % Lastzüge am Schwerverkehr');
540 });
541 });