import { inflateSync } from 'node:zlib'; import { describe, expect, it } from 'vitest'; import { buildProjectPdf, DEFAULT_PDF_OPTIONS, type PdfOptions } from '@/services/export/pdf'; import { intergreenDetailCsv, signalGroupsCsv } from '@/services/export/csv'; import { buildSignalPlan } from '@/domain/plan/signalPlan'; import { validateProject } from '@/domain/validation'; import { createStandardIntersectionProject } from '@/domain/model/factory'; import { ENGER_RADIUS_LABEL, HBS_STANDARDBEDINGUNGEN, RILSA_DEFAULTS, UEBERFAHRZEIT_ANSATZ_LABELS, } from '@/domain/rilsa/constants'; import { PRUEFSTAND_LABELS, QUELLEN } from '@/domain/rilsa/quellen'; import { saturationFlow, schwerverkehrsfaktor } from '@/domain/rilsa/capacity'; import { toWinAnsi } from '@/render/pdfSurface'; import type { Project } from '@/domain/model/project'; import * as fmt from '@/ui/format'; /* * Fassung 5.5.0, Fragen E1, E2 und E6 - was der AUSDRUCK und die * TABELLENAUSGABE davon sagen muessen. * * E6 (Rechenaenderung): Die Saettigungsverkehrsstaerke wurde mit einem festen * Pkw-Gleichwert von 2,0 gemindert. Der Ausdruck nannte diesen Wert unter der * Ueberschrift "Bewertung nach HBS 2015" - er ist nachweislich der Wert des * HCM 2010 (BASt V 400, S. 68). Gerechnet wird jetzt mit dem Anpassungsfaktor * fSV des HBS; die Unterlage muss die Kette fSV -> tB -> qS nennen, sonst * laesst sich die gedruckte Saettigungsverkehrsstaerke aus dem Regelwert je * Fahrstreifen nicht herleiten. * * E2 (waehlbarer Ansatz): Die Ueberfahrzeit des Kraftfahrzeugverkehrs haengt in * der Vorgabe an der Gelbzeit. Wer den Ansatz "feste Ueberfahrzeit" waehlt, * VERKUERZT jede Zwischenzeit eines Kfz-Raeumstroms - beim Abbiegen um bis zu * 3 s. Eine Verkuerzung, die in der Unterlage nicht als Abweichung erscheint, * ist vor der Behoerde nicht ausgewiesen. * * E1 (waehlbares Merkmal): Der enge Innenradius senkt die Raeumgeschwindigkeit * einer einzelnen Beziehung auf 5,0 m/s. Er ist in der RiLSA 2015 nicht * nachweisbar; ohne Kennzeichnung stuende in der Rechenwegtabelle eine * Geschwindigkeit, die wie ein Regelwerkswert aussieht. */ const STICHTAG = new Date('2026-01-01T12:00:00Z'); /** Sichtbaren Text aus dem PDF lesen; die Inhaltsstroeme sind komprimiert. */ function pdfText(bytes: Uint8Array): string { const roh = new TextDecoder('latin1').decode(bytes); const teile: string[] = []; const suche = /stream\r?\n/g; let treffer: RegExpExecArray | null; while ((treffer = suche.exec(roh)) !== null) { const start = treffer.index + treffer[0].length; const ende = roh.indexOf('endstream', start); if (ende < 0) continue; let inhalt: string; try { inhalt = inflateSync(Buffer.from(roh.slice(start, ende), 'latin1')).toString('latin1'); } catch { continue; } for (const t of inhalt.matchAll(/\(((?:[^()\\]|\\.)*)\)\s*Tj/g)) { teile.push(t[1]!.replace(/\\([()\\])/g, '$1')); } } // Der Ausdruck bricht Saetze und Tabellenzellen um; fuer die Suche wieder // zusammenziehen. return teile.join(' ').replace(/\s+/g, ' '); } const gedruckt = (text: string): string => toWinAnsi(text).replace(/\s+/g, ' '); /** * Ein Knotenpunkt, dessen erster Kfz-Strom ABBIEGT - nur dort wirkt der enge * Innenradius, und nur dort unterscheiden sich die beiden Ueberfahrzeit- * Ansaetze um mehr als eine Sekunde. */ function knotenpunkt(): Project { const basis = createStandardIntersectionProject('Ersatzbelege', STICHTAG); return { ...basis, meta: { ...basis.meta, planner: 'O. Willerding' }, signalGroups: basis.signalGroups.map((g, index) => index === 0 ? { ...g, movement: 'links' as const } : g, ), }; } /** Dasselbe Projekt mit dem Merkmal "enger Innenradius" an der ERSTEN Beziehung. */ function mitEngemRadius(basis: Project = knotenpunkt()): Project { return { ...basis, conflicts: basis.conflicts.map((c, index) => (index === 0 ? { ...c, engerRadius: true } : c)), }; } /** Dasselbe Projekt mit dem Ansatz "feste Ueberfahrzeit". */ function mitFesterUeberfahrzeit(basis: Project = knotenpunkt()): Project { return { ...basis, settings: { ...basis.settings, ueberfahrzeitAnsatz: 'fest' } }; } /** * Dasselbe Projekt mit sehr kurzen Raeumwegen und fester Ueberfahrzeit - dort * greift die Untergrenze der Ueberfahr- und Raeumzeit (RiLSA 2015, * Abschnitt 2.5.2: tue + tr >= tG + 1 s). */ function mitGreifenderUntergrenze(): Project { const basis = mitFesterUeberfahrzeit(); return { ...basis, conflicts: basis.conflicts.map((c) => ({ ...c, clearingDistance: 1, enteringDistance: 0 })), }; } function ausdruck(project: Project, optionen: PdfOptions = DEFAULT_PDF_OPTIONS): string { const plan = buildSignalPlan(project); const bericht = validateProject(project, plan, STICHTAG); return pdfText(buildProjectPdf(project, plan, bericht, optionen, STICHTAG)); } describe('E2 - Der Ausdruck nennt den gewaehlten Ueberfahrzeit-Ansatz', () => { it('nennt den Regelansatz auf dem Deckblatt und unter der Zwischenzeitenmatrix', () => { const text = ausdruck(knotenpunkt()); // Deckblatt: die Zeile neben Umlaufzeit und Bemessungsverfahren. Sie steht // dort, weil die Zwischenzeitenmatrix abwaehlbar ist - siehe den zweiten // Test dieses Blocks. expect(text).toContain(gedruckt('Überfahrzeit Kfz (Rechenansatz)')); expect(text).toContain(gedruckt(UEBERFAHRZEIT_ANSATZ_LABELS.gelbzeit)); // Unter der Matrix die Begruendung: dieser Ansatz ist der konservative. expect(text).toContain( gedruckt('Überfahrzeit des Kraftfahrzeugverkehrs: Gerechnet wurde mit dem Ansatz'), ); expect(text).toContain(gedruckt('ist damit die konservative Seite')); // Die Gelbzeitstaffel steht dabei - sonst waere "Überfahrzeit = Gelbzeit" // eine Angabe ohne Zahl. expect(text).toContain(gedruckt('3 s bis 50 km/h, 4 s bis 60 km/h, 5 s bis 70 km/h')); }); it('nennt den Ansatz auch dann, wenn Zwischenzeitenmatrix und Rechenweg abgewaehlt sind', () => { // Der Fall, um den es geht: Ein Ausdruck ohne Zwischenzeitenabschnitte // enthaelt trotzdem Freigabe- und Uebergangszeiten, die auf dem Ansatz // beruhen. Ohne die Deckblattzeile stuende er nirgends. const text = ausdruck(knotenpunkt(), { ...DEFAULT_PDF_OPTIONS, includeIntergreenMatrix: false, includeCalculationDetails: false, }); expect(text).toContain(gedruckt('Überfahrzeit Kfz (Rechenansatz)')); expect(text).toContain(gedruckt(UEBERFAHRZEIT_ANSATZ_LABELS.gelbzeit)); }); it('sagt beim Ansatz "feste Ueberfahrzeit", dass er verkuerzt und woher die Werte stammen', () => { const text = ausdruck(mitFesterUeberfahrzeit()); expect(text).toContain(gedruckt(UEBERFAHRZEIT_ANSATZ_LABELS.fest)); // Die Richtung - das Entscheidende. Eine Verkuerzung, die nur als Name // eines Ansatzes erscheint, sieht wie eine Einstellung aus. expect(text).toContain(gedruckt('VERKÜRZT die Zwischenzeiten gegenüber dem Regelansatz')); expect(text).toContain(gedruckt('beim Abbiegen um bis zu 3 s je Übergang')); /* * NACHGEZOGEN (5.27.0): Die Herkunft ist die RiLSA und nicht mehr "die * Zwischenzeitentafeln behördlicher Leitfäden … NICHT aus der RiLSA 2015". * Und die Untergrenze ist eingebaut, statt als nicht belegbar ausgewiesen * zu werden. */ expect(text).toContain(gedruckt('Regelwerte der RiLSA 2015 (Abschnitt 2.5.2')); expect(text).not.toContain(gedruckt('NICHT aus der RiLSA 2015')); expect(text).toContain(gedruckt('tü + tr mindestens Gelbzeit + 1 s')); expect(text).not.toContain(gedruckt('NICHT eingebaut')); }); it('fuehrt den Ansatz "fest" als begruendungspflichtige Abweichung, den Regelansatz nicht', () => { const fest = ausdruck(mitFesterUeberfahrzeit()); expect(fest).toContain(gedruckt('Abweichungen von den Regelwerten')); expect(fest).toContain( gedruckt('Überfahrzeit des Kraftfahrzeugverkehrs (Rechenansatz, kein Kennwert)'), ); // Regelwert und Angesetztes stehen nebeneinander - eine Abweichung ohne // den Wert, von dem sie abweicht, ist keine. expect(fest).toContain(gedruckt(UEBERFAHRZEIT_ANSATZ_LABELS.gelbzeit)); // Richtung UND Groessenordnung, beide aus den wirksamen Vorgaben // (Fassung 5.5.0, Befund B5). expect(fest).toContain(gedruckt('VERKÜRZT die Zwischenzeiten')); expect(fest).toContain(gedruckt('beim Abbiegen um bis zu 3 s')); // Gegenprobe: Das unveraenderte Projekt hat keine Abweichung und deshalb // auch keinen Abschnitt. expect(ausdruck(knotenpunkt())).not.toContain(gedruckt('Abweichungen von den Regelwerten')); }); it('druckt die festen Ueberfahrzeiten aus den Vorgaben, nicht ausgeschrieben', () => { // Befund C21 an einer neuen Stelle: Die beiden Werte sind einstellbar. Ein // Ausdruck, der "3 s geradeaus und 2 s abbiegend" behauptet, waehrend die // Rechnung mit anderen Zahlen lief, ist schlimmer als gar keine Angabe. const basis = mitFesterUeberfahrzeit(); const projekt: Project = { ...basis, settings: { ...basis.settings, rilsa: { crossingTimeKfzAbbiegend: 4 } }, }; const text = ausdruck(projekt); expect(text).toContain(gedruckt('angesetzt sind 3 s geradeaus und 4 s abbiegend')); expect(text).not.toContain(gedruckt('angesetzt sind 3 s geradeaus und 2 s abbiegend')); // Der geaenderte Kennwert erscheint zusaetzlich als Abweichung mit seinem // Regelwert - und OHNE den Vermerk "ohne Wirkung", denn beim Ansatz "fest" // wirkt er. expect(text).toContain(gedruckt('Überfahrzeit Kfz abbiegend (Ansatz „feste Überfahrzeit")')); expect(text).not.toContain( gedruckt('Überfahrzeit Kfz abbiegend (Ansatz „feste Überfahrzeit") (ohne Wirkung'), ); }); }); describe('Der Rechenweg bleibt nachrechenbar, wo die Untergrenze greift', () => { it('nennt die Untergrenze und die Beziehungen, an denen sie angesetzt ist', () => { /* * Wo tue + tr die Untergrenze tG + 1 s unterschreiten, steht in der Spalte * tz nicht tue + tr - te, sondern die angehobene Summe. Ohne die Fussnote * waere die Zeile aus ihren eigenen Spalten nicht nachzurechnen, und * nachrechenbar muss jede gedruckte Zeile sein. */ const text = ausdruck(mitGreifenderUntergrenze()); expect(text).toContain(gedruckt('tü + tr mindestens Gelbzeit + 1 s')); expect(text).toContain(gedruckt('Abschnitt 2.5.2')); expect(text).toContain(gedruckt('Betroffen')); // Gegenprobe: Im unveraenderten Projekt greift sie nicht, und die // Fussnote steht dann auch nicht da. expect(ausdruck(knotenpunkt())).not.toContain(gedruckt('tü + tr mindestens Gelbzeit + 1 s')); }); }); describe('E1 - Die Zwischenzeitentabelle kennzeichnet den engen Innenradius', () => { it('fuehrt die Spalte mit der vollen Beschriftung an der gekennzeichneten Beziehung', () => { const text = ausdruck(mitEngemRadius()); expect(text).toContain(gedruckt('Enger Innenradius')); // Die volle Beschriftung nennt Schwelle, Herkunft und Grenze der Beleglage // - sie steht in der Zelle, nicht nur in einer Fussnote. expect(text).toContain(gedruckt(ENGER_RADIUS_LABEL)); // Seit 5.27.0 nennt die Beschriftung die Fundstelle statt der Lücke. expect(ENGER_RADIUS_LABEL).toContain('RiLSA 2015, Abschnitt 2.5.2, Fall 2'); // Und die Wirkung: der abbiegende Strom raeumt mit dem kleineren Wert. expect(text).toContain( gedruckt(fmt.metersPerSecond(RILSA_DEFAULTS.clearingSpeed.kfzTurningEngerRadius, 1)), ); }); it('erklaert die Spalte mit den Geschwindigkeiten dieses Plans und nennt die Grenze des Merkmals', () => { const text = ausdruck(knotenpunkt()); expect(text).toContain( gedruckt( `räumt ein abbiegender Kraftfahrzeugstrom mit ` + `${fmt.metersPerSecond(RILSA_DEFAULTS.clearingSpeed.kfzTurningEngerRadius, 1)} statt mit ` + `${fmt.metersPerSecond(RILSA_DEFAULTS.clearingSpeed.kfzTurning, 1)}`, ), ); // Was das Programm NICHT prueft, steht dabei. expect(text).toContain(gedruckt('Auch die Schwelle von 10 m prüft dieses Programm nicht nach')); expect(text).toContain(gedruckt('Bei allen übrigen Strömen bleibt das Merkmal ohne Wirkung')); }); it('fuehrt das Merkmal als Abweichung mit der Zahl der betroffenen Beziehungen', () => { const projekt = mitEngemRadius(); const text = ausdruck(projekt); expect(text).toContain( gedruckt( `Räumgeschwindigkeit Kfz abbiegend, Merkmal „enger Innenradius" an 1 von ` + `${projekt.conflicts.length} Konfliktbeziehungen`, ), ); expect(text).toContain(gedruckt('Die Wahl VERLÄNGERT die Zwischenzeiten.')); // Gegenprobe: ohne gesetztes Merkmal keine Zeile. expect(ausdruck(knotenpunkt())).not.toContain(gedruckt('Merkmal „enger Innenradius" an')); }); it('nennt die neue Quelle im Fundstellenverzeichnis mit Pruefstand und Beleglage', () => { /* * Dieser Test haengt NICHT an der Ausgabeschicht: Das Verzeichnis entsteht * aus settingFieldsByQuelle, eine neue Quelle mit Vorgabenfeld erscheint * darin von selbst. Er steht trotzdem hier, und zwar als Sicherung des * Wegs: Genau dieser Weg hat schon einmal versagt - die Quelle * 'hbs-qualitaetsstufen' hatte kein Vorgabenfeld und fehlte deshalb ganz * im gedruckten Nachweis. */ const quelle = QUELLEN['praxis-enger-radius']; const text = ausdruck(knotenpunkt()); expect(text).toContain(gedruckt('Räumgeschwindigkeit Kfz abbiegend bei engem Innenradius')); expect(text).toContain(gedruckt(quelle.sachgebiet)); expect(text).toContain(gedruckt(PRUEFSTAND_LABELS[quelle.pruefstand])); // Der Hinweis traegt die Fundstelle in die gedruckte Unterlage - bis // 5.26.0 stand dort die Luecke ("In der RiLSA 2015 nicht nachweisbar"). expect(text).toContain(gedruckt('Am Original abgeglichen (10.09.2026)')); expect(text).toContain(gedruckt('Radius des Fahrstreifeninnenrands')); expect(text).not.toContain(gedruckt('zwei byteidentische Spiegel derselben Lehrseite')); }); }); describe('E6 - Der Rechenweg der Kapazitaet nennt fSV und den Zeitbedarfswert', () => { it('nennt die Kette fSV, tB und qS statt des Pkw-Aequivalents 2,0', () => { const text = ausdruck(knotenpunkt()); const qS = RILSA_DEFAULTS.capacity.saturationFlow; // Die falsche Behauptung des Altstands. expect(text).not.toContain(gedruckt('Pkw-Äquivalent 2,0')); expect(text).toContain( gedruckt(`tB = fSV · ${fmt.numShort(3600 / qS, 2)} s und qS = 3600 / tB`), ); expect(text).toContain(gedruckt('fSV = (qLV + 1,9 · qSV) / qKfz, also fSV = 1 + 0,9 · pSV')); /* * NACHGEFUEHRT (Fassung 5.6.0): Hier stand die Erwartung "Programm erfasst * keine Aufteilung des Schwerverkehrs". Der Satz war richtig, solange das * Datenmodell keine Aufteilung kannte; seit sie erfassbar ist, waere er * eine falsche Angabe in der Planunterlage. Der Ausdruck nennt jetzt * BEIDE Gleichungen und sagt, dass die Datenlage des einzelnen Stroms * entscheidet. */ expect(text).not.toContain(gedruckt('erfasst keine Aufteilung des Schwerverkehrs')); expect(text).toContain( gedruckt('fSV = (qLV + 1,75 · qLkw+Bus + 2,5 · qLkwK) / qKfz (Gl. 2-5)'), ); expect(text).toContain(gedruckt('entscheidet die Datenlage DES EINZELNEN STROMS')); }); it('nennt die Standardbedingungen und die Grenze der Anpassungsfaktoren', () => { const text = ausdruck(knotenpunkt()); expect(text).toContain( gedruckt( `Längsneigung höchstens ${fmt.numShort(HBS_STANDARDBEDINGUNGEN.laengsneigungBis)} %, ` + `Kurvenradius mindestens ${fmt.meters(HBS_STANDARDBEDINGUNGEN.kurvenradiusAb, 0)}, ` + `Fahrstreifenbreite mindestens ${fmt.meters(HBS_STANDARDBEDINGUNGEN.fahrstreifenbreiteAb, 0)}`, ), ); /* * UMGEDREHT MIT 5.30.0. Hier stand die Zusage, der Ausdruck nenne die * Anpassungsfaktoren als NICHT abgebildet - und das war bis dahin richtig. * Sie sind es jetzt. Die Fehlerrichtung bleibt trotzdem zu benennen, nur * an anderer Stelle: Nicht das Programm rechnet zu gross, sondern jede * Zufahrt OHNE Eintrag, denn dort gilt die Standardbedingung als Annahme. */ expect(text).toContain(gedruckt('seit 5.30.0 abgebildet')); expect(text).toContain( gedruckt('die Sättigungsverkehrsstärke fällt für eine davon abweichende Zufahrt zu groß aus'), ); }); it('druckt fSV je Signalgruppe so, dass sich die Saettigungsverkehrsstaerke nachrechnen laesst', () => { const projekt = knotenpunkt(); const plan = buildSignalPlan(projekt); const text = ausdruck(projekt); let geprueft = 0; for (const gruppe of plan.groups) { const nachfrage = projekt.demands.find((d) => d.signalGroupId === gruppe.groupId); if (!nachfrage || gruppe.capacity === null) continue; const gruppendaten = projekt.signalGroups.find((g) => g.id === gruppe.groupId)!; const faktor = schwerverkehrsfaktor(nachfrage.heavyVehicleShare); // Die gedruckte Zelle: Anteil, Datenlage und Faktor mit seiner Gleichung. // Fuer dieses Projekt ist keine Aufteilung erfasst - also Gl. 2-6 // (vor der Fassung 5.6.0 standen hier nur Anteil und Faktor). expect(text, gruppe.name).toContain( gedruckt( `${fmt.numShort(nachfrage.heavyVehicleShare * 100, 1)} % Aufteilung nicht erfasst ` + `fSV = ${fmt.numShort(faktor, 3)} (Gl. 2-6)`, ), ); // Und die Gegenprobe an der Zahl, die daneben steht: qS = Grundwert/fSV. const grundwert = saturationFlow(gruppendaten.movement, gruppendaten.lanes, plan.defaults); expect(gruppe.capacity.saturationFlow, gruppe.name).toBeCloseTo(grundwert / faktor, 6); geprueft += 1; } expect(geprueft, 'Vorbedingung: es gibt Gruppen mit Verkehrsstärke').toBeGreaterThan(0); // Der Altstand rechnete qS = Grundwert / (1 + pSV) - bei 5 % also 1905 // statt 1913 Fz/h. Die Zahlen duerfen sich nicht decken, sonst prueft der // Test nichts. expect(schwerverkehrsfaktor(0.05)).not.toBeCloseTo(1.05, 6); }); }); describe('E2 - Wirkungsvermerk der festen Ueberfahrzeiten im Fundstellenverzeichnis', () => { it('vermerkt sie beim Regelansatz als ohne Wirkung', () => { // Befund C20 an einer neuen Stelle: Beim Ansatz "Überfahrzeit = Gelbzeit" // gehen die beiden Zahlen in keinen Plan ein. Ein Verzeichnis, das sie wie // angewandte Werte fuehrt, behauptet eine Wirkung, die sie nicht haben. const text = ausdruck(knotenpunkt()); expect(text).toContain( gedruckt( 'Überfahrzeit Kfz geradeaus (Ansatz „feste Überfahrzeit") (ohne Wirkung: Überfahrzeit = Gelbzeit)', ), ); expect(text).toContain( gedruckt( 'Überfahrzeit Kfz abbiegend (Ansatz „feste Überfahrzeit") (ohne Wirkung: Überfahrzeit = Gelbzeit)', ), ); }); it('laesst den Vermerk beim Ansatz "fest" weg und nennt die Quelle mit ihrem Pruefstand', () => { const quelle = QUELLEN['praxis-ueberfahrzeit-fest']; const text = ausdruck(mitFesterUeberfahrzeit()); expect(text).toContain(gedruckt('Überfahrzeit Kfz geradeaus (Ansatz „feste Überfahrzeit")')); expect(text).not.toContain(gedruckt('(ohne Wirkung: Überfahrzeit = Gelbzeit)')); expect(text).toContain(gedruckt(quelle.sachgebiet)); expect(text).toContain(gedruckt(PRUEFSTAND_LABELS[quelle.pruefstand])); // Der tragende Beleg steht gedruckt da - seit 5.27.0 die Fundstelle // statt des Umkehrschlusses aus den Hamburger Zwischenzeitentafeln. expect(text).toContain(gedruckt('Am Original abgeglichen (10.09.2026)')); expect(text).toContain(gedruckt('DER FESTE ANSATZ IST DAMIT DER REGELWERKSANSATZ')); expect(text).not.toContain(gedruckt('Deshalb kein Regelwerkswert.')); }); }); describe('Tabellenausgabe: Ansatz, enger Innenradius und fSV', () => { it('nennt den Schwerverkehrsfaktor je Signalgruppe neben dem Anteil', () => { const projekt = knotenpunkt(); const plan = buildSignalPlan(projekt); const csv = signalGroupsCsv(projekt, plan); const kopf = csv.split('\r\n')[0]!; expect(kopf).toContain('Schwerverkehrsfaktor fSV'); /* * Die Reihenfolge ist die des Rechenwegs: Anteil, Aufteilung, Faktor, * Gleichung. Vor der Fassung 5.6.0 stand der Faktor unmittelbar hinter dem * Anteil; seitdem liegt die Aufteilung dazwischen, weil sie der zweite * Eingangswert der Gleichung ist und nicht ihr Ergebnis. */ const spalten = kopf.replace('', '').split(';'); const anteil = spalten.indexOf('Schwerverkehrsanteil [%]'); expect(spalten.indexOf('Lastzuganteil am Schwerverkehr [%] (leer = nicht erfasst)')).toBe( anteil + 1, ); expect(spalten.indexOf('Schwerverkehrsfaktor fSV')).toBe(anteil + 2); expect(spalten.indexOf('Gleichung für fSV (HBS 2015)')).toBe(anteil + 3); for (const nachfrage of projekt.demands) { const name = projekt.signalGroups.find((g) => g.id === nachfrage.signalGroupId)!.name; const zeile = csv.split('\r\n').find((z) => z.startsWith(`${name};`))!; // Leere Zelle fuer die Aufteilung: Fuer dieses Projekt ist keine erfasst, // und "0" waere eine Zaehlung, die niemand vorgenommen hat. expect(zeile, name).toContain( `;${fmt.numShort(nachfrage.heavyVehicleShare * 100, 1)};;` + `${fmt.numShort(schwerverkehrsfaktor(nachfrage.heavyVehicleShare), 3)};`, ); } }); it('nennt Ueberfahrzeit-Ansatz und Merkmal je Konfliktbeziehung', () => { const projekt = mitEngemRadius(mitFesterUeberfahrzeit()); const plan = buildSignalPlan(projekt); const csv = intergreenDetailCsv(projekt, plan); const zeilen = csv.split('\r\n'); const kopf = zeilen[0]!; expect(kopf).toContain('Enger Innenradius'); // "Herkunft" und nicht mehr "Ansatz" (Fassung 5.5.0, Befund F5): Der // Kfz-Ansatz erreicht Fuss, Rad und OePNV nicht, und die Spalte nennt bei // ihnen deren eigene Herleitung. expect(kopf).toContain('Überfahrzeit-Herkunft'); // Die gekennzeichnete Beziehung traegt die volle Beschriftung - eine // Tabellenausgabe hat keine Fussnote, in der der Vorbehalt sonst stuende. const erste = projekt.conflicts[0]!; const von = projekt.signalGroups.find((g) => g.id === erste.fromId)!.name; const nach = projekt.signalGroups.find((g) => g.id === erste.toId)!.name; const zeile = zeilen.find((z) => z.startsWith(`${von};${nach};`))!; expect(zeile).toContain(`;${ENGER_RADIUS_LABEL};`); /* * Die Ansatzbeschriftung steht als EIN Feld in der Zeile - mit * Anfuehrungszeichen, sobald sie ein Semikolon enthaelt, sonst ohne. * * NACHGEZOGEN (5.27.0): Hier stand die Anfuehrungszeichen-Fassung fest. * Die Beschriftung des Ansatzes "fest" nennt seit dem Abgleich am Original * die Fundstelle und enthaelt kein Semikolon mehr; die Zusicherung galt * damit einem Zeichen und nicht der Sache. Gemeint war und ist: Die * Beschriftung darf die Zeile nicht in zwei Spalten zerreissen. */ const beschriftung = UEBERFAHRZEIT_ANSATZ_LABELS.fest; const feld = beschriftung.includes(';') ? `"${beschriftung}"` : beschriftung; expect(zeile.split(';').length, 'die Beschriftung zerreißt die Zeile').toBe( kopf.split(';').length, ); expect(zeile).toContain(feld); // Alle uebrigen Beziehungen tragen "nein" - das Merkmal gilt je Beziehung // und nicht fuer den Plan. const weitere = zeilen.filter((z) => z.includes(';nein;')); expect(weitere).toHaveLength(projekt.conflicts.length - 1); }); });