lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests export schwerverkehrsaufteilung.test.ts
| 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 | }); |