lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
| 1 | import { aspectTotals } from '@/domain/plan/aspects'; |
| 2 | import { |
| 3 | UMLAUFZEIT_ERSATZWERT_VERMERK, |
| 4 | hatSaettigungsverkehrsstaerke, |
| 5 | intergreenKey, |
| 6 | umlaufzeitIstErsatzwert, |
| 7 | type GreenInterval, |
| 8 | type SignalPlan, |
| 9 | } from '@/domain/plan/signalPlan'; |
| 10 | import type { Seconds } from '@/domain/units'; |
| 11 | import { |
| 12 | MOVEMENT_LABELS, |
| 13 | TRAFFIC_MODE_LABELS, |
| 14 | fahrzeugartBeschriftung, |
| 15 | } from '@/domain/rilsa/constants'; |
| 16 | import { ueberfahrzeitHerkunft } from '@/domain/rilsa/ansaetze'; |
| 17 | import type { Project, SignalGroup } from '@/domain/model/project'; |
| 18 | import { |
| 19 | KRITERIUM_LABELS, |
| 20 | engerRadiusZelle, |
| 21 | // Anteil, Aufteilung, Faktor und Gleichung eines Stroms - aus derselben |
| 22 | // Funktion des Fachkerns, mit der der Plan die Saettigungsverkehrsstaerke |
| 23 | // bildet (plan/signalPlan.ts, groupSaturationFlow), und mit denselben Worten |
| 24 | // wie im Ausdruck (Fassung 5.6.0, Frage E6). |
| 25 | schwerverkehrsangabe, |
| 26 | verfahrenLabel, |
| 27 | } from './bewertung'; |
| 28 | import { wegherkunftZelle } from './wegherkunft'; |
| 29 | import { CATEGORY_LABELS, SEVERITY_LABELS, type ValidationReport } from '@/domain/validation'; |
| 30 | import * as fmt from '@/ui/format'; |
| 31 | |
| 32 | /** |
| 33 | * Tabellenausgabe fuer die Weiterverarbeitung. |
| 34 | * |
| 35 | * Trennzeichen ist das Semikolon und Dezimaltrennzeichen das Komma - so oeffnet |
| 36 | * Excel in deutscher Einstellung die Datei ohne Importdialog. Zusaetzlich wird |
| 37 | * eine Byte-Reihenfolge-Markierung vorangestellt, damit Umlaute richtig |
| 38 | * erkannt werden; ohne sie zeigte der Altbestand in Excel "ü" statt "ü". |
| 39 | * |
| 40 | * Die vier PLAN-Tabellen (Signalgruppen, beide Zwischenzeiten, Phasen) nehmen |
| 41 | * seit Fassung 5.4.0 (Befund C7) einen Pruefkontext entgegen und |
| 42 | * schreiben bei Warnungen einen Vermerk als letzte Zeile (csvVermerke, dort |
| 43 | * die Begruendung fuer "letzte" statt "erste"). Der Pruefbericht selbst bleibt |
| 44 | * ohne - er IST der Kontext. |
| 45 | */ |
| 46 | |
| 47 | const SEPARATOR = ';'; |
| 48 | const BOM = ''; |
| 49 | |
| 50 | /* |
| 51 | * Zeichen, mit denen eine Tabellenkalkulation eine Formel beginnen laesst. |
| 52 | * |
| 53 | * Bemerkungen und Beanstandungen sind freier Text. Beginnt eine Zelle mit |
| 54 | * einem dieser Zeichen, wertet Excel sie beim Oeffnen als Formel aus - eine |
| 55 | * Bemerkung "=Bordkante" wird zum Bezugsfehler, und boesartig gebaute Inhalte |
| 56 | * koennen weiter gehen. Ein vorangestelltes Hochkomma macht daraus wieder |
| 57 | * Text; die Tabellenkalkulation zeigt es nicht an. |
| 58 | */ |
| 59 | const FORMELSTART = /^[=+\-@\t\r]/; |
| 60 | |
| 61 | /** |
| 62 | * Eine Zahl in deutscher Schreibweise - die darf NICHT entwertet werden. |
| 63 | * |
| 64 | * Der Rechenwert der Zwischenzeit kann negativ sein (tue + tr - te vor dem |
| 65 | * Abschneiden auf 0), und ein vorangestelltes Hochkomma machte aus dieser Zahl |
| 66 | * Text. Die Spalte waere in jeder Auswertung unbrauchbar. |
| 67 | */ |
| 68 | const ZAHL = /^-?\d+(?:,\d+)?$/; |
| 69 | |
| 70 | function escape(value: string): string { |
| 71 | const sicher = FORMELSTART.test(value) && !ZAHL.test(value) ? `'${value}` : value; |
| 72 | // Auch das einzeln stehende CR. Der Datensatztrenner ist \r\n; ein CR ohne |
| 73 | // folgendes LF fiel durch die Pruefung auf \n und zerriss die Zeile. Zu |
| 74 | // erreichen ueber ein Bemerkungsfeld, in dem ein CR steht - etwa aus einer |
| 75 | // eingelesenen Projektdatei. |
| 76 | if (sicher.includes(SEPARATOR) || /["\r\n]/.test(sicher)) { |
| 77 | return `"${sicher.replace(/"/g, '""')}"`; |
| 78 | } |
| 79 | return sicher; |
| 80 | } |
| 81 | |
| 82 | function toCsv(rows: readonly (readonly string[])[]): string { |
| 83 | return BOM + rows.map((row) => row.map(escape).join(SEPARATOR)).join('\r\n'); |
| 84 | } |
| 85 | |
| 86 | /** Wahrheitswert als Anwendertext - "ja"/"nein" statt "true"/"false". */ |
| 87 | function jaNein(wert: boolean): string { |
| 88 | return wert ? 'ja' : 'nein'; |
| 89 | } |
| 90 | |
| 91 | /** |
| 92 | * Was eine PLAN-Tabelle ueber das Pruefergebnis wissen muss. |
| 93 | * |
| 94 | * Ein ganzer ValidationReport erfuellt das ebenso wie `{ warningCount: n }`; |
| 95 | * mehr braucht die Tabelle nicht, und mehr soll sie nicht sehen. Die |
| 96 | * Fehlerzahl ist freiwillig, weil die Oberflaeche eine Tabelle mit Fehlern |
| 97 | * gar nicht erst ausgibt (Sperre); wer die Funktionen unmittelbar aufruft, |
| 98 | * bekommt mit ihr den Fehler-Vermerk. |
| 99 | */ |
| 100 | export type CsvPruefkontext = Pick<ValidationReport, 'warningCount'> & |
| 101 | Partial<Pick<ValidationReport, 'errorCount'>>; |
| 102 | |
| 103 | /** |
| 104 | * Vermerkzeilen einer Plan-Tabelle - als LETZTE Zeilen, nach den Daten. |
| 105 | * |
| 106 | * KORREKTUR (Befund C7): Die Plan-Tabellen (Signalgruppen, Zwischenzeiten, |
| 107 | * Phasen) verliessen das Programm ohne jeden Bezug zum Pruefbericht - wer die |
| 108 | * Datei weiterreichte, reichte die Zahlen ohne die Warnungen weiter, die |
| 109 | * dazugehoeren. Jetzt steht in der Datei, dass der Pruefbericht Warnungen |
| 110 | * meldet. Die Zeile ist eine einzelne Zelle und keine Datenzeile. |
| 111 | * |
| 112 | * Zugleich der Ort fuer den Ersatzwert-Vermerk (Befund C11): Ist die |
| 113 | * Umlaufzeit nur ein Ersatzwert (uebersaettigt), stehen Rotzeiten, |
| 114 | * Kapazitaeten und Phasenbeginne auf einer Zahl, die kein Bemessungsergebnis |
| 115 | * ist. Das muss in der Datei stehen, nicht nur am Bildschirm. |
| 116 | * |
| 117 | * WARUM AM ENDE UND NICHT IN ZEILE 1: Die Kopfzeile bleibt Zeile 1. Erstens |
| 118 | * erwarten die bestehenden Tabellen-Tests sie dort - auch fuer einen |
| 119 | * uebersaettigten Plan (csv.test.ts, "nennt bei Ueberlastung das Kriterium"), |
| 120 | * der den Ersatzwert-Vermerk auch OHNE Pruefkontext bekommt; ein Vermerk in |
| 121 | * Zeile 1 schoebe die Kopfzeile dort weg. Zweitens sind diese Tabellen zum |
| 122 | * Auswerten gedacht ("die Stufe soll filterbar bleiben"): Excel-Autofilter, |
| 123 | * PowerQuery und jeder CSV-Leser mit Kopfzeile nehmen Zeile 1 als Kopf; eine |
| 124 | * Hinweiszeile davor macht aus dem Vermerk den Spaltennamen und aus jeder |
| 125 | * Datenzeile eine mit zu vielen Spalten. Am Ende ist der Vermerk eine |
| 126 | * einzelne Textzelle unter der letzten Datenzeile - sichtbar in jeder |
| 127 | * Tabellenkalkulation, unschaedlich fuer jede Auswertung. |
| 128 | */ |
| 129 | export function csvVermerke( |
| 130 | plan: Pick<SignalPlan, 'cycleTime' | 'cycleResult'> | null, |
| 131 | kontext?: CsvPruefkontext, |
| 132 | ): string[][] { |
| 133 | const zeilen: string[][] = []; |
| 134 | // Fehler zuerst: Die Sperre der Oberflaeche laesst eine solche Tabelle nicht |
| 135 | // hinaus; erzeugt sie jemand an ihr vorbei, sagt die Datei selbst, dass sie |
| 136 | // keine freigegebene Planunterlage ist. |
| 137 | if (kontext?.errorCount !== undefined && kontext.errorCount > 0) { |
| 138 | const anzahl = kontext.errorCount === 1 ? '1 Fehler' : `${kontext.errorCount} Fehler`; |
| 139 | zeilen.push([ |
| 140 | `Hinweis: Prüfbericht meldet ${anzahl} – die Tabelle ist keine freigegebene Planunterlage; ` + |
| 141 | 'siehe Prüfbericht', |
| 142 | ]); |
| 143 | } |
| 144 | if (kontext !== undefined && kontext.warningCount > 0) { |
| 145 | const anzahl = kontext.warningCount === 1 ? '1 Warnung' : `${kontext.warningCount} Warnungen`; |
| 146 | zeilen.push([`Hinweis: Prüfbericht meldet ${anzahl} – siehe Prüfbericht`]); |
| 147 | } |
| 148 | if (plan !== null && umlaufzeitIstErsatzwert(plan)) { |
| 149 | zeilen.push([ |
| 150 | `Hinweis: Umlaufzeit ${fmt.seconds(plan.cycleTime)} ${UMLAUFZEIT_ERSATZWERT_VERMERK}; ` + |
| 151 | 'alle umlaufabhängigen Werte dieser Tabelle beruhen auf dem Ersatzwert', |
| 152 | ]); |
| 153 | } |
| 154 | return zeilen; |
| 155 | } |
| 156 | |
| 157 | /** |
| 158 | * ALLE Freigabezeitfenster einer Signalgruppe als eine Zelle: "12–40; 60–72". |
| 159 | * |
| 160 | * KORREKTUR (Befund C22): Die Signalzeitentabelle des Ausdrucks fuehrte zwei |
| 161 | * Spalten "Freigabe von" und "Freigabe bis" und fuellte sie aus `greens[0]` - |
| 162 | * dem ERSTEN Fenster. Daneben stand in "Freigabezeit" die SUMME aller Fenster. |
| 163 | * Bei einer Signalgruppe, die in zwei getrennten Phasen freigegeben ist, ergab |
| 164 | * das eine Zeile, die sich nicht mehr aufloesen laesst: "von 12 bis 40" neben |
| 165 | * "Freigabezeit 40 s" - 28 s Fenster, 40 s Summe, und das zweite Fenster fehlt |
| 166 | * ganz. Wer danach schaltet oder danach prueft, hat eine falsche |
| 167 | * Signalzeitentabelle. Jetzt nennt eine Spalte alle Fenster, und die |
| 168 | * Summenspalte heisst Summe. |
| 169 | * |
| 170 | * Zahlen mit hoechstens einer Nachkommastelle: Die Fenstergrenzen entstehen aus |
| 171 | * der Freigabezeitverteilung und sind nicht ganzzahlig; auf ganze Sekunden |
| 172 | * gerundet ginge die Summe der gedruckten Fenster nicht mehr gegen die |
| 173 | * gedruckte Freigabezeit auf. |
| 174 | * |
| 175 | * WARUM HIER UND NICHT IM PDF: Ausdruck und Tabellenausgabe muessen dieselbe |
| 176 | * Zeichenkette zeigen - wer beide nebeneinanderlegt, vergleicht Zeile fuer |
| 177 | * Zeile. Die Funktion steht in der Tabellenausgabe, weil diese die |
| 178 | * abhaengigkeitsaermere der beiden ist: Das PDF darf die Tabellenausgabe |
| 179 | * einbinden, umgekehrt zoege die Tabellenausgabe die PDF-Bibliothek nach sich. |
| 180 | */ |
| 181 | export function freigabefensterZelle(greens: readonly GreenInterval[], cycleTime: Seconds): string { |
| 182 | if (greens.length === 0) return '–'; |
| 183 | // Dauerfreigabe ueber den ganzen Umlauf: Der Rueckfall in den Umlauf ergaebe |
| 184 | // Ende 0 und damit "0–0" - im Altbestand stand in dieser Zeile "0 s / 0 s". |
| 185 | if (greens.length === 1 && greens[0]!.duration >= cycleTime) { |
| 186 | return `0–${fmt.numShort(cycleTime, 1)} (ganzer Umlauf)`; |
| 187 | } |
| 188 | return ( |
| 189 | [...greens] |
| 190 | /* |
| 191 | * Nach Umlaufposition, nicht in Speicherreihenfolge: `greens` entsteht |
| 192 | * aus der Phasenfolge und ist nicht sortiert - eine Gruppe mit den |
| 193 | * Fenstern [116,28] und [56,28] stand als "116–24; 56–84" in der Zeile. |
| 194 | * Eine Signalzeitentabelle wird von oben nach unten gegen den Umlauf |
| 195 | * gelesen; eine Zeile, die rueckwaerts springt, liest sich wie ein Fehler |
| 196 | * in der Schaltung. |
| 197 | */ |
| 198 | .sort((a, b) => a.start - b.start) |
| 199 | .map((g) => { |
| 200 | const roh = g.start + g.duration; |
| 201 | const rest = roh % cycleTime; |
| 202 | /* |
| 203 | * Ein Fenster kann ueber den Umlaufbeginn hinausreichen; dann liegt das |
| 204 | * Ende VOR dem Beginn ("72–12"). Das ist keine Verwechslung, sondern der |
| 205 | * Sachverhalt, und die Fussnote der Tabelle sagt es. |
| 206 | * |
| 207 | * Schliesst ein Fenster dagegen GENAU am Umlaufende, ist der Rest 0 - |
| 208 | * und "60–0" behauptete ein Fenster, das rueckwaerts laeuft, statt |
| 209 | * eines, das bis zum Umlaufende reicht. Die Dauerfreigabe oben war |
| 210 | * wegen derselben Modulo-Null gesondert behandelt, dieser Fall nicht. |
| 211 | */ |
| 212 | const ende = rest === 0 && g.duration > 0 ? cycleTime : rest; |
| 213 | return `${fmt.numShort(g.start, 1)}–${fmt.numShort(ende, 1)}`; |
| 214 | }) |
| 215 | .join('; ') |
| 216 | ); |
| 217 | } |
| 218 | |
| 219 | /** |
| 220 | * Fussnote zur Freigabezeit - im Ausdruck unter der Signalzeitentabelle, in der |
| 221 | * Tabellenausgabe als Spaltenname (Befund C22). Beide Ausgaben muessen dasselbe |
| 222 | * sagen: Die Freigabezeit ist die Summe, nicht die Dauer des ersten Fensters. |
| 223 | */ |
| 224 | export const FREIGABEFENSTER_FUSSNOTE = |
| 225 | 'Freigabefenster: alle Zeitfenster mit Freigabe im Umlauf, in Sekunden ab Umlaufbeginn; mehrere ' + |
| 226 | 'Fenster entstehen, wenn eine Signalgruppe in getrennten Phasen freigegeben ist. Reicht ein ' + |
| 227 | 'Fenster über den Umlaufbeginn hinaus, liegt sein Ende vor seinem Beginn. Die Freigabezeit ist ' + |
| 228 | 'die Summe aller Fenster, nicht die Dauer des ersten.'; |
| 229 | |
| 230 | /** |
| 231 | * Die an einer Signalgruppe EINGETRAGENE Hoechstfreigabezeit - oder null, wenn |
| 232 | * keine brauchbare eingetragen ist. |
| 233 | * |
| 234 | * WARUM SIE IN DIE UNTERLAGE GEHOERT: Seit der Fassung 5.10.0 wirkt |
| 235 | * `SignalGroup.maxGreenOverride` in der Freigabezeitverteilung - |
| 236 | * `phaseMaxGreen` (plan/signalPlan.ts) setzt fuer eine Phase die KLEINSTE |
| 237 | * Vorgabe ihrer Signalgruppen an statt des Regelwerts. In keiner Ausgabe stand |
| 238 | * sie: Die gedruckte Phasendauer liess sich aus den gedruckten Groessen nicht |
| 239 | * mehr herleiten, und das ist der Zweck der Unterlage. |
| 240 | * |
| 241 | * DIE ZELLE NENNT DEN EINTRAG UND NICHT DIE GELTENDE SCHRANKE: Die erste |
| 242 | * Fassung dieser Funktion setzte ohne Eintrag den Regelwert ein und sagte zu, |
| 243 | * die Spalte nenne die Schranke, die gilt. Das war an drei Lagen widerlegt: bei |
| 244 | * einer festen Freigabezeit der Phase (sie geht der Vorgabe vor), bei einer |
| 245 | * Vorgabe unter der Mindestfreigabezeit (dann gilt diese - und der Pruefbericht |
| 246 | * derselben Unterlage sagte das Gegenteil der Tabelle) und bei einem negativen |
| 247 | * Eintrag, den das Einlesen durchlaesst. Vor allem aber ist die geltende |
| 248 | * Schranke keine Eigenschaft EINER Signalgruppe: Sie entsteht je PHASE aus der |
| 249 | * kleinsten Vorgabe aller ihrer Gruppen. Eine Zelle je Gruppe kann sie nicht |
| 250 | * nennen, ohne zu behaupten, was sie nicht weiss. |
| 251 | * |
| 252 | * Damit steht die Auswahlregel auch nur noch an einer Stelle: `phaseMaxGreen` |
| 253 | * im Fachkern entscheidet, was gilt; diese Funktion liest ein Feld. Dass die |
| 254 | * Zelle eine Vorgabe fuehrt, sagt in der Tabellenausgabe der Spaltenname; wer |
| 255 | * ueber die wirksame Schranke entscheidet und was ohne jede Vorgabe in einer |
| 256 | * Phase gilt, sagt im Ausdruck der Absatz unter der Signalgruppentabelle |
| 257 | * (pdf.ts, drawSignalGroupTable). |
| 258 | * |
| 259 | * NULL BEI EINEM UNBRAUCHBAREN EINTRAG: Ein nicht endlicher Wert hat keine |
| 260 | * lesbare Schreibweise - "NaN s" waere keine Angabe. Das Einlesen hat ihn |
| 261 | * bereits gemeldet und als nicht gesetzt behandelt (schema.ts, |
| 262 | * `optionalNumGemeldet`), und `phaseMaxGreen` uebergeht ihn ebenso. |
| 263 | * |
| 264 | * WARUM HIER UND NICHT IM PDF: derselbe Grund wie bei `freigabefensterZelle` |
| 265 | * darueber - Ausdruck und Tabellenausgabe muessen dieselbe Angabe zeigen, und |
| 266 | * die Tabellenausgabe ist die abhaengigkeitsaermere der beiden. Die |
| 267 | * Schreibweise der leeren Zelle bleibt jeder Ausgabe selbst ueberlassen: hier |
| 268 | * leer wie jede nicht gebildete Groesse, im Ausdruck "–". |
| 269 | */ |
| 270 | export function hoechstfreigabezeitVorgabe(group: SignalGroup): Seconds | null { |
| 271 | const vorgabe = group.maxGreenOverride; |
| 272 | return vorgabe === null || !Number.isFinite(vorgabe) ? null : vorgabe; |
| 273 | } |
| 274 | |
| 275 | /** Signalgruppen mit Signalzeiten und Leistungsfaehigkeit. */ |
| 276 | export function signalGroupsCsv( |
| 277 | project: Project, |
| 278 | plan: SignalPlan, |
| 279 | kontext?: CsvPruefkontext, |
| 280 | ): string { |
| 281 | const rows: string[][] = [ |
| 282 | [ |
| 283 | 'Signalgruppe', |
| 284 | 'Verkehrsart', |
| 285 | 'Fahrbeziehung', |
| 286 | 'V zul [km/h]', |
| 287 | 'Fahrstreifen', |
| 288 | 'Fahrzeugart', |
| 289 | // Merkmale der Fussgaengerfurt, die in die Mindestfreigabezeit eingehen |
| 290 | // (Befund C1): Zusatzeinrichtung fuer Blinde und Sehbehinderte (Freigabe |
| 291 | // fuer die ganze statt die halbe Furt) und erhoehter Zeitbedarf (1,0 |
| 292 | // statt 1,2 m/s). "ja"/"nein" nur fuer Fussgaengergruppen, sonst leer - |
| 293 | // wie jede Groesse, die fuer die Zeile nicht gebildet wird. Ohne die |
| 294 | // Spalten stuende eine Mindestfreigabezeit von 6 s neben einem Regelwert |
| 295 | // von 5 s ohne erkennbaren Grund. |
| 296 | 'Blindenzusatz', |
| 297 | 'Erhöhter Zeitbedarf', |
| 298 | 'Rot-Gelb [s]', |
| 299 | 'Gelb [s]', |
| 300 | 'Mindestfreigabezeit [s]', |
| 301 | /* |
| 302 | * Die Vorgabe zur oberen Schranke der Freigabezeitverteilung, neben der |
| 303 | * unteren (Fassung 5.10.0): Ohne sie stand in der Unterlage eine |
| 304 | * Phasendauer, die sich aus den gedruckten Groessen nicht herleiten |
| 305 | * liess. |
| 306 | * |
| 307 | * "VORGABE" STEHT IM SPALTENNAMEN: Die Zelle nennt den Eintrag der |
| 308 | * Signalgruppe, nicht die im Plan wirksame Schranke - die bildet |
| 309 | * `phaseMaxGreen` je PHASE aus der kleinsten Vorgabe ihrer Gruppen, und |
| 310 | * eine feste Freigabezeit der Phase wie auch die Mindestfreigabezeit |
| 311 | * gehen ihr vor (siehe `hoechstfreigabezeitVorgabe`). Leer heisst deshalb |
| 312 | * "keine Vorgabe" und nicht "keine Schranke" - dieselbe Form wie bei der |
| 313 | * Spalte "Lastzuganteil ... (leer = nicht erfasst)". |
| 314 | */ |
| 315 | 'Höchstfreigabezeit: Vorgabe der Signalgruppe [s] (leer = keine)', |
| 316 | // Befund C22: Ohne diese Spalte stand die Summe der Freigabezeiten allein |
| 317 | // da, und aus der Tabelle liess sich nicht ablesen, WANN die Gruppe Gruen |
| 318 | // hat - erst recht nicht bei mehreren Fenstern. Die Summenspalte heisst |
| 319 | // seitdem auch Summe: "Freigabezeit 40" neben "Freigabefenster 12–40; |
| 320 | // 60–72" liesse sonst das erste Fenster fuer die ganze Freigabe halten. |
| 321 | 'Freigabefenster [s]', |
| 322 | 'Freigabezeit (Summe aller Fenster) [s]', |
| 323 | 'Rot [s]', |
| 324 | 'Verkehrsstärke [Fz/h]', |
| 325 | // Der Anteil mindert die Saettigungsverkehrsstaerke und stand in keiner |
| 326 | // Ausgabe; die Kapazitaet daneben war damit nicht herleitbar. |
| 327 | // |
| 328 | // DIE EINHEIT STEHT IM KOPF, NICHT AM WERT (Fassung 5.10.0): Die Spalte |
| 329 | // trug ihre Prozentzahl ohne jeden Massstab, waehrend der Ausdruck an |
| 330 | // derselben Stelle "12 %" schreibt (pdf.ts, schwerverkehrZelle) und in |
| 331 | // der Nachbarspalte der Auslastungsgrad als Zahl zwischen 0 und 1 steht. |
| 332 | // Am Wert waere die Einheit falsch aufgehoben: "12 %" liest eine |
| 333 | // Tabellenkalkulation je nach Einstellung als Text oder als 0,12. Die |
| 334 | // Klammerform ist die der uebrigen Einheitenspalten dieser Ausgabe. |
| 335 | 'Schwerverkehrsanteil [%]', |
| 336 | /* |
| 337 | * NEU (Schema 13): die Aufteilung des Schwerverkehrs, aus der sich nach |
| 338 | * HBS 2015 Gl. 2-5 rechnen laesst. |
| 339 | * |
| 340 | * DER LEERE WERT IST EINE AUSSAGE und steht deshalb im Spaltennamen: |
| 341 | * Leer heisst "nicht gezaehlt" und fuehrt auf Gl. 2-6 mit dem |
| 342 | * Pauschalwert, "0" heisst "gezaehlt, keine Lastzuege" und fuehrt auf |
| 343 | * Gl. 2-5. Beide ergeben verschiedene Saettigungsverkehrsstaerken; in |
| 344 | * einer Auswertung, die die leere Zelle als 0 liest, waeren sie |
| 345 | * dieselbe. |
| 346 | */ |
| 347 | 'Lastzuganteil am Schwerverkehr [%] (leer = nicht erfasst)', |
| 348 | /* |
| 349 | * NEU (Fassung 5.5.0, Frage E6): der Anpassungsfaktor, |
| 350 | * mit dem der Anteil in die Saettigungsverkehrsstaerke eingeht. Zwischen |
| 351 | * dem Anteil und der Spalte daneben lag eine Umrechnung, die in keiner |
| 352 | * Ausgabe stand - und die Erlaeuterung des Ausdrucks nannte dafuer ein |
| 353 | * Pkw-Aequivalent von 2,0, das nachweislich nicht der Wert des HBS ist. |
| 354 | * |
| 355 | * BERICHTIGT (Fassung 5.10.0): Hier stand "Mit fSV laesst sich die Spalte |
| 356 | * nachrechnen: qS = 3600 / (fSV · tB)". Mit fSV allein geht das nicht - |
| 357 | * die Spalte "Saettigungsverkehrsstaerke" fuehrt qS0 · n · fA / fSV, also |
| 358 | * auch die Fahrstreifenzahl (Spalte "Fahrstreifen") und bei links und |
| 359 | * rechts abbiegenden Stroemen die Abminderung fA aus den Vorgaben. Der |
| 360 | * Erlaeuterungsabsatz des Ausdrucks nennt die vollstaendige Kette |
| 361 | * (bewertung.ts, schwerverkehrSatz); fSV ist der Teil, den die Tabelle |
| 362 | * ohne eigene Spalte nicht hergaebe. |
| 363 | */ |
| 364 | 'Schwerverkehrsfaktor fSV', |
| 365 | /* |
| 366 | * NEU (Schema 13): nach welcher Gleichung dieser Faktor gebildet wurde. |
| 367 | * Seit beide Gleichungen im Einsatz sind, laesst sich fSV aus dem Anteil |
| 368 | * allein nicht mehr nachrechnen - zwei Zeilen mit demselben |
| 369 | * Schwerverkehrsanteil koennen verschiedene Faktoren tragen. Die Spalte |
| 370 | * nennt auch die Annahme, unter der Gl. 2-6 steht (20 % Lastzuege am |
| 371 | * Schwerverkehr), und den Fall des verworfenen Eintrags: Ein |
| 372 | * unbrauchbarer Lastzuganteil faellt auf Gl. 2-6 zurueck, und das darf |
| 373 | * die Datei nicht verschweigen. |
| 374 | */ |
| 375 | 'Gleichung für fSV (HBS 2015)', |
| 376 | 'Sättigungsverkehrsstärke [Fz/h]', |
| 377 | // Die Abflusszeit tA ist die Groesse, aus der sich die Kapazitaet |
| 378 | // nachrechnen laesst (C = qS · tA/tU). Nach HBS 2015 ist sie tF + 1 s je |
| 379 | // Freigabezeitfenster, nach HCM die Freigabezeit selbst - ohne die Spalte |
| 380 | // ginge die Kapazitaet neben der Freigabezeit nicht auf (Befund B2). |
| 381 | 'Abflusszeit tA [s]', |
| 382 | 'Kapazität [Fz/h]', |
| 383 | 'Auslastungsgrad', |
| 384 | 'Mittlere Wartezeit [s]', |
| 385 | // Laengste Sperrzeit im Umlauf. Fuer Fussgaenger und Radverkehr ist sie |
| 386 | // nach HBS 2015 das Bewertungskriterium (Befund B4); fuer Kfz und OePNV |
| 387 | // eine Kenngroesse, die im Ausdruck ebenfalls steht. |
| 388 | 'Maximale Wartezeit [s]', |
| 389 | 'Qualitätsstufe', |
| 390 | // Woran die Stufe gemessen wurde: mittlere Wartezeit, maximale Wartezeit |
| 391 | // oder Ueberlastung (q > C). Ein blosses "F" liesse offen, ob die |
| 392 | // Wartezeit oder die Kapazitaet den Ausschlag gab (Befund B1). |
| 393 | 'Kriterium der Stufe', |
| 394 | // Ein blosses "B" sagt nichts: Dieselbe Wartezeit ergibt nach HBS 2015 |
| 395 | // und nach HCM verschiedene Stufen - und seit Fassung 5.4.0 |
| 396 | // (Befund B2) auch verschiedene Wartezeiten, weil beide Verfahren |
| 397 | // wirklich getrennt gerechnet werden. Benannt wie im Ausdruck. |
| 398 | 'Bewertungsverfahren', |
| 399 | ], |
| 400 | ]; |
| 401 | |
| 402 | for (const group of project.signalGroups) { |
| 403 | const planned = plan.groups.find((g) => g.groupId === group.id); |
| 404 | const demand = project.demands.find((d) => d.signalGroupId === group.id); |
| 405 | const totals = planned ? aspectTotals(planned, plan.cycleTime) : null; |
| 406 | // Fuer Fussgaenger und Radverkehr gibt es weder Kapazitaet noch Stufe nach |
| 407 | // HCM: `capacity` ist dann null und `serviceLevel` kann null sein. Beide |
| 408 | // Faelle muessen als leere Zelle erscheinen, nicht als Absturz. |
| 409 | const delay = planned?.delay ?? null; |
| 410 | const level = delay?.serviceLevel ?? null; |
| 411 | // Verkehrsstaerke und Schwerverkehrsanteil nur fuer Kfz und OePNV. Fuer |
| 412 | // Fussgaenger und Radverkehr kann eine Zahl gespeichert sein (Altdatei aus |
| 413 | // der Zeit vor Fassung 5.4.0, Befund B4) - sie geht in keine |
| 414 | // Rechnung ein, und der Pruefbericht sagt das. In der Tabelle stuende sie |
| 415 | // neben Kapazitaet und Auslastungsgrad, die es fuer diese Gruppen nicht |
| 416 | // gibt, und laese sich als Eingangsgroesse. Leer wie jede Groesse, die fuer |
| 417 | // die Zeile nicht gebildet wird. |
| 418 | const nachfrage = demand && hatSaettigungsverkehrsstaerke(group.mode) ? demand : null; |
| 419 | // Anteil, Aufteilung, Faktor und Gleichung an einer Stelle gebildet (seit |
| 420 | // Schema 13). Vorher rief diese Datei `schwerverkehrsfaktor(anteil)` selbst auf, |
| 421 | // also OHNE die Aufteilung: Bei erfassten Lastzuegen stand die Spalte fSV |
| 422 | // neben einer Saettigungsverkehrsstaerke, die zu einem anderen Faktor |
| 423 | // gehoert. |
| 424 | const schwerverkehr = nachfrage ? schwerverkehrsangabe(nachfrage) : null; |
| 425 | const maxVorgabe = hoechstfreigabezeitVorgabe(group); |
| 426 | |
| 427 | rows.push([ |
| 428 | group.name, |
| 429 | TRAFFIC_MODE_LABELS[group.mode], |
| 430 | // Dieselben Beschriftungen wie am Bildschirm und im PDF. Zuvor standen |
| 431 | // hier die Schluessel: "rechts" statt "rechts abbiegend". Ein Abgleich |
| 432 | // zwischen Tabelle und Ausdruck stimmte damit Zeile fuer Zeile nicht. |
| 433 | MOVEMENT_LABELS[group.movement], |
| 434 | fmt.numShort(group.vZul), |
| 435 | // Ueber fmt und nicht ueber String (Fassung 5.10.0): Eine gebrochene |
| 436 | // Fahrstreifenzahl stand hier als "2.5" mit englischem Punkt, gegen die |
| 437 | // Zusage im Kopf dieser Datei. Das Einlesen rundet sie seit der |
| 438 | // Fassung 5.10.0 ab und meldet die Ersetzung (schema.ts, |
| 439 | // `ganzzahlAbrunden`) - eine zweite Wache und kein Ersatz fuer diese: |
| 440 | // `SignalGroup.lanes` traegt im Typ keine Ganzzahlschranke, und die Zelle |
| 441 | // hat deutsch zu schreiben, was sie bekommt. Bei ganzen Zahlen aendert |
| 442 | // sich nichts: numShort laesst nachlaufende Nullen weg. |
| 443 | fmt.numShort(group.lanes), |
| 444 | fahrzeugartBeschriftung(group.vehicleClass, plan.defaults), |
| 445 | group.mode === 'fuss' ? jaNein(group.blindenzusatz === true) : '', |
| 446 | group.mode === 'fuss' ? jaNein(group.reducedMobility) : '', |
| 447 | planned ? fmt.numShort(planned.times.redYellow) : '', |
| 448 | planned ? fmt.numShort(planned.times.yellow) : '', |
| 449 | planned ? fmt.numShort(planned.times.minGreen) : '', |
| 450 | // Ohne `planned`: Die Vorgabe ist eine Eingangsgroesse der Gruppe und |
| 451 | // kein Ergebnis des Plans - sie steht auch dann, wenn die Gruppe keiner |
| 452 | // Phase zugeordnet ist. |
| 453 | maxVorgabe === null ? '' : fmt.numShort(maxVorgabe), |
| 454 | planned ? freigabefensterZelle(planned.greens, plan.cycleTime) : '', |
| 455 | planned ? fmt.numShort(planned.totalGreen) : '', |
| 456 | totals ? fmt.numShort(totals.rot) : '', |
| 457 | /* |
| 458 | * LEER AUCH BEI 0 (Fassung 5.10.0): Ein Datensatz ist noch keine |
| 459 | * Zaehlung. Wird in der Signalgruppentabelle allein der |
| 460 | * Schwerverkehrsanteil ausgefuellt, legt die Oberflaeche einen Datensatz |
| 461 | * mit `volume: 0` an; der Fachkern liest ihn seit Fassung 5.10.0 als |
| 462 | * "keine Angabe" (signalPlan.ts, erfassteVerkehrsstaerke; Befunde 12 und |
| 463 | * 18), der Bildschirm zeichnet das Feld leer (Befund 58), und der |
| 464 | * Ausdruck setzt an allen drei Stellen "–". Bliebe hier "0" stehen, sagte |
| 465 | * dieselbe Zeile in zwei Ausgaben zweierlei. |
| 466 | * |
| 467 | * LEER UND NICHT "–": Dieselbe Schreibweise wie in der Nachbarspalte |
| 468 | * "Lastzuganteil ... (leer = nicht erfasst)" und wie bei jeder Groesse, |
| 469 | * die diese Ausgabe fuer eine Zeile nicht bildet; ein Gedankenstrich |
| 470 | * waere in einer Zahlenspalte Text (siehe die Begruendung zur |
| 471 | * Kreuztabelle weiter unten). |
| 472 | * |
| 473 | * Die Spalten zum Schwerverkehr bleiben dagegen stehen: Der Anteil geht |
| 474 | * unabhaengig von der Verkehrsstaerke in die Saettigungsverkehrsstaerke |
| 475 | * derselben Zeile ein (signalPlan.ts, groupSaturationFlow), und ohne ihn |
| 476 | * waere die daneben gedruckte Zahl nicht mehr herleitbar. |
| 477 | */ |
| 478 | nachfrage === null || nachfrage.volume <= 0 ? '' : fmt.numShort(nachfrage.volume), |
| 479 | // Aus derselben Angabe wie fSV zwei Spalten weiter - nicht noch einmal |
| 480 | // aus `nachfrage`, sonst stuende in der einen Spalte der eingetragene |
| 481 | // und in der anderen der gerechnete Anteil (bewertung.ts, anteilProzent). |
| 482 | schwerverkehr ? schwerverkehr.anteilProzent : '', |
| 483 | schwerverkehr ? schwerverkehr.lastzuganteilProzent : '', |
| 484 | schwerverkehr ? fmt.numShort(schwerverkehr.fsv, 3) : '', |
| 485 | schwerverkehr ? schwerverkehr.gleichungSpalte : '', |
| 486 | planned?.capacity ? fmt.numShort(planned.capacity.saturationFlow) : '', |
| 487 | planned?.capacity ? fmt.numShort(planned.capacity.abflusszeit, 1) : '', |
| 488 | planned?.capacity ? fmt.numShort(planned.capacity.capacity) : '', |
| 489 | planned?.capacity?.degreeOfSaturation === undefined |
| 490 | ? '' |
| 491 | : fmt.numShort(planned.capacity.degreeOfSaturation, 3), |
| 492 | delay ? fmt.numShort(delay.averageDelay, 1) : '', |
| 493 | delay ? fmt.numShort(delay.maximumDelay, 1) : '', |
| 494 | // Nur der Buchstabe: Den Grund fuer ein F traegt die Spalte daneben; in |
| 495 | // einer Auswertung soll die Stufe filterbar bleiben. |
| 496 | level ? level.grade : '', |
| 497 | level ? KRITERIUM_LABELS[level.kriterium] : '', |
| 498 | delay ? verfahrenLabel(delay.verfahren) : '', |
| 499 | ]); |
| 500 | } |
| 501 | |
| 502 | // Rotzeit, Kapazitaet und Auslastung haengen an der Umlaufzeit - deshalb |
| 503 | // hier neben dem Warnungsvermerk auch der Ersatzwert-Vermerk (Befund C11). |
| 504 | rows.push(...csvVermerke(plan, kontext)); |
| 505 | return toCsv(rows); |
| 506 | } |
| 507 | |
| 508 | /** |
| 509 | * Was in einer Zelle der Herkunftsmatrix steht. |
| 510 | * |
| 511 | * Die Diagonale bleibt leer: Eine Signalgruppe gegen sich selbst ist kein |
| 512 | * Sachverhalt, ueber dessen Herkunft sich etwas sagen liesse. |
| 513 | */ |
| 514 | const HERKUNFT_VERTRAEGLICH = 'verträglich'; |
| 515 | |
| 516 | /** |
| 517 | * Zwischenzeitenmatrix als Kreuztabelle, gefolgt von der Herkunftsmatrix. |
| 518 | * |
| 519 | * WARUM ZWEI TABELLEN (Fassung 5.8.0): Der Ausdruck unterscheidet drei |
| 520 | * Sachverhalte, die diese Ausgabe bis dahin auf zwei Darstellungen zusammenzog. |
| 521 | * |
| 522 | * - Der Ausdruck setzt hinter eine von Hand vorgegebene Zwischenzeit einen |
| 523 | * Stern. Hier stand dieselbe Zahl ohne Kennzeichnung: Wer die Datei |
| 524 | * auswertete, sah nicht, welche Werte die Berechnung ersetzt hatten - und |
| 525 | * genau das ist der Nachweis, den eine Pruefstelle sucht. |
| 526 | * - Die leere Zelle stand fuer ZWEI Dinge: die Diagonale und ein |
| 527 | * vertraegliches Paar ohne erfasste Konfliktbeziehung. Im Ausdruck ist das |
| 528 | * erste leer und das zweite ein Punkt. Diese Ausgabe ist zugleich die |
| 529 | * einzige maschinell lesbare Quelle dafuer, WELCHE Paare vertraeglich |
| 530 | * sind - der Rechenweg (intergreenDetailCsv) fuehrt nur die erfassten |
| 531 | * Beziehungen. |
| 532 | * |
| 533 | * WARUM NICHT DER STERN WIE IM AUSDRUCK: "7*" ist keine Zahl mehr. Eine |
| 534 | * Tabellenkalkulation liest die Zelle als Text, und in einer Spalte, in der |
| 535 | * einzelne Werte vorgegeben sind, stuenden Zahl und Text gemischt - Summen, |
| 536 | * Hoechstwerte und Vergleiche schluegen dort fehl. Die Kreuztabelle wird |
| 537 | * gerechnet; das ist ihr Zweck und der Unterschied zum Ausdruck, der gelesen |
| 538 | * wird. Sie bleibt deshalb rein numerisch, und die Kennzeichnung steht in |
| 539 | * einer zweiten Kreuztabelle gleichen Zuschnitts darunter. Beide stehen in |
| 540 | * DERSELBEN Datei, weil die Oberflaeche Matrix und Rechenweg als getrennte |
| 541 | * Schaltflaechen anbietet: Wer nur die Matrix laedt, haette den Nachweis sonst |
| 542 | * nicht. |
| 543 | * |
| 544 | * Die Vermerke bleiben die letzten Zeilen der Datei (Befund C7) - die zweite |
| 545 | * Tabelle steht davor. |
| 546 | */ |
| 547 | export function intergreenCsv( |
| 548 | project: Project, |
| 549 | plan: SignalPlan, |
| 550 | kontext?: CsvPruefkontext, |
| 551 | ): string { |
| 552 | const groups = project.signalGroups; |
| 553 | const kopf = ['räumt \\ fährt ein', ...groups.map((g) => g.name)]; |
| 554 | const rows: string[][] = [kopf]; |
| 555 | |
| 556 | for (const from of groups) { |
| 557 | rows.push([ |
| 558 | from.name, |
| 559 | ...groups.map((to) => { |
| 560 | if (from.id === to.id) return ''; |
| 561 | const resolved = plan.intergreens.get(intergreenKey(from.id, to.id)); |
| 562 | return resolved ? fmt.numShort(resolved.value, 0) : ''; |
| 563 | }), |
| 564 | ]); |
| 565 | } |
| 566 | |
| 567 | /* |
| 568 | * Leerzeile als Trenner: Eine Tabellenkalkulation erkennt daran das Ende des |
| 569 | * ersten Blocks, und beim Lesen mit dem Auge steht der Kopf der zweiten |
| 570 | * Tabelle nicht unmittelbar unter der letzten Wertezeile. |
| 571 | * |
| 572 | * Die zweite Tabelle bekommt DIESELBE Kopfzeile wie die erste. Damit liegen |
| 573 | * beide Bloecke Zelle auf Zelle uebereinander - wer sie verknuepft, braucht |
| 574 | * keine Zuordnung, nur denselben Zeilen- und Spaltenschluessel. Die |
| 575 | * Titelzeile darueber sagt, was der Block enthaelt, und erklaert die leere |
| 576 | * Zelle: Sie waere sonst die einzige Angabe, die sich nicht von selbst |
| 577 | * versteht. |
| 578 | */ |
| 579 | rows.push([]); |
| 580 | rows.push(['Herkunft der Zwischenzeit – leere Zelle: dieselbe Signalgruppe']); |
| 581 | rows.push(kopf); |
| 582 | |
| 583 | for (const from of groups) { |
| 584 | rows.push([ |
| 585 | from.name, |
| 586 | ...groups.map((to) => { |
| 587 | if (from.id === to.id) return ''; |
| 588 | const resolved = plan.intergreens.get(intergreenKey(from.id, to.id)); |
| 589 | return resolved ? resolved.source : HERKUNFT_VERTRAEGLICH; |
| 590 | }), |
| 591 | ]); |
| 592 | } |
| 593 | |
| 594 | // Zwischenzeiten haengen nicht an der Umlaufzeit - nur der Warnungsvermerk. |
| 595 | rows.push(...csvVermerke(null, kontext)); |
| 596 | return toCsv(rows); |
| 597 | } |
| 598 | |
| 599 | /** Zwischenzeiten mit vollstaendigem Rechenweg. */ |
| 600 | export function intergreenDetailCsv( |
| 601 | project: Project, |
| 602 | plan: SignalPlan, |
| 603 | kontext?: CsvPruefkontext, |
| 604 | ): string { |
| 605 | const rows: string[][] = [ |
| 606 | [ |
| 607 | 'Räumende SG', |
| 608 | 'Einfahrende SG', |
| 609 | 'Räumweg [m]', |
| 610 | 'Fahrzeuglänge [m]', |
| 611 | 'Räumweg gesamt [m]', |
| 612 | 'Räumgeschwindigkeit [m/s]', |
| 613 | /* |
| 614 | * NEU (Fassung 5.5.0, Frage E1): das Merkmal der einzelnen |
| 615 | * Konfliktbeziehung, das die Raeumgeschwindigkeit daneben senkt. Ohne die |
| 616 | * Spalte stuenden in derselben Tabelle 5,0 und 7,0 m/s nebeneinander, |
| 617 | * ohne dass der Unterschied erklaerbar waere. Die Zelle nennt die volle |
| 618 | * Beschriftung samt Vorbehalt (bewertung.ts, engerRadiusZelle) - eine |
| 619 | * Tabellenausgabe hat keine Fussnote, in der er sonst stuende. |
| 620 | */ |
| 621 | 'Enger Innenradius', |
| 622 | // Sagt, wie die Raeumzeit aus den Spalten daneben entsteht: im Regelfall |
| 623 | // Raeumweg gesamt / Raeumgeschwindigkeit, beim OePNV mit Halt vor dem |
| 624 | // Knotenpunkt der Anfahransatz der RiLSA (Fall 4) - dort ist die |
| 625 | // Raeumgeschwindigkeit nur die Obergrenze beim Beschleunigen. |
| 626 | 'Räumansatz', |
| 627 | 'Räumzeit [s]', |
| 628 | 'Einfahrweg [m]', |
| 629 | 'Einfahrgeschwindigkeit [m/s]', |
| 630 | 'Einfahrzeit [s]', |
| 631 | 'Überfahrzeit [s]', |
| 632 | /* |
| 633 | * NEU (Fassung 5.5.0, Frage E2): woher die Ueberfahrzeit daneben stammt. Bei |
| 634 | * Kraftfahrzeugen ist das der gewaehlte Rechenansatz - er gilt fuer den |
| 635 | * ganzen Plan, steht aber in jeder Zeile, weil die Tabellenausgabe zum |
| 636 | * Auswerten und zum Zusammenfuehren mehrerer Plaene gedacht ist und eine |
| 637 | * Angabe, die nur einmal am Ende stuende, dabei verlorenginge. Dieselbe |
| 638 | * Beschriftung wie am Bildschirm und im Ausdruck. |
| 639 | * |
| 640 | * Bei den uebrigen Verkehrsarten steht deren eigene Herleitung da |
| 641 | * (Fassung 5.5.0, Befund F5): Der Kfz-Ansatz erreicht sie nicht, und seine Beschriftung |
| 642 | * neben einer Fussgaenger-Ueberfahrzeit war schlicht falsch. Die Spalte |
| 643 | * heisst deshalb "Herkunft" und nicht mehr "Ansatz". |
| 644 | */ |
| 645 | 'Überfahrzeit-Herkunft', |
| 646 | 'Rechenwert [s]', |
| 647 | 'Zwischenzeit [s]', |
| 648 | // "Herkunft" allein war zweideutig: Die Spalte sagt, ob der ZWISCHENZEIT- |
| 649 | // wert gerechnet oder vorgegeben ist - nicht, woher Raeum- und Einfahrweg |
| 650 | // stammen. Im PDF steht daneben eine Spalte "Herkunft der Wege"; wer |
| 651 | // beide Ausgaben nebeneinanderlegte, hielt "berechnet" fuer eine Aussage |
| 652 | // ueber die Vermessung. |
| 653 | 'Herkunft der Zwischenzeit', |
| 654 | 'Herkunft der Wege', |
| 655 | 'Bemerkung', |
| 656 | ], |
| 657 | ]; |
| 658 | |
| 659 | for (const conflict of project.conflicts) { |
| 660 | const from = project.signalGroups.find((g) => g.id === conflict.fromId); |
| 661 | const to = project.signalGroups.find((g) => g.id === conflict.toId); |
| 662 | const resolved = plan.intergreens.get(intergreenKey(conflict.fromId, conflict.toId)); |
| 663 | if (!from || !to || !resolved) continue; |
| 664 | const calc = resolved.calculation; |
| 665 | |
| 666 | rows.push([ |
| 667 | from.name, |
| 668 | to.name, |
| 669 | fmt.numShort(conflict.clearingDistance, 2), |
| 670 | calc ? fmt.numShort(calc.vehicleLength, 2) : '', |
| 671 | calc ? fmt.numShort(calc.clearingPath, 2) : '', |
| 672 | calc ? fmt.numShort(calc.clearingSpeed, 2) : '', |
| 673 | // Das Merkmal steht auch dann da, wenn die Zwischenzeit von Hand |
| 674 | // vorgegeben ist (calc === null): Es ist eine Angabe ueber die |
| 675 | // Oertlichkeit und keine Zwischengroesse der Rechnung. |
| 676 | engerRadiusZelle(conflict.engerRadius, 'nein'), |
| 677 | calc |
| 678 | ? calc.raeumansatz === 'anfahren' |
| 679 | ? `Anfahren nach Halt (RiLSA Fall 4), a = ${fmt.num(calc.anfahrbeschleunigung ?? 0, 1)} m/s²` |
| 680 | : 'konstante Geschwindigkeit' |
| 681 | : '', |
| 682 | calc ? fmt.numShort(calc.clearingTime, 3) : '', |
| 683 | fmt.numShort(conflict.enteringDistance, 2), |
| 684 | calc ? fmt.numShort(calc.enteringSpeed, 2) : '', |
| 685 | calc ? fmt.numShort(calc.enteringTime, 3) : '', |
| 686 | calc ? fmt.numShort(calc.crossingTime, 2) : '', |
| 687 | // Leer, wo keine Ueberfahrzeit gerechnet wurde: Bei vorgegebener |
| 688 | // Zwischenzeit hat der Ansatz nichts bestimmt, und eine Angabe daneben |
| 689 | // behauptete das Gegenteil. |
| 690 | // |
| 691 | // KORREKTUR (Fassung 5.5.0, Befund F5): Hier stand die Beschriftung des |
| 692 | // Kfz-Ansatzes in JEDER Zeile - auch neben der 0,00 s eines |
| 693 | // Fussgaengerstroms und neben den 5 s eines OePNV-Stroms aus der |
| 694 | // Vmax-Staffel. Der Ansatz gilt ausschliesslich fuer Kraftfahrzeuge. |
| 695 | // Woher die Ueberfahrzeit je Verkehrsart stammt, entscheidet jetzt der |
| 696 | // Fachkern (ansaetze.ts, ueberfahrzeitHerkunft). |
| 697 | // |
| 698 | // KORREKTUR (Fassung 5.9.0): Diese Aufrufstelle gab nur `vorgegeben` mit. |
| 699 | // Eine von resolveCrossingTime VERWORFENE Handeingabe - etwa 0 s neben |
| 700 | // einer Gelbzeit von 3 s - wies die Spalte weiter als "an dieser |
| 701 | // Beziehung von Hand eingetragen" aus, waehrend die Spalte links davon |
| 702 | // den Regelwert zeigte. Wie in conflictsView.ts gehen deshalb der |
| 703 | // Vorgabewert und der angesetzte Wert mit; entschieden wird im Fachkern. |
| 704 | // |
| 705 | // KORREKTUR (Fassung 5.43.0): Das VERFAHREN geht mit. An einer |
| 706 | // einstreifigen Verkehrsfuehrung steht in der Spalte "Ueberfahrzeit [s]" |
| 707 | // links davon die feste Zahl des Abschnitts 5.2.2 - 4 s -, und daneben |
| 708 | // stand die Beschriftung des Kfz-Ansatzes, der dort nichts bestimmt. |
| 709 | calc |
| 710 | ? ueberfahrzeitHerkunft(calc.verfahren, from.mode, project.settings.ueberfahrzeitAnsatz, { |
| 711 | haltVorKnoten: conflict.haltVorKnoten === true, |
| 712 | vorgegeben: conflict.crossingTimeOverride !== null, |
| 713 | ...(conflict.crossingTimeOverride !== null |
| 714 | ? { vorgabewert: conflict.crossingTimeOverride } |
| 715 | : {}), |
| 716 | angesetzt: calc.crossingTime, |
| 717 | }) |
| 718 | : '', |
| 719 | calc ? fmt.numShort(calc.raw, 3) : '', |
| 720 | fmt.numShort(resolved.value, 0), |
| 721 | resolved.source, |
| 722 | // Wie im PDF zwei getrennte Angaben: Die Herkunft der Wege fuehrt das |
| 723 | // Programm selbst, die Bemerkung ist freier Text und kann aelter sein als |
| 724 | // der Wert neben ihr. |
| 725 | wegherkunftZelle(conflict), |
| 726 | conflict.note, |
| 727 | ]); |
| 728 | } |
| 729 | |
| 730 | // Zwischenzeiten haengen nicht an der Umlaufzeit - nur der Warnungsvermerk. |
| 731 | rows.push(...csvVermerke(null, kontext)); |
| 732 | return toCsv(rows); |
| 733 | } |
| 734 | |
| 735 | /** |
| 736 | * Pruefbericht als Tabelle. |
| 737 | * |
| 738 | * Bewusst OHNE Pruefkontext und ohne Sperre (Befund C7): Der Pruefbericht ist |
| 739 | * keine Planunterlage, sondern das Dokument der Fehler - er muss immer |
| 740 | * ausgebbar sein, gerade wenn der Export gesperrt ist. |
| 741 | */ |
| 742 | export function reportCsv(report: ValidationReport): string { |
| 743 | const rows: string[][] = [ |
| 744 | ['Art', 'Bereich', 'Beanstandung', 'Beschreibung', 'Zu tun', 'Fundstelle'], |
| 745 | ]; |
| 746 | for (const finding of report.findings) { |
| 747 | rows.push([ |
| 748 | SEVERITY_LABELS[finding.severity], |
| 749 | CATEGORY_LABELS[finding.category], |
| 750 | finding.title, |
| 751 | finding.message, |
| 752 | finding.suggestion, |
| 753 | finding.reference, |
| 754 | ]); |
| 755 | } |
| 756 | return toCsv(rows); |
| 757 | } |
| 758 | |
| 759 | /** Phasen mit Freigabezeiten und Uebergaengen. */ |
| 760 | export function phasesCsv(project: Project, plan: SignalPlan, kontext?: CsvPruefkontext): string { |
| 761 | const rows: string[][] = [ |
| 762 | ['Nr.', 'Phase', 'Signalgruppen', 'Beginn [s]', 'Freigabezeit [s]', 'Übergangszeit danach [s]'], |
| 763 | ]; |
| 764 | |
| 765 | plan.phases.forEach((phase, index) => { |
| 766 | const transition = plan.transitions[index]; |
| 767 | rows.push([ |
| 768 | String(index + 1), |
| 769 | phase.name, |
| 770 | phase.signalGroupIds |
| 771 | .map((id) => project.signalGroups.find((g) => g.id === id)?.name ?? '?') |
| 772 | .join(', '), |
| 773 | fmt.numShort(phase.start, 1), |
| 774 | fmt.numShort(phase.duration, 1), |
| 775 | transition ? fmt.numShort(transition.duration, 1) : '', |
| 776 | ]); |
| 777 | }); |
| 778 | |
| 779 | // Beginn und Dauer der Phasen sind auf die Umlaufzeit verteilt - deshalb |
| 780 | // hier neben dem Warnungsvermerk auch der Ersatzwert-Vermerk (Befund C11). |
| 781 | rows.push(...csvVermerke(plan, kontext)); |
| 782 | return toCsv(rows); |
| 783 | } |