lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests export pdfBewertung.test.ts
| 1 | import { inflateSync } from 'node:zlib'; |
| 2 | import { describe, expect, it } from 'vitest'; |
| 3 | import { buildProjectPdf, DEFAULT_PDF_OPTIONS, type PdfOptions } from '@/services/export/pdf'; |
| 4 | import { buildSignalPlan } from '@/domain/plan/signalPlan'; |
| 5 | import { validateProject } from '@/domain/validation'; |
| 6 | import { createSignalGroup, createStandardIntersectionProject } from '@/domain/model/factory'; |
| 7 | import { SERVICE_LEVEL_SCALES } from '@/domain/rilsa/constants'; |
| 8 | import { PRUEFSTAND_LABELS, QUELLEN } from '@/domain/rilsa/quellen'; |
| 9 | import { quellenOhneFeld, stufentafelSatz } from '@/services/export/bewertung'; |
| 10 | import type { Project } from '@/domain/model/project'; |
| 11 | import * as fmt from '@/ui/format'; |
| 12 | |
| 13 | /* |
| 14 | * Befunde B1 bis B4: Der Leistungsabschnitt des Ausdrucks behauptete "Bewertung |
| 15 | * nach HBS 2015", waehrend fuer beide Einstellungen dieselbe HCM-Formel |
| 16 | * gerechnet und nach der Kfz-Tafel des HBS 2001 bewertet wurde; Fussgaenger |
| 17 | * fehlten mit der Begruendung, ihre Leistungsfaehigkeit sei "nicht |
| 18 | * massgebend" - das HBS 2015 bewertet sie nach der maximalen Wartezeit. Diese |
| 19 | * Tests sichern, dass der Ausdruck nur noch sagt, was gerechnet wurde. |
| 20 | */ |
| 21 | |
| 22 | const STICHTAG = new Date('2026-01-01T12:00:00Z'); |
| 23 | |
| 24 | /** Sichtbaren Text aus dem PDF lesen; die Inhaltsstroeme sind komprimiert. */ |
| 25 | function pdfText(bytes: Uint8Array): string { |
| 26 | const roh = new TextDecoder('latin1').decode(bytes); |
| 27 | const teile: string[] = []; |
| 28 | const suche = /stream\r?\n/g; |
| 29 | let treffer: RegExpExecArray | null; |
| 30 | |
| 31 | while ((treffer = suche.exec(roh)) !== null) { |
| 32 | const start = treffer.index + treffer[0].length; |
| 33 | const ende = roh.indexOf('endstream', start); |
| 34 | if (ende < 0) continue; |
| 35 | let inhalt: string; |
| 36 | try { |
| 37 | inhalt = inflateSync(Buffer.from(roh.slice(start, ende), 'latin1')).toString('latin1'); |
| 38 | } catch { |
| 39 | continue; |
| 40 | } |
| 41 | for (const t of inhalt.matchAll(/\(((?:[^()\\]|\\.)*)\)\s*Tj/g)) { |
| 42 | teile.push(t[1]!.replace(/\\([()\\])/g, '$1')); |
| 43 | } |
| 44 | } |
| 45 | return teile.join('\n'); |
| 46 | } |
| 47 | |
| 48 | /** |
| 49 | * Nur der Leistungsabschnitt, ohne das Fundstellenverzeichnis: Dort steht |
| 50 | * "HBS 2015" zu Recht als Herkunft der Saettigungsverkehrsstaerke - auch wenn |
| 51 | * nach HCM bewertet wird. Die Behauptung, um die es hier geht, ist die |
| 52 | * "Bewertung nach ...". |
| 53 | */ |
| 54 | const NUR_LEISTUNG: PdfOptions = { |
| 55 | ...DEFAULT_PDF_OPTIONS, |
| 56 | includeSignalGroups: false, |
| 57 | includeIntergreenMatrix: false, |
| 58 | includePhases: false, |
| 59 | includePlan: false, |
| 60 | includeReport: false, |
| 61 | includeSources: false, |
| 62 | }; |
| 63 | |
| 64 | /** Fliesstext ohne Zeilenumbrueche - der Ausdruck bricht Saetze fuer den Satzspiegel um. */ |
| 65 | function ausdruck(project: Project, options: PdfOptions = NUR_LEISTUNG): string { |
| 66 | const plan = buildSignalPlan(project); |
| 67 | return pdfText( |
| 68 | buildProjectPdf(project, plan, validateProject(project, plan, STICHTAG), options), |
| 69 | ).replace(/\s+/g, ' '); |
| 70 | } |
| 71 | |
| 72 | function vollstaendig(verfahren: 'HBS' | 'HCM' = 'HBS'): Project { |
| 73 | const base = createStandardIntersectionProject('Bewertung', STICHTAG); |
| 74 | return { |
| 75 | ...base, |
| 76 | meta: { ...base.meta, planner: 'O. Willerding' }, |
| 77 | settings: { ...base.settings, serviceLevelScale: verfahren }, |
| 78 | }; |
| 79 | } |
| 80 | |
| 81 | describe('Ausdruck nennt das Bewertungsverfahren, das gerechnet wurde', () => { |
| 82 | it('sagt "Bewertung nach HBS 2015" nur beim HBS-Verfahren, sonst "Bewertung nach HCM"', () => { |
| 83 | const hbs = ausdruck(vollstaendig('HBS')); |
| 84 | expect(hbs).toContain('Bewertung nach HBS 2015'); |
| 85 | expect(hbs).not.toContain('Bewertung nach HCM'); |
| 86 | // Die Formelkette, die tatsaechlich gerechnet wird - nicht die HCM-Formel |
| 87 | // unter fremder Ueberschrift (Befund B2). |
| 88 | expect(hbs).toContain('tA = tF + 1 s'); |
| 89 | expect(hbs).toContain('tW = tW,G + tW,R'); |
| 90 | expect(hbs).toContain('Instationaritätsfaktor fin = 1,1'); |
| 91 | expect(hbs).not.toContain('d1 + d2'); |
| 92 | |
| 93 | const hcm = ausdruck(vollstaendig('HCM')); |
| 94 | expect(hcm).toContain('Bewertung nach HCM'); |
| 95 | expect(hcm).not.toContain('Bewertung nach HBS 2015'); |
| 96 | /* |
| 97 | * Ohne Fundstellenverzeichnis darf "HBS 2015" im HCM-Ausdruck nur dort |
| 98 | * vorkommen, wo es die HERKUNFT einer Eingangsgroesse ist - nie als |
| 99 | * Bewertungsverfahren. Zwei solche Stellen gibt es: |
| 100 | * |
| 101 | * - die ehrliche Anmerkung zur Fuss/Rad-Bewertung (Befund B4), |
| 102 | * - der Rechenweg der Saettigungsverkehrsstaerke (Fassung 5.5.0, Frage E6): Der |
| 103 | * Anpassungsfaktor fSV stammt auch in einem HCM-Projekt aus dem |
| 104 | * HBS 2015, weil die Saettigungsverkehrsstaerke es tut. Bis zum |
| 105 | * 15.08.2026 stand an dieser Stelle "Pkw-Aequivalent 2,0" - der Wert |
| 106 | * des HCM 2010, gedruckt unter der Ueberschrift HBS. Die Nennung des |
| 107 | * Regelwerks ist hier also gerade die Behebung des Befunds und nicht |
| 108 | * sein Rueckfall. |
| 109 | */ |
| 110 | const ohneHerkunftsangaben = hcm |
| 111 | .replace(/nach der das HBS 2015 bewerten würde/g, '') |
| 112 | .replace(/Der Schwerverkehr geht nach HBS 2015 über den Anpassungsfaktor fSV/g, ''); |
| 113 | expect(ohneHerkunftsangaben).not.toContain('HBS 2015'); |
| 114 | expect(hcm).toContain('d = d1 + d2'); |
| 115 | expect(hcm).toContain('T = 0,25 h'); |
| 116 | expect(hcm).not.toContain('tW,R'); |
| 117 | }); |
| 118 | |
| 119 | it('druckt die Kfz-Stufentafel des HBS 2015 aus den Konstanten - E ohne Obergrenze, F bei q > C', () => { |
| 120 | // Befund B1: Bis dahin stand die Tafel des HBS 2001 (E bis 100 s, F |
| 121 | // darueber) unter der Ueberschrift "HBS 2015". Die gedruckte Tafel kommt |
| 122 | // jetzt aus denselben Konstanten wie die Bewertung. |
| 123 | const text = ausdruck(vollstaendig('HBS')); |
| 124 | expect(text).toContain('A bis 20 s, B bis 35 s, C bis 50 s, D bis 70 s, E darüber'); |
| 125 | expect(text).toContain('Stufe F bei Überlastung (q > C)'); |
| 126 | expect(text).not.toContain('E bis 100 s'); |
| 127 | // Gegenprobe gegen die Konstante selbst. |
| 128 | const e = SERVICE_LEVEL_SCALES.HBS.kfz.find((s) => s.grade === 'E')!; |
| 129 | expect(Number.isFinite(e.maxDelay)).toBe(false); |
| 130 | }); |
| 131 | |
| 132 | it('fuehrt Fussgaenger mit der maximalen Wartezeit auf und streicht den falschen Satz', () => { |
| 133 | // Befund B4: Der alte Satz "Fussgaengersignalgruppen sind nicht |
| 134 | // aufgefuehrt, da ihre Leistungsfaehigkeit ... nicht massgebend ist" war |
| 135 | // falsch - das HBS 2015 bewertet sie nach der laengsten Sperrzeit. |
| 136 | const projekt = vollstaendig('HBS'); |
| 137 | const plan = buildSignalPlan(projekt); |
| 138 | const text = ausdruck(projekt); |
| 139 | |
| 140 | expect(text).not.toContain('nicht aufgeführt'); |
| 141 | expect(text).not.toContain('nicht maßgebend'); |
| 142 | expect(text).toContain('Fußgänger und Radverkehr'); |
| 143 | expect(text).toContain('Längste Sperrzeit = maximale Wartezeit'); |
| 144 | expect(text).toContain('nach der maximalen Wartezeit bewertet'); |
| 145 | expect(text).toContain('A bis 30 s, B bis 40 s, C bis 55 s, D bis 70 s, E bis 85 s, F darüber'); |
| 146 | |
| 147 | // Die gedruckte maximale Wartezeit ist die laengste Sperrzeit der Gruppe - |
| 148 | // und die Stufe dazu ist die des Fachkerns. |
| 149 | const fuss = plan.groups.filter((g) => g.mode === 'fuss'); |
| 150 | expect(fuss.length).toBeGreaterThan(0); |
| 151 | for (const gruppe of fuss) { |
| 152 | expect(gruppe.delay, gruppe.name).not.toBeNull(); |
| 153 | const maximal = gruppe.delay!.maximumDelay; |
| 154 | expect(text, gruppe.name).toContain(`${maximal.toFixed(1).replace('.', ',')} s`); |
| 155 | expect(gruppe.delay!.serviceLevel, gruppe.name).not.toBeNull(); |
| 156 | expect(gruppe.delay!.serviceLevel!.kriterium).toBe('maximale-wartezeit'); |
| 157 | } |
| 158 | }); |
| 159 | |
| 160 | it('vergibt nach HCM fuer Fussgaenger keine Stufe und sagt das', () => { |
| 161 | const projekt = vollstaendig('HCM'); |
| 162 | const plan = buildSignalPlan(projekt); |
| 163 | for (const gruppe of plan.groups.filter((g) => g.mode === 'fuss')) { |
| 164 | expect(gruppe.delay?.serviceLevel ?? null, gruppe.name).toBeNull(); |
| 165 | } |
| 166 | const text = ausdruck(projekt); |
| 167 | expect(text).toContain('Fußgänger und Radverkehr'); |
| 168 | expect(text).toContain('Nach HCM wird für Fußgänger und Radverkehr keine Stufe vergeben'); |
| 169 | // Auch ohne Stufe steht die maximale Wartezeit da. |
| 170 | expect(text).toContain('Längste Sperrzeit = maximale Wartezeit'); |
| 171 | }); |
| 172 | |
| 173 | it('kennzeichnet die Stufe F bei Ueberlastung als "überlastet"', () => { |
| 174 | // Nach HBS 2015 haengt F fuer Kfz an q > C, nicht an der Wartezeit. Ein |
| 175 | // blosses "F" neben einer Wartezeit liesse sich mit der gedruckten Tafel |
| 176 | // nicht in Einklang bringen. |
| 177 | const roh = vollstaendig('HBS'); |
| 178 | const projekt: Project = { |
| 179 | ...roh, |
| 180 | demands: roh.demands.map((d, i) => (i === 0 ? { ...d, volume: 3000 } : d)), |
| 181 | }; |
| 182 | const plan = buildSignalPlan(projekt); |
| 183 | const ueberlastet = plan.groups.filter( |
| 184 | (g) => g.delay?.serviceLevel?.kriterium === 'ueberlastung', |
| 185 | ); |
| 186 | expect(ueberlastet.length, 'die Vorbereitung muss eine Ueberlastung erzeugen').toBeGreaterThan( |
| 187 | 0, |
| 188 | ); |
| 189 | |
| 190 | const text = ausdruck(projekt); |
| 191 | expect(text).toContain('F (überlastet)'); |
| 192 | }); |
| 193 | |
| 194 | it('druckt auf Wunsch den Rechenweg der Wartezeit mit den Zwischenwerten', () => { |
| 195 | // Grundsatz: tW muss sich aus den gedruckten Groessen |
| 196 | // nachrechnen lassen. Der Rechenweg wird wie bei den Zwischenzeiten ueber |
| 197 | // includeCalculationDetails zugeschaltet. |
| 198 | const projekt = vollstaendig('HBS'); |
| 199 | const plan = buildSignalPlan(projekt); |
| 200 | // Seit Befund C15 ist der Rechenweg voreingestellt; die Abwahl muss hier |
| 201 | // ausdruecklich gesetzt werden. Die Zeile stand bis dahin ohne Zusatz da - |
| 202 | // sie las die Voreinstellung mit. |
| 203 | const ohne = ausdruck(projekt, { ...NUR_LEISTUNG, includeCalculationDetails: false }); |
| 204 | expect(ohne).not.toContain('Rechenweg Wartezeit'); |
| 205 | |
| 206 | const mit = ausdruck(projekt, { ...NUR_LEISTUNG, includeCalculationDetails: true }); |
| 207 | expect(mit).toContain('Rechenweg Wartezeit'); |
| 208 | expect(mit).toContain('NGE'); |
| 209 | expect(mit).toContain('tW,G'); |
| 210 | expect(mit).toContain('tW,R'); |
| 211 | expect(mit).toContain('Wurzel aus'); |
| 212 | |
| 213 | // Die Zwischenwerte des Fachkerns stehen im Ausdruck. |
| 214 | const k1 = plan.groups.find((g) => g.mode === 'kfz' && g.delay !== null)!; |
| 215 | const rw = k1.delay!.rechenweg; |
| 216 | expect(rw.reststau).not.toBeNull(); |
| 217 | expect(mit).toContain(`${k1.delay!.uniformDelay.toFixed(1).replace('.', ',')} s`); |
| 218 | expect(mit).toContain(rw.reststau!.toFixed(2).replace('.', ',')); |
| 219 | |
| 220 | // Nach HCM stehen die HCM-Beiwerte im Rechenweg, nicht NGE. |
| 221 | const hcm = ausdruck(vollstaendig('HCM'), { ...NUR_LEISTUNG, includeCalculationDetails: true }); |
| 222 | expect(hcm).toContain('Rechenweg Wartezeit'); |
| 223 | expect(hcm).toContain('d1'); |
| 224 | expect(hcm).toContain('k = 0,5'); |
| 225 | expect(hcm).not.toContain('NGE'); |
| 226 | }); |
| 227 | |
| 228 | it('druckt die Abflusszeit, aus der sich die Kapazitaet nachrechnen laesst', () => { |
| 229 | // C = qS · tA/tU - ohne tA in der Tabelle ginge die Kapazitaet neben der |
| 230 | // Freigabezeit nicht auf (Befund B2, tA = tF + 1 s). |
| 231 | const projekt = vollstaendig('HBS'); |
| 232 | const plan = buildSignalPlan(projekt); |
| 233 | const text = ausdruck(projekt); |
| 234 | expect(text).toContain('Abflusszeit tA'); |
| 235 | const k1 = plan.groups.find((g) => g.mode === 'kfz' && g.capacity !== null)!; |
| 236 | expect(k1.capacity!.abflusszeit).toBeCloseTo(k1.totalGreen + 1, 3); |
| 237 | expect(text).toContain(`${k1.capacity!.abflusszeit.toFixed(1).replace('.', ',')} s`); |
| 238 | }); |
| 239 | |
| 240 | it('druckt den Auslastungsgrad mit drei Nachkommastellen', () => { |
| 241 | // Mit zwei Stellen stand bei q = 244 und C = 243 "1,00" neben "F |
| 242 | // (ueberlastet)" - die gedruckte Zahl widersprach der gedruckten Stufe. |
| 243 | // Drei Stellen wie im Rechenweg und im Pruefbericht. |
| 244 | const projekt = vollstaendig('HBS'); |
| 245 | const plan = buildSignalPlan(projekt); |
| 246 | const text = ausdruck(projekt); |
| 247 | const kfz = plan.groups.filter((g) => g.mode === 'kfz' && g.capacity !== null); |
| 248 | expect(kfz.length).toBeGreaterThan(0); |
| 249 | for (const gruppe of kfz) { |
| 250 | expect(gruppe.capacity!.degreeOfSaturation, gruppe.name).toBeDefined(); |
| 251 | const drei = fmt.ratio(gruppe.capacity!.degreeOfSaturation!, 3); |
| 252 | expect(drei, gruppe.name).toMatch(/^\d,\d{3}$/); |
| 253 | expect(text, gruppe.name).toContain(drei); |
| 254 | } |
| 255 | // Der Fall des Befunds: q knapp ueber C darf nicht als "1,00" erscheinen. |
| 256 | // Der Umlauf haengt an der Nachfrage (Webster); deshalb wird die |
| 257 | // Verkehrsstaerke aus dem Plan abgeleitet und der Plan neu gebaut. |
| 258 | const k1 = kfz[0]!; |
| 259 | const knapp: Project = { |
| 260 | ...projekt, |
| 261 | demands: projekt.demands.map((d) => |
| 262 | d.signalGroupId === k1.groupId ? { ...d, volume: k1.capacity!.capacity + 1 } : d, |
| 263 | ), |
| 264 | }; |
| 265 | const neu = buildSignalPlan(knapp).groups.find((g) => g.groupId === k1.groupId)!; |
| 266 | const x = neu.capacity!.degreeOfSaturation!; |
| 267 | const gedruckt = ausdruck(knapp); |
| 268 | if (x > 1 && x < 1.005) { |
| 269 | expect(fmt.ratio(x, 2), 'Vorbedingung des Befunds').toBe('1,00'); |
| 270 | expect(gedruckt).toContain(fmt.ratio(x, 3)); |
| 271 | expect(gedruckt).toContain('F (überlastet)'); |
| 272 | } else { |
| 273 | // Der Neuaufbau hat den Umlauf verschoben; dann gilt nur die Stellenzahl. |
| 274 | expect(gedruckt).toContain(fmt.ratio(x, 3)); |
| 275 | } |
| 276 | }); |
| 277 | }); |
| 278 | |
| 279 | describe('Signalgruppentabelle: Verkehrsstaerke nur fuer Kfz und OePNV', () => { |
| 280 | it('druckt fuer eine Radgruppe mit gespeicherter Verkehrsstaerke "–"', () => { |
| 281 | // Altdatei aus der Zeit vor Fassung 5.4.0 (Befund B4): Fuer |
| 282 | // eine Radgruppe ist eine Verkehrsstaerke gespeichert. Sie geht in keine |
| 283 | // Rechnung ein, und der Pruefbericht sagt das - im Ausdruck darf sie |
| 284 | // deshalb nicht als Eingangsgroesse erscheinen. |
| 285 | const roh = vollstaendig('HBS'); |
| 286 | const rad = createSignalGroup({ name: 'R9', mode: 'rad', index: 9 }); |
| 287 | const projekt: Project = { |
| 288 | ...roh, |
| 289 | signalGroups: [...roh.signalGroups, rad], |
| 290 | demands: [...roh.demands, { signalGroupId: rad.id, volume: 777, heavyVehicleShare: 0 }], |
| 291 | }; |
| 292 | const text = ausdruck(projekt, { |
| 293 | ...NUR_LEISTUNG, |
| 294 | includeSignalGroups: true, |
| 295 | includePerformance: false, |
| 296 | }); |
| 297 | expect(text).toContain('R9'); |
| 298 | expect(text).not.toContain('777 Fz/h'); |
| 299 | // Die Kfz-Verkehrsstaerken stehen weiterhin da. |
| 300 | expect(text).toContain(`${fmt.vehiclesPerHour(roh.demands[0]!.volume)}`); |
| 301 | }); |
| 302 | }); |
| 303 | |
| 304 | const NUR_GRUNDLAGEN: PdfOptions = { |
| 305 | ...NUR_LEISTUNG, |
| 306 | includePerformance: false, |
| 307 | includeSources: true, |
| 308 | }; |
| 309 | |
| 310 | describe('Grundlagen der Kennwerte', () => { |
| 311 | it('sagt bei HCM, dass dieses Projekt nach dem HCM bewertet und aus dem HBS nur Eingangsgroessen nimmt', () => { |
| 312 | // Die Rechtsgrundlagentafel nennt das HBS 2015 allgemein als Grundlage |
| 313 | // von Kapazitaet, Wartezeit und Qualitaetsstufen. Ohne den Satz zum |
| 314 | // Projekt hielte der Leser einer HCM-Bewertung die ganze Bewertung fuer |
| 315 | // eine nach HBS 2015. |
| 316 | const hcm = ausdruck(vollstaendig('HCM'), NUR_GRUNDLAGEN); |
| 317 | expect(hcm).toContain('Dieses Projekt bewertet nach dem HCM (Highway Capacity Manual)'); |
| 318 | expect(hcm).toContain('Wartezeit d = d1 + d2, Level of Service'); |
| 319 | expect(hcm).toContain( |
| 320 | 'Aus dem HBS 2015 stammen hier nur Sättigungsverkehrsstärke und Abbiegerfaktor', |
| 321 | ); |
| 322 | expect(hcm).not.toContain('rechnet und bewertet nach dem HBS 2015'); |
| 323 | |
| 324 | const hbs = ausdruck(vollstaendig('HBS'), NUR_GRUNDLAGEN); |
| 325 | expect(hbs).toContain('Dieses Projekt rechnet und bewertet nach dem HBS 2015'); |
| 326 | expect(hbs).not.toContain('Dieses Projekt bewertet nach dem HCM'); |
| 327 | }); |
| 328 | |
| 329 | it('fuehrt die Stufentafeln der Qualitaetsstufen im Fundstellenverzeichnis mit Pruefstand', () => { |
| 330 | // Die Quelle 'hbs-qualitaetsstufen' hat kein Vorgabenfeld und fehlte im |
| 331 | // Verzeichnis deshalb ganz: Die gedruckten Tafeln hatten weder Herkunft |
| 332 | // noch Pruefstand, obwohl OePNV- und Fuss/Rad-Tafel nur ueber |
| 333 | // Sekundaerquellen belegt sind. |
| 334 | const quelle = QUELLEN['hbs-qualitaetsstufen']; |
| 335 | /* |
| 336 | * PRUEFSTAND SEIT DEM 15.08.2026 "gesichert" (Fassung 5.5.0, Frage E5): Hier stand |
| 337 | * "abzugleichen", weil die OePNV- und die Fuss/Rad-Tafel nur ueber |
| 338 | * Sekundaerquellen belegt waren. Inzwischen liegen zwei |
| 339 | * HERAUSGEBERSEITIGE Wiedergaben derselben gedruckten HBS-Tabelle vor - |
| 340 | * der Vortrag des Verfassers von Kapitel S4 beim FGSV-Einfuehrungs- |
| 341 | * kolloquium 2015 und Tab. 2-2 des BASt-Berichts V 400 (2025). Der Test |
| 342 | * haelt weiter den Pruefstand der Quelle gegen den gedruckten fest; die |
| 343 | * Zusicherung ist also nicht schwaecher geworden, sondern folgt der |
| 344 | * Beleglage. |
| 345 | */ |
| 346 | expect(quelle.pruefstand, 'Vorbedingung: die Tafeln sind zweifach belegt').toBe('gesichert'); |
| 347 | const text = ausdruck(vollstaendig('HBS'), NUR_GRUNDLAGEN); |
| 348 | |
| 349 | expect(text).toContain('Fundstellenverzeichnis'); |
| 350 | expect(text).toContain( |
| 351 | 'Stufentafeln der Qualitätsstufen (Kfz/ÖPNV: mittlere Wartezeit; Fuß/Rad: maximale Wartezeit)', |
| 352 | ); |
| 353 | expect(text).toContain(quelle.sachgebiet); |
| 354 | // Die Werte selbst, aus den Konstanten - und die Stufe F mit ihrem Grund. |
| 355 | expect(text).toContain( |
| 356 | `Stufentafel Kfz (mittlere Wartezeit): ${stufentafelSatz(SERVICE_LEVEL_SCALES.HBS.kfz)}`, |
| 357 | ); |
| 358 | expect(text).toContain( |
| 359 | `Stufentafel ÖPNV (mittlere Wartezeit): ${stufentafelSatz(SERVICE_LEVEL_SCALES.HBS.oepnv)}`, |
| 360 | ); |
| 361 | expect(text).toContain( |
| 362 | `Stufentafel Fußgänger und Radverkehr (maximale Wartezeit): ${stufentafelSatz(SERVICE_LEVEL_SCALES.HBS.fussRad)}`, |
| 363 | ); |
| 364 | expect(text).toContain('F bei Überlastung (q > C)'); |
| 365 | // Der Hinweis mit der Beleglage steht dabei; der Pruefstand ist derselbe |
| 366 | // wie in der Quelle. Der Hinweis nennt jetzt beide Herausgeberquellen und |
| 367 | // sagt zugleich, was sie NICHT sind - zwei Wiedergaben eines Originals |
| 368 | // sind kein zweiter Zeuge fuer das Original (Fassung 5.5.0, Frage E5). |
| 369 | expect(text).toContain('Zu beachten: Alle drei Stufentafeln'); |
| 370 | expect(text).toContain('zwei Wiedergaben eines Originals'); |
| 371 | expect(text).toContain(PRUEFSTAND_LABELS[quelle.pruefstand]); |
| 372 | // Die Liste der feldlosen Quellen kennt genau diese Quelle. |
| 373 | expect(quellenOhneFeld().map((q) => q.schluessel)).toContain('hbs-qualitaetsstufen'); |
| 374 | }); |
| 375 | }); |
| 376 | |
| 377 | describe('HCM-Projekt: Nachweis der Kennwerte ohne HBS-Bewertungsetikett', () => { |
| 378 | it('fuehrt die HCM-Tafel statt der HBS-Tafeln und vermerkt wirkungslose HBS-Wartezeitkennwerte', () => { |
| 379 | // Im HCM-Projekt gehen die HBS-Tafeln und der Instationaritaetsfaktor in |
| 380 | // keine Zahl ein. Das Verzeichnis druckte sie trotzdem als verwendet |
| 381 | // (Befund F6). |
| 382 | const projekt: Project = { |
| 383 | ...vollstaendig('HCM'), |
| 384 | settings: { ...vollstaendig('HCM').settings, rilsa: { instationaritaetsfaktor: 1.2 } }, |
| 385 | }; |
| 386 | const text = ausdruck(projekt, NUR_GRUNDLAGEN); |
| 387 | |
| 388 | expect(text).toContain('Level of Service (Kfz/ÖPNV: mittlere Wartezeit; Fuß/Rad: keine Tafel)'); |
| 389 | expect(text).toContain( |
| 390 | `Stufentafel Kfz und ÖPNV (mittlere Wartezeit): ${stufentafelSatz(SERVICE_LEVEL_SCALES.HCM.kfz)}`, |
| 391 | ); |
| 392 | expect(text).not.toContain('Stufentafeln der Qualitätsstufen (Kfz/ÖPNV'); |
| 393 | expect(text).toContain( |
| 394 | 'Instationaritätsfaktor (HBS 2015) (ohne Wirkung: Bewertungsverfahren HCM)', |
| 395 | ); |
| 396 | // Die Abweichungstabelle verlangt fuer den wirkungslosen Wert keine Begruendung. |
| 397 | expect(text).toContain( |
| 398 | 'Instationaritätsfaktor (HBS 2015) (ohne Wirkung: Bewertungsverfahren HCM)', |
| 399 | ); |
| 400 | // Die HCM-Tafel statt der HBS-Tafeln; die VwV-StVO steht unabhaengig vom |
| 401 | // Verfahren dabei, die RSA nur bei einstreifiger Fuehrung. |
| 402 | // Die Geometriefaktoren stehen seit 5.30.0 unabhaengig vom Verfahren |
| 403 | // dabei: Sie wirken auf jede Saettigungsverkehrsstaerke, und wo keine |
| 404 | // Zufahrt vermessen ist, steht der Faktor 1 fuer eine ANGENOMMENE |
| 405 | // Standardbedingung - genau das gehoert in den Nachweis. |
| 406 | expect(quellenOhneFeld('HCM').map((q) => q.schluessel)).toEqual([ |
| 407 | 'hcm-verfahren', |
| 408 | 'vwv-stvo', |
| 409 | 'hbs-geometriefaktoren', |
| 410 | // Seit 5.31.0: die Laenge je wartendem Fahrzeug, aus der die |
| 411 | // gedruckte Rueckstaulaenge entsteht. |
| 412 | 'praxis-stauraum-je-fahrzeug', |
| 413 | ]); |
| 414 | expect(quellenOhneFeld('HCM').some((q) => q.schluessel === 'hbs-qualitaetsstufen')).toBe(false); |
| 415 | expect(QUELLEN['hcm-verfahren'].pruefstand).toBe('fachliteratur'); |
| 416 | }); |
| 417 | |
| 418 | it('vermerkt im HBS-Projekt keinen Kennwert als verfahrensbedingt wirkungslos', () => { |
| 419 | const text = ausdruck(vollstaendig('HBS'), NUR_GRUNDLAGEN); |
| 420 | // Der verfahrensbedingte Vermerk gilt nur dem HBS-Wartezeitverfahren; im |
| 421 | // HBS-Projekt wirkt es, also darf er nirgends stehen. |
| 422 | expect(text).not.toContain('ohne Wirkung: Bewertungsverfahren HCM'); |
| 423 | expect(text).not.toContain('Instationaritätsfaktor (HBS 2015) (ohne Wirkung'); |
| 424 | // Befund C20: Die Progressionsgeschwindigkeiten sind dagegen UNABHAENGIG |
| 425 | // vom Bewertungsverfahren wirkungslos, solange kein Strassenzug erfasst ist |
| 426 | // (seit 5.35.0 haengt der Vermerk daran und nicht mehr an der Fassung). |
| 427 | // Dieser Vermerk steht deshalb auch hier, und zwar mit seinem eigenen |
| 428 | // Grund statt mit dem des Bewertungsverfahrens. |
| 429 | expect(text).toContain( |
| 430 | 'Progressionsgeschwindigkeit (ohne Wirkung: keine Koordinierung erfasst)', |
| 431 | ); |
| 432 | }); |
| 433 | |
| 434 | it('nennt in der Abweichungstabelle den eigenen Grund der Koordinierung, nicht das Bewertungsverfahren', () => { |
| 435 | // Befund C20, Folgefehler im Ausdruck: Die Bedingung fuer den Zusatz |
| 436 | // fragte bereits den Fachkern, der Zusatztext selbst stand fest auf |
| 437 | // " (ohne Wirkung: Bewertungsverfahren HCM)". Die Zeile der |
| 438 | // Progressionsgeschwindigkeit behauptete damit in einem HBS-Projekt ein |
| 439 | // Bewertungsverfahren als Grund, das mit ihr nichts zu tun hat. |
| 440 | const roh = vollstaendig('HBS'); |
| 441 | const projekt: Project = { |
| 442 | ...roh, |
| 443 | settings: { ...roh.settings, rilsa: { ...roh.settings.rilsa, coordinationDefaultSpeed: 70 } }, |
| 444 | }; |
| 445 | const text = ausdruck(projekt, NUR_GRUNDLAGEN); |
| 446 | |
| 447 | expect(text).toContain('Abweichungen von den Regelwerten'); |
| 448 | // Kennwert | Regelwert | Angesetzt | Einheit - mit dem Grund an der Zeile. |
| 449 | expect(text).toMatch( |
| 450 | /Progressionsgeschwindigkeit \(ohne Wirkung: keine Koordinierung erfasst\)\s+50\s+70\s+km\/h/, |
| 451 | ); |
| 452 | expect(text).not.toContain('Bewertungsverfahren HCM'); |
| 453 | }); |
| 454 | }); |