lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests export pdfErsatzbelege.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 { intergreenDetailCsv, signalGroupsCsv } from '@/services/export/csv'; |
| 5 | import { buildSignalPlan } from '@/domain/plan/signalPlan'; |
| 6 | import { validateProject } from '@/domain/validation'; |
| 7 | import { createStandardIntersectionProject } from '@/domain/model/factory'; |
| 8 | import { |
| 9 | ENGER_RADIUS_LABEL, |
| 10 | HBS_STANDARDBEDINGUNGEN, |
| 11 | RILSA_DEFAULTS, |
| 12 | UEBERFAHRZEIT_ANSATZ_LABELS, |
| 13 | } from '@/domain/rilsa/constants'; |
| 14 | import { PRUEFSTAND_LABELS, QUELLEN } from '@/domain/rilsa/quellen'; |
| 15 | import { saturationFlow, schwerverkehrsfaktor } from '@/domain/rilsa/capacity'; |
| 16 | import { toWinAnsi } from '@/render/pdfSurface'; |
| 17 | import type { Project } from '@/domain/model/project'; |
| 18 | import * as fmt from '@/ui/format'; |
| 19 | |
| 20 | /* |
| 21 | * Fassung 5.5.0, Fragen E1, E2 und E6 - was der AUSDRUCK und die |
| 22 | * TABELLENAUSGABE davon sagen muessen. |
| 23 | * |
| 24 | * E6 (Rechenaenderung): Die Saettigungsverkehrsstaerke wurde mit einem festen |
| 25 | * Pkw-Gleichwert von 2,0 gemindert. Der Ausdruck nannte diesen Wert unter der |
| 26 | * Ueberschrift "Bewertung nach HBS 2015" - er ist nachweislich der Wert des |
| 27 | * HCM 2010 (BASt V 400, S. 68). Gerechnet wird jetzt mit dem Anpassungsfaktor |
| 28 | * fSV des HBS; die Unterlage muss die Kette fSV -> tB -> qS nennen, sonst |
| 29 | * laesst sich die gedruckte Saettigungsverkehrsstaerke aus dem Regelwert je |
| 30 | * Fahrstreifen nicht herleiten. |
| 31 | * |
| 32 | * E2 (waehlbarer Ansatz): Die Ueberfahrzeit des Kraftfahrzeugverkehrs haengt in |
| 33 | * der Vorgabe an der Gelbzeit. Wer den Ansatz "feste Ueberfahrzeit" waehlt, |
| 34 | * VERKUERZT jede Zwischenzeit eines Kfz-Raeumstroms - beim Abbiegen um bis zu |
| 35 | * 3 s. Eine Verkuerzung, die in der Unterlage nicht als Abweichung erscheint, |
| 36 | * ist vor der Behoerde nicht ausgewiesen. |
| 37 | * |
| 38 | * E1 (waehlbares Merkmal): Der enge Innenradius senkt die Raeumgeschwindigkeit |
| 39 | * einer einzelnen Beziehung auf 5,0 m/s. Er ist in der RiLSA 2015 nicht |
| 40 | * nachweisbar; ohne Kennzeichnung stuende in der Rechenwegtabelle eine |
| 41 | * Geschwindigkeit, die wie ein Regelwerkswert aussieht. |
| 42 | */ |
| 43 | |
| 44 | const STICHTAG = new Date('2026-01-01T12:00:00Z'); |
| 45 | |
| 46 | /** Sichtbaren Text aus dem PDF lesen; die Inhaltsstroeme sind komprimiert. */ |
| 47 | function pdfText(bytes: Uint8Array): string { |
| 48 | const roh = new TextDecoder('latin1').decode(bytes); |
| 49 | const teile: string[] = []; |
| 50 | const suche = /stream\r?\n/g; |
| 51 | let treffer: RegExpExecArray | null; |
| 52 | |
| 53 | while ((treffer = suche.exec(roh)) !== null) { |
| 54 | const start = treffer.index + treffer[0].length; |
| 55 | const ende = roh.indexOf('endstream', start); |
| 56 | if (ende < 0) continue; |
| 57 | let inhalt: string; |
| 58 | try { |
| 59 | inhalt = inflateSync(Buffer.from(roh.slice(start, ende), 'latin1')).toString('latin1'); |
| 60 | } catch { |
| 61 | continue; |
| 62 | } |
| 63 | for (const t of inhalt.matchAll(/\(((?:[^()\\]|\\.)*)\)\s*Tj/g)) { |
| 64 | teile.push(t[1]!.replace(/\\([()\\])/g, '$1')); |
| 65 | } |
| 66 | } |
| 67 | // Der Ausdruck bricht Saetze und Tabellenzellen um; fuer die Suche wieder |
| 68 | // zusammenziehen. |
| 69 | return teile.join(' ').replace(/\s+/g, ' '); |
| 70 | } |
| 71 | |
| 72 | const gedruckt = (text: string): string => toWinAnsi(text).replace(/\s+/g, ' '); |
| 73 | |
| 74 | /** |
| 75 | * Ein Knotenpunkt, dessen erster Kfz-Strom ABBIEGT - nur dort wirkt der enge |
| 76 | * Innenradius, und nur dort unterscheiden sich die beiden Ueberfahrzeit- |
| 77 | * Ansaetze um mehr als eine Sekunde. |
| 78 | */ |
| 79 | function knotenpunkt(): Project { |
| 80 | const basis = createStandardIntersectionProject('Ersatzbelege', STICHTAG); |
| 81 | return { |
| 82 | ...basis, |
| 83 | meta: { ...basis.meta, planner: 'O. Willerding' }, |
| 84 | signalGroups: basis.signalGroups.map((g, index) => |
| 85 | index === 0 ? { ...g, movement: 'links' as const } : g, |
| 86 | ), |
| 87 | }; |
| 88 | } |
| 89 | |
| 90 | /** Dasselbe Projekt mit dem Merkmal "enger Innenradius" an der ERSTEN Beziehung. */ |
| 91 | function mitEngemRadius(basis: Project = knotenpunkt()): Project { |
| 92 | return { |
| 93 | ...basis, |
| 94 | conflicts: basis.conflicts.map((c, index) => (index === 0 ? { ...c, engerRadius: true } : c)), |
| 95 | }; |
| 96 | } |
| 97 | |
| 98 | /** Dasselbe Projekt mit dem Ansatz "feste Ueberfahrzeit". */ |
| 99 | function mitFesterUeberfahrzeit(basis: Project = knotenpunkt()): Project { |
| 100 | return { ...basis, settings: { ...basis.settings, ueberfahrzeitAnsatz: 'fest' } }; |
| 101 | } |
| 102 | |
| 103 | /** |
| 104 | * Dasselbe Projekt mit sehr kurzen Raeumwegen und fester Ueberfahrzeit - dort |
| 105 | * greift die Untergrenze der Ueberfahr- und Raeumzeit (RiLSA 2015, |
| 106 | * Abschnitt 2.5.2: tue + tr >= tG + 1 s). |
| 107 | */ |
| 108 | function mitGreifenderUntergrenze(): Project { |
| 109 | const basis = mitFesterUeberfahrzeit(); |
| 110 | return { |
| 111 | ...basis, |
| 112 | conflicts: basis.conflicts.map((c) => ({ ...c, clearingDistance: 1, enteringDistance: 0 })), |
| 113 | }; |
| 114 | } |
| 115 | |
| 116 | function ausdruck(project: Project, optionen: PdfOptions = DEFAULT_PDF_OPTIONS): string { |
| 117 | const plan = buildSignalPlan(project); |
| 118 | const bericht = validateProject(project, plan, STICHTAG); |
| 119 | return pdfText(buildProjectPdf(project, plan, bericht, optionen, STICHTAG)); |
| 120 | } |
| 121 | |
| 122 | describe('E2 - Der Ausdruck nennt den gewaehlten Ueberfahrzeit-Ansatz', () => { |
| 123 | it('nennt den Regelansatz auf dem Deckblatt und unter der Zwischenzeitenmatrix', () => { |
| 124 | const text = ausdruck(knotenpunkt()); |
| 125 | |
| 126 | // Deckblatt: die Zeile neben Umlaufzeit und Bemessungsverfahren. Sie steht |
| 127 | // dort, weil die Zwischenzeitenmatrix abwaehlbar ist - siehe den zweiten |
| 128 | // Test dieses Blocks. |
| 129 | expect(text).toContain(gedruckt('Überfahrzeit Kfz (Rechenansatz)')); |
| 130 | expect(text).toContain(gedruckt(UEBERFAHRZEIT_ANSATZ_LABELS.gelbzeit)); |
| 131 | |
| 132 | // Unter der Matrix die Begruendung: dieser Ansatz ist der konservative. |
| 133 | expect(text).toContain( |
| 134 | gedruckt('Überfahrzeit des Kraftfahrzeugverkehrs: Gerechnet wurde mit dem Ansatz'), |
| 135 | ); |
| 136 | expect(text).toContain(gedruckt('ist damit die konservative Seite')); |
| 137 | // Die Gelbzeitstaffel steht dabei - sonst waere "Überfahrzeit = Gelbzeit" |
| 138 | // eine Angabe ohne Zahl. |
| 139 | expect(text).toContain(gedruckt('3 s bis 50 km/h, 4 s bis 60 km/h, 5 s bis 70 km/h')); |
| 140 | }); |
| 141 | |
| 142 | it('nennt den Ansatz auch dann, wenn Zwischenzeitenmatrix und Rechenweg abgewaehlt sind', () => { |
| 143 | // Der Fall, um den es geht: Ein Ausdruck ohne Zwischenzeitenabschnitte |
| 144 | // enthaelt trotzdem Freigabe- und Uebergangszeiten, die auf dem Ansatz |
| 145 | // beruhen. Ohne die Deckblattzeile stuende er nirgends. |
| 146 | const text = ausdruck(knotenpunkt(), { |
| 147 | ...DEFAULT_PDF_OPTIONS, |
| 148 | includeIntergreenMatrix: false, |
| 149 | includeCalculationDetails: false, |
| 150 | }); |
| 151 | expect(text).toContain(gedruckt('Überfahrzeit Kfz (Rechenansatz)')); |
| 152 | expect(text).toContain(gedruckt(UEBERFAHRZEIT_ANSATZ_LABELS.gelbzeit)); |
| 153 | }); |
| 154 | |
| 155 | it('sagt beim Ansatz "feste Ueberfahrzeit", dass er verkuerzt und woher die Werte stammen', () => { |
| 156 | const text = ausdruck(mitFesterUeberfahrzeit()); |
| 157 | |
| 158 | expect(text).toContain(gedruckt(UEBERFAHRZEIT_ANSATZ_LABELS.fest)); |
| 159 | // Die Richtung - das Entscheidende. Eine Verkuerzung, die nur als Name |
| 160 | // eines Ansatzes erscheint, sieht wie eine Einstellung aus. |
| 161 | expect(text).toContain(gedruckt('VERKÜRZT die Zwischenzeiten gegenüber dem Regelansatz')); |
| 162 | expect(text).toContain(gedruckt('beim Abbiegen um bis zu 3 s je Übergang')); |
| 163 | /* |
| 164 | * NACHGEZOGEN (5.27.0): Die Herkunft ist die RiLSA und nicht mehr "die |
| 165 | * Zwischenzeitentafeln behördlicher Leitfäden … NICHT aus der RiLSA 2015". |
| 166 | * Und die Untergrenze ist eingebaut, statt als nicht belegbar ausgewiesen |
| 167 | * zu werden. |
| 168 | */ |
| 169 | expect(text).toContain(gedruckt('Regelwerte der RiLSA 2015 (Abschnitt 2.5.2')); |
| 170 | expect(text).not.toContain(gedruckt('NICHT aus der RiLSA 2015')); |
| 171 | expect(text).toContain(gedruckt('tü + tr mindestens Gelbzeit + 1 s')); |
| 172 | expect(text).not.toContain(gedruckt('NICHT eingebaut')); |
| 173 | }); |
| 174 | |
| 175 | it('fuehrt den Ansatz "fest" als begruendungspflichtige Abweichung, den Regelansatz nicht', () => { |
| 176 | const fest = ausdruck(mitFesterUeberfahrzeit()); |
| 177 | expect(fest).toContain(gedruckt('Abweichungen von den Regelwerten')); |
| 178 | expect(fest).toContain( |
| 179 | gedruckt('Überfahrzeit des Kraftfahrzeugverkehrs (Rechenansatz, kein Kennwert)'), |
| 180 | ); |
| 181 | // Regelwert und Angesetztes stehen nebeneinander - eine Abweichung ohne |
| 182 | // den Wert, von dem sie abweicht, ist keine. |
| 183 | expect(fest).toContain(gedruckt(UEBERFAHRZEIT_ANSATZ_LABELS.gelbzeit)); |
| 184 | // Richtung UND Groessenordnung, beide aus den wirksamen Vorgaben |
| 185 | // (Fassung 5.5.0, Befund B5). |
| 186 | expect(fest).toContain(gedruckt('VERKÜRZT die Zwischenzeiten')); |
| 187 | expect(fest).toContain(gedruckt('beim Abbiegen um bis zu 3 s')); |
| 188 | |
| 189 | // Gegenprobe: Das unveraenderte Projekt hat keine Abweichung und deshalb |
| 190 | // auch keinen Abschnitt. |
| 191 | expect(ausdruck(knotenpunkt())).not.toContain(gedruckt('Abweichungen von den Regelwerten')); |
| 192 | }); |
| 193 | |
| 194 | it('druckt die festen Ueberfahrzeiten aus den Vorgaben, nicht ausgeschrieben', () => { |
| 195 | // Befund C21 an einer neuen Stelle: Die beiden Werte sind einstellbar. Ein |
| 196 | // Ausdruck, der "3 s geradeaus und 2 s abbiegend" behauptet, waehrend die |
| 197 | // Rechnung mit anderen Zahlen lief, ist schlimmer als gar keine Angabe. |
| 198 | const basis = mitFesterUeberfahrzeit(); |
| 199 | const projekt: Project = { |
| 200 | ...basis, |
| 201 | settings: { ...basis.settings, rilsa: { crossingTimeKfzAbbiegend: 4 } }, |
| 202 | }; |
| 203 | const text = ausdruck(projekt); |
| 204 | |
| 205 | expect(text).toContain(gedruckt('angesetzt sind 3 s geradeaus und 4 s abbiegend')); |
| 206 | expect(text).not.toContain(gedruckt('angesetzt sind 3 s geradeaus und 2 s abbiegend')); |
| 207 | // Der geaenderte Kennwert erscheint zusaetzlich als Abweichung mit seinem |
| 208 | // Regelwert - und OHNE den Vermerk "ohne Wirkung", denn beim Ansatz "fest" |
| 209 | // wirkt er. |
| 210 | expect(text).toContain(gedruckt('Überfahrzeit Kfz abbiegend (Ansatz „feste Überfahrzeit")')); |
| 211 | expect(text).not.toContain( |
| 212 | gedruckt('Überfahrzeit Kfz abbiegend (Ansatz „feste Überfahrzeit") (ohne Wirkung'), |
| 213 | ); |
| 214 | }); |
| 215 | }); |
| 216 | |
| 217 | describe('Der Rechenweg bleibt nachrechenbar, wo die Untergrenze greift', () => { |
| 218 | it('nennt die Untergrenze und die Beziehungen, an denen sie angesetzt ist', () => { |
| 219 | /* |
| 220 | * Wo tue + tr die Untergrenze tG + 1 s unterschreiten, steht in der Spalte |
| 221 | * tz nicht tue + tr - te, sondern die angehobene Summe. Ohne die Fussnote |
| 222 | * waere die Zeile aus ihren eigenen Spalten nicht nachzurechnen, und |
| 223 | * nachrechenbar muss jede gedruckte Zeile sein. |
| 224 | */ |
| 225 | const text = ausdruck(mitGreifenderUntergrenze()); |
| 226 | |
| 227 | expect(text).toContain(gedruckt('tü + tr mindestens Gelbzeit + 1 s')); |
| 228 | expect(text).toContain(gedruckt('Abschnitt 2.5.2')); |
| 229 | expect(text).toContain(gedruckt('Betroffen')); |
| 230 | |
| 231 | // Gegenprobe: Im unveraenderten Projekt greift sie nicht, und die |
| 232 | // Fussnote steht dann auch nicht da. |
| 233 | expect(ausdruck(knotenpunkt())).not.toContain(gedruckt('tü + tr mindestens Gelbzeit + 1 s')); |
| 234 | }); |
| 235 | }); |
| 236 | |
| 237 | describe('E1 - Die Zwischenzeitentabelle kennzeichnet den engen Innenradius', () => { |
| 238 | it('fuehrt die Spalte mit der vollen Beschriftung an der gekennzeichneten Beziehung', () => { |
| 239 | const text = ausdruck(mitEngemRadius()); |
| 240 | |
| 241 | expect(text).toContain(gedruckt('Enger Innenradius')); |
| 242 | // Die volle Beschriftung nennt Schwelle, Herkunft und Grenze der Beleglage |
| 243 | // - sie steht in der Zelle, nicht nur in einer Fussnote. |
| 244 | expect(text).toContain(gedruckt(ENGER_RADIUS_LABEL)); |
| 245 | // Seit 5.27.0 nennt die Beschriftung die Fundstelle statt der Lücke. |
| 246 | expect(ENGER_RADIUS_LABEL).toContain('RiLSA 2015, Abschnitt 2.5.2, Fall 2'); |
| 247 | |
| 248 | // Und die Wirkung: der abbiegende Strom raeumt mit dem kleineren Wert. |
| 249 | expect(text).toContain( |
| 250 | gedruckt(fmt.metersPerSecond(RILSA_DEFAULTS.clearingSpeed.kfzTurningEngerRadius, 1)), |
| 251 | ); |
| 252 | }); |
| 253 | |
| 254 | it('erklaert die Spalte mit den Geschwindigkeiten dieses Plans und nennt die Grenze des Merkmals', () => { |
| 255 | const text = ausdruck(knotenpunkt()); |
| 256 | |
| 257 | expect(text).toContain( |
| 258 | gedruckt( |
| 259 | `räumt ein abbiegender Kraftfahrzeugstrom mit ` + |
| 260 | `${fmt.metersPerSecond(RILSA_DEFAULTS.clearingSpeed.kfzTurningEngerRadius, 1)} statt mit ` + |
| 261 | `${fmt.metersPerSecond(RILSA_DEFAULTS.clearingSpeed.kfzTurning, 1)}`, |
| 262 | ), |
| 263 | ); |
| 264 | // Was das Programm NICHT prueft, steht dabei. |
| 265 | expect(text).toContain(gedruckt('Auch die Schwelle von 10 m prüft dieses Programm nicht nach')); |
| 266 | expect(text).toContain(gedruckt('Bei allen übrigen Strömen bleibt das Merkmal ohne Wirkung')); |
| 267 | }); |
| 268 | |
| 269 | it('fuehrt das Merkmal als Abweichung mit der Zahl der betroffenen Beziehungen', () => { |
| 270 | const projekt = mitEngemRadius(); |
| 271 | const text = ausdruck(projekt); |
| 272 | |
| 273 | expect(text).toContain( |
| 274 | gedruckt( |
| 275 | `Räumgeschwindigkeit Kfz abbiegend, Merkmal „enger Innenradius" an 1 von ` + |
| 276 | `${projekt.conflicts.length} Konfliktbeziehungen`, |
| 277 | ), |
| 278 | ); |
| 279 | expect(text).toContain(gedruckt('Die Wahl VERLÄNGERT die Zwischenzeiten.')); |
| 280 | // Gegenprobe: ohne gesetztes Merkmal keine Zeile. |
| 281 | expect(ausdruck(knotenpunkt())).not.toContain(gedruckt('Merkmal „enger Innenradius" an')); |
| 282 | }); |
| 283 | |
| 284 | it('nennt die neue Quelle im Fundstellenverzeichnis mit Pruefstand und Beleglage', () => { |
| 285 | /* |
| 286 | * Dieser Test haengt NICHT an der Ausgabeschicht: Das Verzeichnis entsteht |
| 287 | * aus settingFieldsByQuelle, eine neue Quelle mit Vorgabenfeld erscheint |
| 288 | * darin von selbst. Er steht trotzdem hier, und zwar als Sicherung des |
| 289 | * Wegs: Genau dieser Weg hat schon einmal versagt - die Quelle |
| 290 | * 'hbs-qualitaetsstufen' hatte kein Vorgabenfeld und fehlte deshalb ganz |
| 291 | * im gedruckten Nachweis. |
| 292 | */ |
| 293 | const quelle = QUELLEN['praxis-enger-radius']; |
| 294 | const text = ausdruck(knotenpunkt()); |
| 295 | |
| 296 | expect(text).toContain(gedruckt('Räumgeschwindigkeit Kfz abbiegend bei engem Innenradius')); |
| 297 | expect(text).toContain(gedruckt(quelle.sachgebiet)); |
| 298 | expect(text).toContain(gedruckt(PRUEFSTAND_LABELS[quelle.pruefstand])); |
| 299 | // Der Hinweis traegt die Fundstelle in die gedruckte Unterlage - bis |
| 300 | // 5.26.0 stand dort die Luecke ("In der RiLSA 2015 nicht nachweisbar"). |
| 301 | expect(text).toContain(gedruckt('Am Original abgeglichen (10.09.2026)')); |
| 302 | expect(text).toContain(gedruckt('Radius des Fahrstreifeninnenrands')); |
| 303 | expect(text).not.toContain(gedruckt('zwei byteidentische Spiegel derselben Lehrseite')); |
| 304 | }); |
| 305 | }); |
| 306 | |
| 307 | describe('E6 - Der Rechenweg der Kapazitaet nennt fSV und den Zeitbedarfswert', () => { |
| 308 | it('nennt die Kette fSV, tB und qS statt des Pkw-Aequivalents 2,0', () => { |
| 309 | const text = ausdruck(knotenpunkt()); |
| 310 | const qS = RILSA_DEFAULTS.capacity.saturationFlow; |
| 311 | |
| 312 | // Die falsche Behauptung des Altstands. |
| 313 | expect(text).not.toContain(gedruckt('Pkw-Äquivalent 2,0')); |
| 314 | |
| 315 | expect(text).toContain( |
| 316 | gedruckt(`tB = fSV · ${fmt.numShort(3600 / qS, 2)} s und qS = 3600 / tB`), |
| 317 | ); |
| 318 | expect(text).toContain(gedruckt('fSV = (qLV + 1,9 · qSV) / qKfz, also fSV = 1 + 0,9 · pSV')); |
| 319 | /* |
| 320 | * NACHGEFUEHRT (Fassung 5.6.0): Hier stand die Erwartung "Programm erfasst |
| 321 | * keine Aufteilung des Schwerverkehrs". Der Satz war richtig, solange das |
| 322 | * Datenmodell keine Aufteilung kannte; seit sie erfassbar ist, waere er |
| 323 | * eine falsche Angabe in der Planunterlage. Der Ausdruck nennt jetzt |
| 324 | * BEIDE Gleichungen und sagt, dass die Datenlage des einzelnen Stroms |
| 325 | * entscheidet. |
| 326 | */ |
| 327 | expect(text).not.toContain(gedruckt('erfasst keine Aufteilung des Schwerverkehrs')); |
| 328 | expect(text).toContain( |
| 329 | gedruckt('fSV = (qLV + 1,75 · qLkw+Bus + 2,5 · qLkwK) / qKfz (Gl. 2-5)'), |
| 330 | ); |
| 331 | expect(text).toContain(gedruckt('entscheidet die Datenlage DES EINZELNEN STROMS')); |
| 332 | }); |
| 333 | |
| 334 | it('nennt die Standardbedingungen und die Grenze der Anpassungsfaktoren', () => { |
| 335 | const text = ausdruck(knotenpunkt()); |
| 336 | |
| 337 | expect(text).toContain( |
| 338 | gedruckt( |
| 339 | `Längsneigung höchstens ${fmt.numShort(HBS_STANDARDBEDINGUNGEN.laengsneigungBis)} %, ` + |
| 340 | `Kurvenradius mindestens ${fmt.meters(HBS_STANDARDBEDINGUNGEN.kurvenradiusAb, 0)}, ` + |
| 341 | `Fahrstreifenbreite mindestens ${fmt.meters(HBS_STANDARDBEDINGUNGEN.fahrstreifenbreiteAb, 0)}`, |
| 342 | ), |
| 343 | ); |
| 344 | /* |
| 345 | * UMGEDREHT MIT 5.30.0. Hier stand die Zusage, der Ausdruck nenne die |
| 346 | * Anpassungsfaktoren als NICHT abgebildet - und das war bis dahin richtig. |
| 347 | * Sie sind es jetzt. Die Fehlerrichtung bleibt trotzdem zu benennen, nur |
| 348 | * an anderer Stelle: Nicht das Programm rechnet zu gross, sondern jede |
| 349 | * Zufahrt OHNE Eintrag, denn dort gilt die Standardbedingung als Annahme. |
| 350 | */ |
| 351 | expect(text).toContain(gedruckt('seit 5.30.0 abgebildet')); |
| 352 | expect(text).toContain( |
| 353 | gedruckt('die Sättigungsverkehrsstärke fällt für eine davon abweichende Zufahrt zu groß aus'), |
| 354 | ); |
| 355 | }); |
| 356 | |
| 357 | it('druckt fSV je Signalgruppe so, dass sich die Saettigungsverkehrsstaerke nachrechnen laesst', () => { |
| 358 | const projekt = knotenpunkt(); |
| 359 | const plan = buildSignalPlan(projekt); |
| 360 | const text = ausdruck(projekt); |
| 361 | |
| 362 | let geprueft = 0; |
| 363 | for (const gruppe of plan.groups) { |
| 364 | const nachfrage = projekt.demands.find((d) => d.signalGroupId === gruppe.groupId); |
| 365 | if (!nachfrage || gruppe.capacity === null) continue; |
| 366 | const gruppendaten = projekt.signalGroups.find((g) => g.id === gruppe.groupId)!; |
| 367 | const faktor = schwerverkehrsfaktor(nachfrage.heavyVehicleShare); |
| 368 | |
| 369 | // Die gedruckte Zelle: Anteil, Datenlage und Faktor mit seiner Gleichung. |
| 370 | // Fuer dieses Projekt ist keine Aufteilung erfasst - also Gl. 2-6 |
| 371 | // (vor der Fassung 5.6.0 standen hier nur Anteil und Faktor). |
| 372 | expect(text, gruppe.name).toContain( |
| 373 | gedruckt( |
| 374 | `${fmt.numShort(nachfrage.heavyVehicleShare * 100, 1)} % Aufteilung nicht erfasst ` + |
| 375 | `fSV = ${fmt.numShort(faktor, 3)} (Gl. 2-6)`, |
| 376 | ), |
| 377 | ); |
| 378 | // Und die Gegenprobe an der Zahl, die daneben steht: qS = Grundwert/fSV. |
| 379 | const grundwert = saturationFlow(gruppendaten.movement, gruppendaten.lanes, plan.defaults); |
| 380 | expect(gruppe.capacity.saturationFlow, gruppe.name).toBeCloseTo(grundwert / faktor, 6); |
| 381 | geprueft += 1; |
| 382 | } |
| 383 | expect(geprueft, 'Vorbedingung: es gibt Gruppen mit Verkehrsstärke').toBeGreaterThan(0); |
| 384 | |
| 385 | // Der Altstand rechnete qS = Grundwert / (1 + pSV) - bei 5 % also 1905 |
| 386 | // statt 1913 Fz/h. Die Zahlen duerfen sich nicht decken, sonst prueft der |
| 387 | // Test nichts. |
| 388 | expect(schwerverkehrsfaktor(0.05)).not.toBeCloseTo(1.05, 6); |
| 389 | }); |
| 390 | }); |
| 391 | |
| 392 | describe('E2 - Wirkungsvermerk der festen Ueberfahrzeiten im Fundstellenverzeichnis', () => { |
| 393 | it('vermerkt sie beim Regelansatz als ohne Wirkung', () => { |
| 394 | // Befund C20 an einer neuen Stelle: Beim Ansatz "Überfahrzeit = Gelbzeit" |
| 395 | // gehen die beiden Zahlen in keinen Plan ein. Ein Verzeichnis, das sie wie |
| 396 | // angewandte Werte fuehrt, behauptet eine Wirkung, die sie nicht haben. |
| 397 | const text = ausdruck(knotenpunkt()); |
| 398 | expect(text).toContain( |
| 399 | gedruckt( |
| 400 | 'Überfahrzeit Kfz geradeaus (Ansatz „feste Überfahrzeit") (ohne Wirkung: Überfahrzeit = Gelbzeit)', |
| 401 | ), |
| 402 | ); |
| 403 | expect(text).toContain( |
| 404 | gedruckt( |
| 405 | 'Überfahrzeit Kfz abbiegend (Ansatz „feste Überfahrzeit") (ohne Wirkung: Überfahrzeit = Gelbzeit)', |
| 406 | ), |
| 407 | ); |
| 408 | }); |
| 409 | |
| 410 | it('laesst den Vermerk beim Ansatz "fest" weg und nennt die Quelle mit ihrem Pruefstand', () => { |
| 411 | const quelle = QUELLEN['praxis-ueberfahrzeit-fest']; |
| 412 | const text = ausdruck(mitFesterUeberfahrzeit()); |
| 413 | |
| 414 | expect(text).toContain(gedruckt('Überfahrzeit Kfz geradeaus (Ansatz „feste Überfahrzeit")')); |
| 415 | expect(text).not.toContain(gedruckt('(ohne Wirkung: Überfahrzeit = Gelbzeit)')); |
| 416 | expect(text).toContain(gedruckt(quelle.sachgebiet)); |
| 417 | expect(text).toContain(gedruckt(PRUEFSTAND_LABELS[quelle.pruefstand])); |
| 418 | // Der tragende Beleg steht gedruckt da - seit 5.27.0 die Fundstelle |
| 419 | // statt des Umkehrschlusses aus den Hamburger Zwischenzeitentafeln. |
| 420 | expect(text).toContain(gedruckt('Am Original abgeglichen (10.09.2026)')); |
| 421 | expect(text).toContain(gedruckt('DER FESTE ANSATZ IST DAMIT DER REGELWERKSANSATZ')); |
| 422 | expect(text).not.toContain(gedruckt('Deshalb kein Regelwerkswert.')); |
| 423 | }); |
| 424 | }); |
| 425 | |
| 426 | describe('Tabellenausgabe: Ansatz, enger Innenradius und fSV', () => { |
| 427 | it('nennt den Schwerverkehrsfaktor je Signalgruppe neben dem Anteil', () => { |
| 428 | const projekt = knotenpunkt(); |
| 429 | const plan = buildSignalPlan(projekt); |
| 430 | const csv = signalGroupsCsv(projekt, plan); |
| 431 | const kopf = csv.split('\r\n')[0]!; |
| 432 | |
| 433 | expect(kopf).toContain('Schwerverkehrsfaktor fSV'); |
| 434 | /* |
| 435 | * Die Reihenfolge ist die des Rechenwegs: Anteil, Aufteilung, Faktor, |
| 436 | * Gleichung. Vor der Fassung 5.6.0 stand der Faktor unmittelbar hinter dem |
| 437 | * Anteil; seitdem liegt die Aufteilung dazwischen, weil sie der zweite |
| 438 | * Eingangswert der Gleichung ist und nicht ihr Ergebnis. |
| 439 | */ |
| 440 | const spalten = kopf.replace('', '').split(';'); |
| 441 | const anteil = spalten.indexOf('Schwerverkehrsanteil [%]'); |
| 442 | expect(spalten.indexOf('Lastzuganteil am Schwerverkehr [%] (leer = nicht erfasst)')).toBe( |
| 443 | anteil + 1, |
| 444 | ); |
| 445 | expect(spalten.indexOf('Schwerverkehrsfaktor fSV')).toBe(anteil + 2); |
| 446 | expect(spalten.indexOf('Gleichung für fSV (HBS 2015)')).toBe(anteil + 3); |
| 447 | |
| 448 | for (const nachfrage of projekt.demands) { |
| 449 | const name = projekt.signalGroups.find((g) => g.id === nachfrage.signalGroupId)!.name; |
| 450 | const zeile = csv.split('\r\n').find((z) => z.startsWith(`${name};`))!; |
| 451 | // Leere Zelle fuer die Aufteilung: Fuer dieses Projekt ist keine erfasst, |
| 452 | // und "0" waere eine Zaehlung, die niemand vorgenommen hat. |
| 453 | expect(zeile, name).toContain( |
| 454 | `;${fmt.numShort(nachfrage.heavyVehicleShare * 100, 1)};;` + |
| 455 | `${fmt.numShort(schwerverkehrsfaktor(nachfrage.heavyVehicleShare), 3)};`, |
| 456 | ); |
| 457 | } |
| 458 | }); |
| 459 | |
| 460 | it('nennt Ueberfahrzeit-Ansatz und Merkmal je Konfliktbeziehung', () => { |
| 461 | const projekt = mitEngemRadius(mitFesterUeberfahrzeit()); |
| 462 | const plan = buildSignalPlan(projekt); |
| 463 | const csv = intergreenDetailCsv(projekt, plan); |
| 464 | const zeilen = csv.split('\r\n'); |
| 465 | const kopf = zeilen[0]!; |
| 466 | |
| 467 | expect(kopf).toContain('Enger Innenradius'); |
| 468 | // "Herkunft" und nicht mehr "Ansatz" (Fassung 5.5.0, Befund F5): Der |
| 469 | // Kfz-Ansatz erreicht Fuss, Rad und OePNV nicht, und die Spalte nennt bei |
| 470 | // ihnen deren eigene Herleitung. |
| 471 | expect(kopf).toContain('Überfahrzeit-Herkunft'); |
| 472 | |
| 473 | // Die gekennzeichnete Beziehung traegt die volle Beschriftung - eine |
| 474 | // Tabellenausgabe hat keine Fussnote, in der der Vorbehalt sonst stuende. |
| 475 | const erste = projekt.conflicts[0]!; |
| 476 | const von = projekt.signalGroups.find((g) => g.id === erste.fromId)!.name; |
| 477 | const nach = projekt.signalGroups.find((g) => g.id === erste.toId)!.name; |
| 478 | const zeile = zeilen.find((z) => z.startsWith(`${von};${nach};`))!; |
| 479 | expect(zeile).toContain(`;${ENGER_RADIUS_LABEL};`); |
| 480 | /* |
| 481 | * Die Ansatzbeschriftung steht als EIN Feld in der Zeile - mit |
| 482 | * Anfuehrungszeichen, sobald sie ein Semikolon enthaelt, sonst ohne. |
| 483 | * |
| 484 | * NACHGEZOGEN (5.27.0): Hier stand die Anfuehrungszeichen-Fassung fest. |
| 485 | * Die Beschriftung des Ansatzes "fest" nennt seit dem Abgleich am Original |
| 486 | * die Fundstelle und enthaelt kein Semikolon mehr; die Zusicherung galt |
| 487 | * damit einem Zeichen und nicht der Sache. Gemeint war und ist: Die |
| 488 | * Beschriftung darf die Zeile nicht in zwei Spalten zerreissen. |
| 489 | */ |
| 490 | const beschriftung = UEBERFAHRZEIT_ANSATZ_LABELS.fest; |
| 491 | const feld = beschriftung.includes(';') ? `"${beschriftung}"` : beschriftung; |
| 492 | expect(zeile.split(';').length, 'die Beschriftung zerreißt die Zeile').toBe( |
| 493 | kopf.split(';').length, |
| 494 | ); |
| 495 | expect(zeile).toContain(feld); |
| 496 | |
| 497 | // Alle uebrigen Beziehungen tragen "nein" - das Merkmal gilt je Beziehung |
| 498 | // und nicht fuer den Plan. |
| 499 | const weitere = zeilen.filter((z) => z.includes(';nein;')); |
| 500 | expect(weitere).toHaveLength(projekt.conflicts.length - 1); |
| 501 | }); |
| 502 | }); |