lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
| 1 | import { jsPDF } from 'jspdf'; |
| 2 | import autoTable, { type CellDef, type RowInput, type Styles } from 'jspdf-autotable'; |
| 3 | import { |
| 4 | MESSFEHLER_SCHRANKE, |
| 5 | ZEICHENFEHLER_BILDPUNKTE, |
| 6 | planungsgrundlage, |
| 7 | type Lageplan, |
| 8 | type Planungsgrundlage, |
| 9 | } from '@/domain/geometrie/lageplan'; |
| 10 | import { planblatt, type Planblatt } from '@/domain/geometrie/planblatt'; |
| 11 | import { massketten, type Masskette } from '@/domain/geometrie/masskette'; |
| 12 | import type { Blattbild } from './blattbild'; |
| 13 | import { drawLageplanblatt, type LageplanZeichnungOptionen } from '@/render/lageplanDrawing'; |
| 14 | import { drawZeitWeg, UMLAEUFE_IM_BILD } from '@/render/zeitWegDrawing'; |
| 15 | import { koordinierungslage, HINDERNIS_TEXT } from '@/domain/plan/koordinierung'; |
| 16 | import { modulo } from '@/domain/rilsa/coordination'; |
| 17 | import { aspectTotals } from '@/domain/plan/aspects'; |
| 18 | import { |
| 19 | fahrstreifengeometrie, |
| 20 | hatSaettigungsverkehrsstaerke, |
| 21 | intergreenKey, |
| 22 | massgebendeRaeumbeziehung, |
| 23 | umlaufzeitIstErsatzwert, |
| 24 | umlaufzeitMitVermerk, |
| 25 | type PlannedSignalGroup, |
| 26 | type SignalPlan, |
| 27 | } from '@/domain/plan/signalPlan'; |
| 28 | import { geometriefaktor } from '@/domain/rilsa/capacity'; |
| 29 | import { CYCLE_METHOD_LABELS, CYCLE_METHODS } from '@/domain/rilsa/cycle'; |
| 30 | import type { Bewertungsverfahren, CycleTimeResult } from '@/domain/rilsa/types'; |
| 31 | import { |
| 32 | resolveRilsaSettings, |
| 33 | settingFields, |
| 34 | settingFieldsByQuelle, |
| 35 | } from '@/domain/rilsa/settings'; |
| 36 | import { |
| 37 | FUNDSTELLEN_HINWEIS, |
| 38 | PRUEFSTAND_ERLAEUTERUNG, |
| 39 | PRUEFSTAND_LABELS, |
| 40 | PRUEFSTAND_REIHENFOLGE, |
| 41 | QUELLEN, |
| 42 | type QuellenSchluessel, |
| 43 | rechtsgrundlagenFuer, |
| 44 | } from '@/domain/rilsa/quellen'; |
| 45 | import { |
| 46 | ANLAGENART_GRENZEN, |
| 47 | ANLAGENART_LABELS, |
| 48 | ENGER_RADIUS_LABEL, |
| 49 | ENGSTELLE_PRAXIS, |
| 50 | MOVEMENT_LABELS, |
| 51 | RILSA_DEFAULTS, |
| 52 | STAURAUMBEDARF_JE_FAHRZEUG, |
| 53 | RSA_ALTERNATIVEN_ZUR_SIGNALREGELUNG, |
| 54 | TRAFFIC_MODE_LABELS, |
| 55 | UEBERFAHRZEIT_ANSATZ_LABELS, |
| 56 | fahrzeugartBeschriftung, |
| 57 | type AnlagenartGrenzen, |
| 58 | type RilsaDefaults, |
| 59 | type SchrankenHerkunft, |
| 60 | } from '@/domain/rilsa/constants'; |
| 61 | import { |
| 62 | engerRadiusRichtungSatzteil, |
| 63 | ueberfahrzeitAnsatzOhneWirkung, |
| 64 | ueberfahrzeitAnsatzRichtungSatz, |
| 65 | verfahrenDerKfzRaeumstroeme, |
| 66 | } from '@/domain/rilsa/ansaetze'; |
| 67 | import { engerRadiusWirktAn } from '@/domain/rilsa/intergreen'; |
| 68 | import { engstellenFolgen, umlaufBeiFreigabe } from '@/domain/rilsa/engstelle'; |
| 69 | import type { Project, SignalGroup } from '@/domain/model/project'; |
| 70 | import { programmanzahl } from '@/domain/model/programme'; |
| 71 | import { programmPruefungen } from '@/domain/validation'; |
| 72 | import { |
| 73 | KRITERIUM_LABELS, |
| 74 | aufteilungslage, |
| 75 | engerRadiusZelle, |
| 76 | // Regelwert und angesetzter Wert je Kennwert - aus derselben Quelle wie die |
| 77 | // Aufstellung "Abweichungen von den Regelwerten". |
| 78 | fundstellenkennwerte, |
| 79 | quellenOhneFeld, |
| 80 | // Anteil, Aufteilung, Faktor und Gleichung eines Stroms - gebildet aus |
| 81 | // derselben Funktion des Fachkerns, mit der der Plan rechnet |
| 82 | // (plan/signalPlan.ts, groupSaturationFlow), und mit denselben Worten wie in |
| 83 | // der Tabellenausgabe (Fassung 5.6.0, Frage E6). |
| 84 | schwerverkehrsangabe, |
| 85 | schwerverkehrSatz, |
| 86 | stufeZelle, |
| 87 | stufentafeln, |
| 88 | ueberfahrzeitAnsatzSatz, |
| 89 | verfahrenLabel, |
| 90 | wirkungsvermerk, |
| 91 | } from './bewertung'; |
| 92 | import { wegherkunftZelle } from './wegherkunft'; |
| 93 | // Befund C22: Ausdruck und Tabellenausgabe muessen dieselbe Fensterangabe |
| 94 | // zeigen; die Zeichenkette entsteht deshalb an einer Stelle (siehe dort). |
| 95 | import { FREIGABEFENSTER_FUSSNOTE, freigabefensterZelle, hoechstfreigabezeitVorgabe } from './csv'; |
| 96 | import { CATEGORY_LABELS, SEVERITY_LABELS, type ValidationReport } from '@/domain/validation'; |
| 97 | import { messeignungSatz } from '@/domain/validation/rules'; |
| 98 | import { PdfSurface, toWinAnsi } from '@/render/pdfSurface'; |
| 99 | import { FASSUNGSANGABE } from '@/fassung'; |
| 100 | import { |
| 101 | DEFAULT_LAYOUT, |
| 102 | LIGHT_THEME, |
| 103 | drawSignalPlan, |
| 104 | planHeight, |
| 105 | type PlanLayout, |
| 106 | } from '@/render/signalPlanDrawing'; |
| 107 | import * as fmt from '@/ui/format'; |
| 108 | |
| 109 | /** |
| 110 | * Ausdruck der Planunterlagen. |
| 111 | * |
| 112 | * Aufbau: Deckblatt, Planungsgrundlage, Signalgruppen, Zwischenzeitenmatrix, |
| 113 | * Phasen, Signalzeitenplan als Vektorgrafik, Leistungsfaehigkeit, Pruefbericht. |
| 114 | * |
| 115 | * Korrekturen gegenueber dem Altbestand: |
| 116 | * - Der Ausdruck enthielt eine unbedingte Zeile "Die Anlage entspricht den |
| 117 | * Anforderungen und wird zur Genehmigung empfohlen" - unabhaengig davon, ob |
| 118 | * die Pruefung Fehler ergeben hatte. Diese Aussage ist entfallen; stattdessen |
| 119 | * steht das tatsaechliche Pruefergebnis im Dokument. |
| 120 | * - Der Signalzeitenplan wurde als Rastergrafik eingebettet; jetzt als Vektor. |
| 121 | * - Haken, Kreuze und Emoji sind mit den PDF-Standardschriften nicht |
| 122 | * darstellbar und erschienen als leere Kaesten. Sie sind durch Text ersetzt. |
| 123 | * - Befunde B1 bis B4 (Fassung 5.4.0): Der Leistungsabschnitt |
| 124 | * behauptete "Bewertung nach HBS 2015" fuer eine HCM-Rechnung mit der |
| 125 | * Kfz-Tafel des HBS 2001 und liess die Fussgaenger als "nicht massgebend" |
| 126 | * weg. Jetzt nennt er das gerechnete Verfahren samt Formelkette und Tafel, |
| 127 | * druckt die Abflusszeit tA und bewertet Fussgaenger und Radverkehr in |
| 128 | * eigener Tabelle nach der maximalen Wartezeit. |
| 129 | */ |
| 130 | |
| 131 | const PAGE_MARGIN = 36; |
| 132 | const HEADER_HEIGHT = 46; |
| 133 | const FOOTER_HEIGHT = 26; |
| 134 | |
| 135 | /** |
| 136 | * Abstand zwischen Projektname und Grundlagenangabe in der Kopfzeile. |
| 137 | * |
| 138 | * Ohne ihn stiessen die beiden Texte bei einem langen Namen unmittelbar |
| 139 | * aneinander und lasen sich als ein Wort. |
| 140 | */ |
| 141 | const KOPFZEILEN_ABSTAND = 12; |
| 142 | |
| 143 | /** Zeilenabstand des Deckblatttitels (15 pt fett). */ |
| 144 | const TITEL_ZEILENHOEHE = 18; |
| 145 | |
| 146 | /** |
| 147 | * Hoehe des Kastens "Ergebnis der Pruefung" auf dem Deckblatt. |
| 148 | * |
| 149 | * Sie steht einmal, weil sie an drei Stellen gebraucht wird: zum Reservieren |
| 150 | * des Platzes, zum Zeichnen der Flaeche und zum Weiterruecken auf den |
| 151 | * Bemerkungsblock darunter. Liefen die ersten beiden auseinander, waere die |
| 152 | * Umbruchpruefung wieder wertlos; liefe die dritte mit, schoebe sich die |
| 153 | * Ueberschrift "Bemerkungen" in den Kasten hinein. |
| 154 | */ |
| 155 | const ERGEBNISKASTEN_HOEHE = 52; |
| 156 | |
| 157 | /** Abstand zwischen der Unterkante des Ergebniskastens und dem, was folgt. */ |
| 158 | const ERGEBNISKASTEN_NACHLAUF = 16; |
| 159 | |
| 160 | /** |
| 161 | * Der Satzspiegel, in dem JEDE Tabelle dieser Unterlage steht. |
| 162 | * |
| 163 | * OBEN UND UNTEN GEHOEREN HIER HINEIN, und das ist kein Beiwerk: Wo `margin` |
| 164 | * nur `left` und `right` nennt, setzt jspdf-autotable fuer die beiden anderen |
| 165 | * seinen eigenen Regelwert von 40 pt an |
| 166 | * (`parseSpacing(options.margin, 40 / doc.scaleFactor())`, |
| 167 | * jspdf.plugin.autotable.js:937). Das Folgeblatt einer mehrseitigen Tabelle |
| 168 | * beginnt dann bei 40 pt - mitten in der Kopfzeile, die von PAGE_MARGIN bis |
| 169 | * PAGE_MARGIN + HEADER_HEIGHT reicht, also bis 82 pt. Die wiederholte |
| 170 | * Tabellenkopfzeile ueberdruckte dort Projektname und Untertitel. Gemessen an |
| 171 | * der vollstaendigen Unterlage: bei sechzehn Signalgruppen mit langen Namen |
| 172 | * trugen 41 von 62 Blaettern Tabellentext oberhalb der Trennlinie. Unten |
| 173 | * dasselbe in klein: mit 40 statt 62 pt durfte eine Tabelle 22 pt in den |
| 174 | * Fusssteg laufen. |
| 175 | * |
| 176 | * WARUM EINE KONSTANTE UND NICHT ZWANZIG EINZELANGABEN: Der Fehler entstand |
| 177 | * dadurch, dass jeder Aufruf seinen Rand selbst mitbrachte und dabei zwei |
| 178 | * Seiten vergass. Eine Tabelle, die kuenftig hinzukommt, erbt den richtigen |
| 179 | * Satzspiegel jetzt, statt ihn erneut zusammenzusuchen. |
| 180 | */ |
| 181 | const TABELLENRAND = { |
| 182 | left: PAGE_MARGIN, |
| 183 | right: PAGE_MARGIN, |
| 184 | top: PAGE_MARGIN + HEADER_HEIGHT, |
| 185 | bottom: PAGE_MARGIN + FOOTER_HEIGHT, |
| 186 | } as const; |
| 187 | |
| 188 | export interface PdfOptions { |
| 189 | readonly includeSignalGroups: boolean; |
| 190 | readonly includeIntergreenMatrix: boolean; |
| 191 | readonly includePhases: boolean; |
| 192 | /** Die vier Rechenansaetze der Umlaufzeit nebeneinander. */ |
| 193 | readonly includeCycleComparison: boolean; |
| 194 | readonly includePlan: boolean; |
| 195 | readonly includePerformance: boolean; |
| 196 | readonly includeReport: boolean; |
| 197 | readonly includeCalculationDetails: boolean; |
| 198 | /** Nachweis, woher die Kennwerte stammen - fuer die Pruefung der Unterlagen. */ |
| 199 | readonly includeSources: boolean; |
| 200 | /** Selbst gepruefte Fundstellen, je Sachgebiet. */ |
| 201 | readonly fundstellen: Readonly<Record<string, string>>; |
| 202 | } |
| 203 | |
| 204 | export const DEFAULT_PDF_OPTIONS: PdfOptions = { |
| 205 | includeSignalGroups: true, |
| 206 | includeIntergreenMatrix: true, |
| 207 | includePhases: true, |
| 208 | /* |
| 209 | * VORBELEGT MIT true (auf ausdrueckliche Entscheidung des Anwenders): Eine |
| 210 | * Anordnung nach Paragraf 45 StVO ist zu begruenden, und die Wahl der |
| 211 | * Umlaufzeit war der eine Punkt, zu dem die Unterlage nichts sagte. Wer den |
| 212 | * Abschnitt nicht will, waehlt ihn ab; die Bestandteileuebersicht vermerkt |
| 213 | * das dann. |
| 214 | */ |
| 215 | includeCycleComparison: true, |
| 216 | includePlan: true, |
| 217 | includePerformance: true, |
| 218 | includeReport: true, |
| 219 | /* |
| 220 | * KORREKTUR (Fassung 5.4.0, Befund C15): Hier stand false. Der Ausdruck |
| 221 | * lieferte damit von Haus aus eine Zwischenzeitenmatrix ohne den Nachweis, |
| 222 | * aus dem ihre Zahlen stammen - und genau diesen Nachweis verlangt die |
| 223 | * Behoerdenpraxis. Wer ihn abwaehlt, liest jetzt an der Stelle des |
| 224 | * Abschnitts, was der Unterlage damit fehlt (RECHENWEG_ABGEWAEHLT). |
| 225 | */ |
| 226 | includeCalculationDetails: true, |
| 227 | includeSources: true, |
| 228 | fundstellen: {}, |
| 229 | }; |
| 230 | |
| 231 | /** |
| 232 | * Wo der Rechenweg in DIESER Ausgabe steht - und wo nicht. |
| 233 | * |
| 234 | * Der Rechenweg haengt an ZWEI Bedingungen: am eigenen Schalter |
| 235 | * (`includeCalculationDetails`) und daran, ob der Abschnitt ueberhaupt gedruckt |
| 236 | * wird, in dem er steht - der Zwischenzeitennachweis in der |
| 237 | * Zwischenzeitenmatrix, der Wartezeitnachweis in der Leistungsfaehigkeit. |
| 238 | * |
| 239 | * KORREKTUR (Fassung 5.4.0): Die Uebersicht "Bestandteile der |
| 240 | * Anordnungsunterlage" fragte nur den Schalter ab und druckte "ja - Abschnitte |
| 241 | * ... und ..." fuer Abschnitte, die im Dokument gar nicht vorkamen; der |
| 242 | * Abwahlvermerk wiederum stand nur INNERHALB der beiden Abschnitte und fiel |
| 243 | * damit ganz weg, wenn beide abgewaehlt waren. Ausgerechnet der Lueckenausweis |
| 244 | * gab damit eine falsche Auskunft. Beide Aussagen kommen jetzt aus diesem einen |
| 245 | * Stand. |
| 246 | */ |
| 247 | interface RechenwegStand { |
| 248 | /** Ist der Rechenweg selbst angefordert? */ |
| 249 | readonly angefordert: boolean; |
| 250 | /** Wird die Zwischenzeitenmatrix gedruckt - der Ort des ersten Rechenwegs? */ |
| 251 | readonly matrix: boolean; |
| 252 | /** Wird die Leistungsfaehigkeit gedruckt - der Ort des zweiten Rechenwegs? */ |
| 253 | readonly leistung: boolean; |
| 254 | } |
| 255 | |
| 256 | function rechenwegStand(options: PdfOptions): RechenwegStand { |
| 257 | return { |
| 258 | angefordert: options.includeCalculationDetails, |
| 259 | matrix: options.includeIntergreenMatrix, |
| 260 | leistung: options.includePerformance, |
| 261 | }; |
| 262 | } |
| 263 | |
| 264 | /** Steht der Rechenweg der Zwischenzeiten in dieser Ausgabe? */ |
| 265 | function zwischenzeitRechenweg(stand: RechenwegStand): boolean { |
| 266 | return stand.angefordert && stand.matrix; |
| 267 | } |
| 268 | |
| 269 | /** Steht der Rechenweg der Wartezeit in dieser Ausgabe? */ |
| 270 | function wartezeitRechenweg(stand: RechenwegStand): boolean { |
| 271 | return stand.angefordert && stand.leistung; |
| 272 | } |
| 273 | |
| 274 | /** |
| 275 | * Was an der Stelle des Rechenwegs steht, wenn er abgewaehlt wurde |
| 276 | * (Befund C15). |
| 277 | * |
| 278 | * Der Satz steht EINMAL im Dokument, weil er beide Rechenwege nennt - den der |
| 279 | * Zwischenzeiten und den der Wartezeit -, und zwar an der ersten Stelle, an der |
| 280 | * einer von beiden gestanden haette. Eine Luecke, die nirgends vermerkt ist, |
| 281 | * sieht wie eine vollstaendige Unterlage aus; das ist der Fehler, um den es |
| 282 | * geht. |
| 283 | * |
| 284 | * Die zweite Haelfte nennt nur Abschnitte, die es in dieser Ausgabe wirklich |
| 285 | * gibt: Bei abgewaehlter Matrix von "den Werten der Matrix" zu sprechen, waere |
| 286 | * derselbe Fehler eine Ebene tiefer. Sind beide Abschnitte abgewaehlt, gibt es |
| 287 | * im Dokument keine Stelle mehr, an der der Rechenweg gestanden haette - der |
| 288 | * Satz steht dann in der Uebersicht der Bestandteile und sagt genau das. |
| 289 | */ |
| 290 | function rechenwegAbgewaehlt(stand: RechenwegStand): string { |
| 291 | const kopf = |
| 292 | 'Der Rechenweg der Zwischenzeiten und der Wartezeit ist in dieser Ausgabe abgewählt; '; |
| 293 | if (stand.matrix && stand.leistung) { |
| 294 | return ( |
| 295 | kopf + |
| 296 | 'die Werte der Zwischenzeitenmatrix und der Leistungsfähigkeit sind ohne ihn nicht ' + |
| 297 | 'nachrechenbar.' |
| 298 | ); |
| 299 | } |
| 300 | if (stand.matrix) return kopf + 'die Werte der Matrix sind ohne ihn nicht nachrechenbar.'; |
| 301 | if (stand.leistung) { |
| 302 | return kopf + 'die Werte der Leistungsfähigkeit sind ohne ihn nicht nachrechenbar.'; |
| 303 | } |
| 304 | return ( |
| 305 | kopf + |
| 306 | 'die Abschnitte, in denen er gestanden hätte - Zwischenzeitenmatrix und Leistungsfähigkeit -, ' + |
| 307 | 'sind ebenfalls abgewählt.' |
| 308 | ); |
| 309 | } |
| 310 | |
| 311 | interface Context { |
| 312 | readonly doc: jsPDF; |
| 313 | readonly project: Project; |
| 314 | readonly plan: SignalPlan; |
| 315 | readonly report: ValidationReport; |
| 316 | readonly pageWidth: number; |
| 317 | readonly pageHeight: number; |
| 318 | readonly contentWidth: number; |
| 319 | /** |
| 320 | * Das vorbereitete Luftbild fuer das zweite Lageplanblatt - oder null. |
| 321 | * |
| 322 | * Es wird HEREINGEREICHT und nicht hier erzeugt: Zuschneiden und Verkleinern |
| 323 | * gehen nur asynchron (siehe `blattbild.ts`), und diese Funktion ist synchron. |
| 324 | */ |
| 325 | readonly blattbild: Blattbild | null; |
| 326 | cursor: number; |
| 327 | /** |
| 328 | * Wie die Zwischenzeitenmatrix DIESER Ausgabe gesetzt wurde - oder null, |
| 329 | * solange sie nicht gezeichnet ist. |
| 330 | * |
| 331 | * Gesetzt wird das erst beim Zeichnen der Matrix, gelesen erst im Abschnitt |
| 332 | * "Barrierefreiheit dieser Datei" - der steht am Ende und beschreibt die |
| 333 | * fertige Datei. Er darf keine Aussage ueber eine Darstellung machen, die in |
| 334 | * dieser Ausgabe gar nicht vorkommt: Wer kurze Bezeichnungen benutzt, hat |
| 335 | * die Namen im Kopf, und ein Absatz ueber Ordnungsnummern waere fuer SEINE |
| 336 | * Datei schlicht falsch. |
| 337 | * |
| 338 | * WARUM EIN BEFUND UND NICHT NUR EIN SCHALTER (Fassung 5.8.0): Der Absatz |
| 339 | * sagte bis dahin unbedingt zu, jeder Name stehe in der Zuordnung "ungeteilt |
| 340 | * in einer Zeile". Das trifft nur zu, solange der Name in eine Bannerzeile |
| 341 | * passt - gemessen bis rund 180 Zeichen. Ein Lueckenausweis darf keine Zusage |
| 342 | * machen, die er fuer DIESE Datei nicht halten kann; also traegt der Befund |
| 343 | * auch, ob es zugetroffen hat. |
| 344 | */ |
| 345 | matrixSatzbefund: MatrixSatzbefund | null; |
| 346 | } |
| 347 | |
| 348 | /** Was ueber den Satz der Matrix in dieser Ausgabe zu sagen ist. */ |
| 349 | interface MatrixSatzbefund { |
| 350 | /** Tragen die Spaltenkoepfe Ordnungsnummern statt der Namen? */ |
| 351 | readonly mitOrdnungsnummern: boolean; |
| 352 | /** Steht in der Zuordnung ueber der Tabelle JEDER Name ungeteilt in einer Zeile? */ |
| 353 | readonly namenUngeteilt: boolean; |
| 354 | /** |
| 355 | * Wird die Zuordnung auf JEDEM Blatt des Ausschnitts wiederholt? |
| 356 | * |
| 357 | * Sie wird es, so lange sie in den Tabellenkopf passt. Ist sie fuer sich |
| 358 | * schon hoeher als ein Blatt, steht sie ungekuerzt vor dem Ausschnitt - und |
| 359 | * dann darf der Lueckenausweis nicht behaupten, sie stehe ueber jeder Seite. |
| 360 | */ |
| 361 | readonly zuordnungAufJedemBlatt: boolean; |
| 362 | } |
| 363 | |
| 364 | /** |
| 365 | * Ein leeres Dokument in der Seiteneinrichtung dieser Unterlage. |
| 366 | * |
| 367 | * EIGENE FUNKTION, WEIL ES ZWEI AUFRUFER GIBT: den Ausdruck und |
| 368 | * `lageplanblattVorlage`. Die Blattbreite folgt aus der Seitengroesse, und |
| 369 | * das Luftbild muss fuer GENAU das Blatt zugeschnitten werden, auf das es |
| 370 | * spaeter kommt. Stuende die Einrichtung zweimal im Baum, waere ein |
| 371 | * verschobenes Bild die erste Folge einer Aenderung an ihr - und das faellt |
| 372 | * niemandem auf, der die Seitengroesse aendert. |
| 373 | */ |
| 374 | function neuesDokument(): jsPDF { |
| 375 | return new jsPDF({ orientation: 'landscape', unit: 'pt', format: 'a4', compress: true }); |
| 376 | } |
| 377 | |
| 378 | /** Hoehe des Lageplanblatts in Blattpunkten. */ |
| 379 | export const LAGEPLANBLATT_HOEHE = 300; |
| 380 | |
| 381 | /** |
| 382 | * Das Blatt, auf das die Unterlage den Lageplan zeichnet. |
| 383 | * |
| 384 | * Oeffentlich, damit die Oberflaeche das Luftbild fuer DIESES Blatt vorbereiten |
| 385 | * kann, bevor sie den Ausdruck anstoesst. Sie darf die Masse nicht selbst |
| 386 | * bilden: Ein um einen Punkt abweichendes Blatt ergaebe ein um einen Punkt |
| 387 | * verschobenes Bild, und Linien, die neben der Fahrbahn liegen, sind auf einem |
| 388 | * Nachweisblatt keine Kleinigkeit. |
| 389 | */ |
| 390 | export function lageplanblattVorlage(lageplan: Lageplan): Planblatt | null { |
| 391 | const breite = neuesDokument().internal.pageSize.getWidth() - 2 * PAGE_MARGIN; |
| 392 | return planblatt(lageplan, breite, LAGEPLANBLATT_HOEHE); |
| 393 | } |
| 394 | |
| 395 | /** |
| 396 | * Erzeugt die Planunterlagen als PDF. |
| 397 | * Rueckgabe sind die rohen Bytes; das Einpacken in eine Datei uebernimmt die |
| 398 | * Speicherschicht, damit dieselbe Funktion auch im Test verwendbar bleibt. |
| 399 | */ |
| 400 | export function buildProjectPdf( |
| 401 | project: Project, |
| 402 | plan: SignalPlan, |
| 403 | report: ValidationReport, |
| 404 | options: PdfOptions = DEFAULT_PDF_OPTIONS, |
| 405 | now: Date = new Date(), |
| 406 | blattbild: Blattbild | null = null, |
| 407 | ): Uint8Array { |
| 408 | const doc = neuesDokument(); |
| 409 | doc.setLanguage('de-DE'); |
| 410 | |
| 411 | /* |
| 412 | * Befund L10 (2.4.2 Seite mit Titel, A; EN 301 549 10.2.4.2; WCAG-Technik |
| 413 | * PDF18). |
| 414 | * |
| 415 | * /Title war gesetzt und ist immer sprechend - ein leerer Projektname wird |
| 416 | * vor der Ausgabe als Fehler gemeldet. Ohne /DisplayDocTitle zeigen |
| 417 | * Anzeigeprogramme in Fenster- und Registerleiste trotzdem den DATEINAMEN. |
| 418 | * Eine Sprachausgabe sagt beim Wechsel zwischen offenen Dokumenten dann |
| 419 | * "plan-2026-03-11 (2).pdf" statt "Musterkreuzung - Signalzeitenplan". |
| 420 | */ |
| 421 | doc.viewerPreferences({ DisplayDocTitle: true }); |
| 422 | |
| 423 | /* |
| 424 | * HIER NICHT /MarkInfo << /Marked true >> ERGAENZEN. |
| 425 | * |
| 426 | * Befund S1. Ueber die Ereignishaken von jsPDF liesse sich der Eintrag in |
| 427 | * drei Zeilen setzen, und das Dokument bestuende danach manche automatische |
| 428 | * Pruefung. Es waere eine falsche Angabe: Ein Strukturbaum (/StructTreeRoot) |
| 429 | * entsteht dadurch nicht, und jsPDF 2.5.2 kann keinen erzeugen - es gibt |
| 430 | * keine Schnittstelle fuer Marked Content, Artefakte oder Alternativtexte, |
| 431 | * und jspdf-autotable 3.8.4 hilft dabei nicht. Bildschirmleseprogramme |
| 432 | * schalten bei "/Marked true" von ihren Positionsheuristiken auf den - dann |
| 433 | * leeren - Strukturbaum um und lesen danach WENIGER vor als heute. Die |
| 434 | * Behauptung machte das Dokument also fuer genau die Nutzergruppe schlechter, |
| 435 | * der sie zu nuetzen vorgibt. |
| 436 | * |
| 437 | * Der Weg zu einem wirklich getaggten Dokument fuehrt ueber eine andere |
| 438 | * Ausgabe (webContents.printToPDF mit generateTaggedPDF aus einer |
| 439 | * semantischen Vorlage), nicht ueber einen Katalogeintrag. Bis dahin sagt |
| 440 | * der Abschnitt "Barrierefreiheit dieser Datei" am Ende der Unterlage, wie |
| 441 | * es steht - siehe drawBarrierefreiheit. |
| 442 | */ |
| 443 | |
| 444 | const context: Context = { |
| 445 | doc, |
| 446 | project, |
| 447 | plan, |
| 448 | report, |
| 449 | pageWidth: doc.internal.pageSize.getWidth(), |
| 450 | pageHeight: doc.internal.pageSize.getHeight(), |
| 451 | contentWidth: doc.internal.pageSize.getWidth() - 2 * PAGE_MARGIN, |
| 452 | blattbild, |
| 453 | cursor: PAGE_MARGIN + HEADER_HEIGHT, |
| 454 | matrixSatzbefund: null, |
| 455 | }; |
| 456 | |
| 457 | doc.setProperties({ |
| 458 | title: toWinAnsi(`${project.meta.name} – Signalzeitenplan`), |
| 459 | subject: toWinAnsi(`Lichtsignalanlage ${project.meta.location}`), |
| 460 | author: toWinAnsi(project.meta.planner), |
| 461 | creator: 'LSA-Planer Professional', |
| 462 | }); |
| 463 | |
| 464 | // Erstes Lesezeichen; die uebrigen entstehen in newSection. |
| 465 | merkeLesezeichen(context, 'Deckblatt'); |
| 466 | drawCoverSheet(context, now); |
| 467 | |
| 468 | const rechenweg = rechenwegStand(options); |
| 469 | |
| 470 | // Ebenfalls ohne Schalter (Befund C14): Der Abschnitt sagt, WAS diese |
| 471 | // Unterlage enthaelt und was nicht. Ihn abwaehlbar zu machen hiesse, den |
| 472 | // Lueckenausweis abwaehlbar zu machen. |
| 473 | drawAnordnungsunterlage(context, options, rechenweg); |
| 474 | |
| 475 | // Bewusst ohne Schalter in PdfOptions - wie drawSettingsDeviations weiter |
| 476 | // unten ist der Abschnitt ein Nachweis. Ein Ausdruck, dem man die |
| 477 | // Vermessungsgrundlage abwaehlen kann, ist als Planunterlage wertlos. |
| 478 | drawPlanungsgrundlage(context, rechenweg); |
| 479 | |
| 480 | /* |
| 481 | * Ebenfalls ohne Schalter: Wo mehrere Tagesprogramme nebeneinander |
| 482 | * stehen, ist die Auskunft, WELCHES dieser Ausdruck zeigt, kein Beiwerk. Bei |
| 483 | * einem einzigen Programm entfaellt der Abschnitt - dort gibt es nichts zu |
| 484 | * unterscheiden, und die Bestandteileuebersicht sagt es in einer Zeile. |
| 485 | */ |
| 486 | if (programmanzahl(project) > 1) { |
| 487 | newSection(context, 'Tagesprogramme dieser Anlage'); |
| 488 | drawProgrammuebersicht(context, now); |
| 489 | } |
| 490 | |
| 491 | if (options.includeSignalGroups) { |
| 492 | newSection(context, 'Signalgruppen'); |
| 493 | drawSignalGroupTable(context); |
| 494 | } |
| 495 | |
| 496 | /* |
| 497 | * Befund C15: Der Vermerk ueber den abgewaehlten Rechenweg steht an der |
| 498 | * Stelle, an der der Abschnitt gestanden haette - einmal, denn der Satz nennt |
| 499 | * beide Rechenwege. |
| 500 | * |
| 501 | * Sind beide moeglichen Stellen abgewaehlt, hat ihn die Uebersicht der |
| 502 | * Bestandteile bereits gedruckt; dann gilt er hier schon als vermerkt. |
| 503 | */ |
| 504 | let rechenwegVermerkt = rechenweg.angefordert || (!rechenweg.matrix && !rechenweg.leistung); |
| 505 | |
| 506 | if (options.includeIntergreenMatrix) { |
| 507 | newSection(context, 'Zwischenzeitenmatrix'); |
| 508 | drawIntergreenMatrix(context, options.includeSignalGroups); |
| 509 | if (zwischenzeitRechenweg(rechenweg)) { |
| 510 | newSection(context, 'Zwischenzeiten – Rechenweg'); |
| 511 | drawIntergreenDetails(context, options.includeSignalGroups); |
| 512 | } else { |
| 513 | writeParagraph(context, rechenwegAbgewaehlt(rechenweg)); |
| 514 | rechenwegVermerkt = true; |
| 515 | } |
| 516 | } |
| 517 | |
| 518 | if (options.includePhases) { |
| 519 | newSection(context, 'Phasen und Phasenübergänge'); |
| 520 | drawPhaseTables(context); |
| 521 | } |
| 522 | |
| 523 | if (options.includeCycleComparison) { |
| 524 | newSection(context, 'Umlaufzeit – Vergleich der Rechenansätze'); |
| 525 | drawCycleComparison(context); |
| 526 | } |
| 527 | |
| 528 | /* |
| 529 | * Die Koordinierung steht VOR dem Signalzeitenplan: Sie sagt, in welchem |
| 530 | * Strassenzug dieser Plan liegt, und das ist eine Randbedingung seiner |
| 531 | * Zeiten - kein Nachtrag. Ohne erfassten Strassenzug entfaellt der |
| 532 | * Abschnitt; die Bestandteileuebersicht sagt es dann in ihrer Zeile. |
| 533 | */ |
| 534 | if (project.koordinierung !== null) { |
| 535 | newSection(context, 'Koordinierung'); |
| 536 | drawKoordinierung(context); |
| 537 | } |
| 538 | |
| 539 | if (options.includePlan) { |
| 540 | newSection(context, 'Signalzeitenplan'); |
| 541 | drawPlanGraphic(context, options.includePhases); |
| 542 | drawSignalTimeTable(context); |
| 543 | } |
| 544 | |
| 545 | if (options.includePerformance) { |
| 546 | newSection(context, 'Leistungsfähigkeit'); |
| 547 | drawPerformanceTable(context, options.includeSignalGroups); |
| 548 | drawFussRadTable(context); |
| 549 | // Wie beim Rechenweg der Zwischenzeiten: Die Formelkette der Wartezeit |
| 550 | // wird auf Wunsch mitgedruckt, damit sich tW aus den gedruckten Groessen |
| 551 | // nachrechnen laesst. |
| 552 | if (wartezeitRechenweg(rechenweg)) { |
| 553 | newSection(context, 'Leistungsfähigkeit – Rechenweg Wartezeit'); |
| 554 | drawDelayDetails(context); |
| 555 | } else if (!rechenwegVermerkt) { |
| 556 | // Letzte Stelle, an der der Vermerk stehen kann - danach wird er nicht |
| 557 | // mehr abgefragt, deshalb hier ohne weitere Zuweisung. |
| 558 | writeParagraph(context, rechenwegAbgewaehlt(rechenweg)); |
| 559 | } |
| 560 | } |
| 561 | |
| 562 | if (options.includeReport) { |
| 563 | newSection(context, 'Prüfbericht'); |
| 564 | drawReport(context); |
| 565 | } |
| 566 | |
| 567 | drawSettingsDeviations(context); |
| 568 | |
| 569 | if (options.includeSources) { |
| 570 | newSection(context, 'Grundlagen der Kennwerte'); |
| 571 | drawLegalBasis(context, options.includePerformance); |
| 572 | drawAnlagenartGrenzen(context); |
| 573 | drawSourceRegister(context, options); |
| 574 | } |
| 575 | |
| 576 | // Ohne Schalter, aus demselben Grund wie die Bestandteileuebersicht: Eine |
| 577 | // abwaehlbare Erklaerung zur Barrierefreiheit waere keine Erklaerung. |
| 578 | drawBarrierefreiheit(context, options.includePlan, options.includePhases, rechenweg); |
| 579 | |
| 580 | paintHeadersAndFooters(context, now); |
| 581 | |
| 582 | return new Uint8Array(doc.output('arraybuffer')); |
| 583 | } |
| 584 | |
| 585 | // --- Seitengeruest ---------------------------------------------------------- |
| 586 | |
| 587 | /** |
| 588 | * Lesezeichen auf die zuletzt begonnene Seite. |
| 589 | * |
| 590 | * Das Dokument hatte bei 21 Seiten und 14 benannten Abschnitten |
| 591 | * kein einziges Lesezeichen (/Outlines 0). Ein Pflichtkriterium der Stufe AA |
| 592 | * ist das nicht - 2.4.5 ist fuer Nicht-Web-Dokumente in EN 301 549 gestrichen |
| 593 | * (10.2.4.5), PDF/UA fuehrt Lesezeichen als Empfehlung. Es ist aber der einzige |
| 594 | * Weg, in diesem ungetaggten Dokument ueberhaupt gezielt zu einem Abschnitt zu |
| 595 | * springen, statt Seite fuer Seite zu blaettern: Ohne Strukturbaum gibt es |
| 596 | * keine Ueberschriftennavigation, an der ein Bildschirmleseprogramm sich |
| 597 | * entlanghangeln koennte. |
| 598 | * |
| 599 | * Der Titel geht durch toWinAnsi wie jeder andere gedruckte Text - jsPDF |
| 600 | * schreibt ihn als Latin-1-Zeichenkette in das Outline-Objekt. |
| 601 | */ |
| 602 | function merkeLesezeichen(context: Context, titel: string): void { |
| 603 | context.doc.outline.add(null, toWinAnsi(titel), { |
| 604 | pageNumber: context.doc.getNumberOfPages(), |
| 605 | }); |
| 606 | } |
| 607 | |
| 608 | function newSection(context: Context, title: string): void { |
| 609 | context.doc.addPage(); |
| 610 | merkeLesezeichen(context, title); |
| 611 | context.cursor = PAGE_MARGIN + HEADER_HEIGHT; |
| 612 | context.doc.setFont('helvetica', 'bold'); |
| 613 | context.doc.setFontSize(14); |
| 614 | context.doc.setTextColor('#1b2230'); |
| 615 | context.doc.text(toWinAnsi(title), PAGE_MARGIN, context.cursor); |
| 616 | context.cursor += 20; |
| 617 | } |
| 618 | |
| 619 | function ensureSpace(context: Context, needed: number): void { |
| 620 | if (context.cursor + needed <= context.pageHeight - PAGE_MARGIN - FOOTER_HEIGHT) return; |
| 621 | context.doc.addPage(); |
| 622 | context.cursor = PAGE_MARGIN + HEADER_HEIGHT; |
| 623 | } |
| 624 | |
| 625 | /** |
| 626 | * Was auf ein leeres Blatt passt - zwischen Kopfzeile und Fusszeile. |
| 627 | * |
| 628 | * Die Rechnung steht einmal: Sie ist die Schranke, an der die beiden |
| 629 | * Matrixproben (passt der Satz ueberhaupt auf ein Blatt, passt der |
| 630 | * Tabellenkopf) und die Absatzreserve haengen. Ausgeschriebene Fassungen davon |
| 631 | * liefen frueher oder spaeter auseinander. |
| 632 | */ |
| 633 | function nutzhoeheJeBlatt(context: Context): number { |
| 634 | return context.pageHeight - (PAGE_MARGIN + HEADER_HEIGHT) - (PAGE_MARGIN + FOOTER_HEIGHT); |
| 635 | } |
| 636 | |
| 637 | function paintHeadersAndFooters(context: Context, now: Date): void { |
| 638 | const { doc, project } = context; |
| 639 | const total = doc.getNumberOfPages(); |
| 640 | |
| 641 | for (let page = 1; page <= total; page += 1) { |
| 642 | doc.setPage(page); |
| 643 | |
| 644 | doc.setDrawColor('#b3bccb'); |
| 645 | doc.setLineWidth(0.5); |
| 646 | doc.line( |
| 647 | PAGE_MARGIN, |
| 648 | PAGE_MARGIN + HEADER_HEIGHT - 16, |
| 649 | context.pageWidth - PAGE_MARGIN, |
| 650 | PAGE_MARGIN + HEADER_HEIGHT - 16, |
| 651 | ); |
| 652 | |
| 653 | /* |
| 654 | * FASSUNG 5.9.0, BEFUND 43: Beide Kopfzeilentexte gingen ohne |
| 655 | * Breitenbegrenzung an doc.text. Die Grundlagenangabe steht rechtsbuendig |
| 656 | * am Satzspiegelrand; der Projektname lief bei einer ueblichen langen |
| 657 | * Bezeichnung in sie hinein, und beide waren an der Ueberlappung unlesbar - |
| 658 | * auf JEDER Seite. Ausgerechnet die einstreifige Verkehrsfuehrung traf es |
| 659 | * zuerst: Dort ist die Angabe am laengsten, und sie ist die Korrektur C16, |
| 660 | * die das Verhaeltnis von RSA 21 und RiLSA klarstellt. |
| 661 | * |
| 662 | * Der Name wird deshalb auf den Platz links der gemessenen |
| 663 | * Grundlagenangabe gekuerzt. Das darf er hier - vollstaendig steht er auf |
| 664 | * dem Deckblatt, das ihn umbricht statt ihn zu kuerzen. |
| 665 | */ |
| 666 | const grundlage = toWinAnsi( |
| 667 | `LSA-Planer Professional · ${kopfzeilenGrundlage(project.anlagenart)}`, |
| 668 | ); |
| 669 | doc.setFont('helvetica', 'normal'); |
| 670 | doc.setFontSize(9); |
| 671 | const grundlageBreite = doc.getTextWidth(grundlage); |
| 672 | |
| 673 | doc.setFont('helvetica', 'bold'); |
| 674 | doc.setTextColor('#1b2230'); |
| 675 | doc.text( |
| 676 | gekuerztAufBreite( |
| 677 | doc, |
| 678 | toWinAnsi(project.meta.name || 'Ohne Titel'), |
| 679 | context.contentWidth - grundlageBreite - KOPFZEILEN_ABSTAND, |
| 680 | ), |
| 681 | PAGE_MARGIN, |
| 682 | PAGE_MARGIN + 12, |
| 683 | ); |
| 684 | |
| 685 | doc.setFont('helvetica', 'normal'); |
| 686 | doc.setTextColor('#5b6577'); |
| 687 | // Die zweite Zeile hat rechts keinen Nachbarn, lief aber ebenso ueber die |
| 688 | // Blattkante hinaus - sie bekommt den ganzen Satzspiegel. |
| 689 | doc.text( |
| 690 | gekuerztAufBreite( |
| 691 | doc, |
| 692 | toWinAnsi( |
| 693 | [project.meta.projectNumber, project.meta.location, project.meta.variant] |
| 694 | .filter((s) => s !== '') |
| 695 | .join(' · '), |
| 696 | ), |
| 697 | context.contentWidth, |
| 698 | ), |
| 699 | PAGE_MARGIN, |
| 700 | PAGE_MARGIN + 26, |
| 701 | ); |
| 702 | doc.text(grundlage, context.pageWidth - PAGE_MARGIN, PAGE_MARGIN + 12, { align: 'right' }); |
| 703 | |
| 704 | doc.setFontSize(8); |
| 705 | doc.line( |
| 706 | PAGE_MARGIN, |
| 707 | context.pageHeight - PAGE_MARGIN - FOOTER_HEIGHT + 8, |
| 708 | context.pageWidth - PAGE_MARGIN, |
| 709 | context.pageHeight - PAGE_MARGIN - FOOTER_HEIGHT + 8, |
| 710 | ); |
| 711 | /* |
| 712 | * "Ausgedruckt am" und nicht "Erstellt am" (Fassung 5.9.0). |
| 713 | * |
| 714 | * `now` ist der Zeitpunkt der Ausgabe. Unter "Erstellt am" stand damit in |
| 715 | * der Fusszeile jeder Seite ein anderer Zeitpunkt als unter derselben |
| 716 | * Beschriftung auf dem Deckblatt, wo das Anlagedatum des Projekts steht - |
| 717 | * und zugleich derselbe Wert, den das Deckblatt zwei Zellen weiter |
| 718 | * "Ausgedruckt am" nennt. Beides auf Blatt 1. Fuer eine Anordnung nach |
| 719 | * Paragraf 45 StVO blieb offen, welches Datum gilt; deshalb traegt jeder |
| 720 | * der beiden Zeitpunkte in der ganzen Unterlage nur noch eine Benennung. |
| 721 | * |
| 722 | * Die linke Fusszeile ging als einzige der vier Zeilen dieser Schleife ohne |
| 723 | * Breitenbegrenzung an doc.text. Sie traegt mit `project.meta.planner` |
| 724 | * freien Anwendertext ohne Laengengrenze; mit einer ueblichen |
| 725 | * Buerobezeichnung von 192 Zeichen schob sie sich unter die rechtsbuendige |
| 726 | * Seitenangabe und endete 92 pt jenseits der Blattkante - auf JEDEM Blatt. |
| 727 | * Unlesbar wurde dabei ausgerechnet die Angabe, die die Vollstaendigkeit |
| 728 | * des Schriftstuecks belegt. |
| 729 | * |
| 730 | * Derselbe Weg wie in der Kopfzeile: die Breite der Seitenangabe messen und |
| 731 | * die Bearbeiterzeile auf den verbleibenden Platz kuerzen. Auch das darf sie |
| 732 | * hier - vollstaendig steht der Bearbeiter auf dem Deckblatt und in den |
| 733 | * Dokumentangaben. |
| 734 | */ |
| 735 | const seitenangabe = toWinAnsi(`Seite ${page} von ${total}`); |
| 736 | const seitenbreite = doc.getTextWidth(seitenangabe); |
| 737 | doc.text( |
| 738 | gekuerztAufBreite( |
| 739 | doc, |
| 740 | toWinAnsi( |
| 741 | `Ausgedruckt am ${fmt.dateTime(now.toISOString())} · Bearbeiter: ${project.meta.planner || '–'}`, |
| 742 | ), |
| 743 | context.contentWidth - seitenbreite - KOPFZEILEN_ABSTAND, |
| 744 | ), |
| 745 | PAGE_MARGIN, |
| 746 | context.pageHeight - PAGE_MARGIN - 6, |
| 747 | ); |
| 748 | doc.text(seitenangabe, context.pageWidth - PAGE_MARGIN, context.pageHeight - PAGE_MARGIN - 6, { |
| 749 | align: 'right', |
| 750 | }); |
| 751 | } |
| 752 | } |
| 753 | |
| 754 | /** |
| 755 | * Was in der Kopfzeile JEDER Seite als Grundlage steht. |
| 756 | * |
| 757 | * KORREKTUR (Fassung 5.4.0, Befund C16): Hier stand auf jeder Seite pauschal |
| 758 | * "Planung nach RiLSA 2015" - auch im Ausdruck einer Baustellenampel, dessen |
| 759 | * Abschnitt "Schranken der Anlagenart" wenige Seiten weiter sagt: "Die |
| 760 | * Umlaufzeitgrenzen dieses Plans stammen NICHT aus der RiLSA". Zwei |
| 761 | * gegenlaeufige Aussagen in derselben Unterlage; wer nur die Kopfzeile liest, |
| 762 | * behaelt die falsche. |
| 763 | * |
| 764 | * Bei einer einstreifigen Verkehrsfuehrung nennt die Kopfzeile deshalb beide |
| 765 | * Regelwerke in ihrem Verhaeltnis: Die RSA 21 ist die Grundlage der |
| 766 | * Verkehrsfuehrung und verweist fuer die Berechnung auf die RiLSA (Teil A 3.2 |
| 767 | * Abs. 3) - gerechnet wird also nach RiLSA, angeordnet nach RSA. |
| 768 | * |
| 769 | * Nur eine Beschriftung des Ausdrucks, kein Kennwert - deshalb hier und nicht |
| 770 | * im Fachkern. Die Zahlen und ihre Herkunft stehen in constants.ts |
| 771 | * (ANLAGENART_GRENZEN.herkunft) und im Fundstellenverzeichnis. |
| 772 | */ |
| 773 | function kopfzeilenGrundlage(anlagenart: Project['anlagenart']): string { |
| 774 | return anlagenart === 'einstreifig' |
| 775 | ? 'Signalzeitenplan nach RiLSA 2015 (RSA 21, Teil A 3.2)' |
| 776 | : 'Signalzeitenplan nach RiLSA 2015'; |
| 777 | } |
| 778 | |
| 779 | // --- Deckblatt -------------------------------------------------------------- |
| 780 | |
| 781 | function drawCoverSheet(context: Context, now: Date): void { |
| 782 | const { doc, project, plan, report } = context; |
| 783 | context.cursor = PAGE_MARGIN + HEADER_HEIGHT + 20; |
| 784 | |
| 785 | doc.setFont('helvetica', 'bold'); |
| 786 | doc.setFontSize(22); |
| 787 | doc.setTextColor('#1b2230'); |
| 788 | doc.text(toWinAnsi('Signalzeitenplan'), PAGE_MARGIN, context.cursor); |
| 789 | context.cursor += 26; |
| 790 | |
| 791 | /* |
| 792 | * FASSUNG 5.9.0, BEFUND 43: Der Titel ging in 15 pt ohne Breitenangabe |
| 793 | * an doc.text und lief bei einer langen Bezeichnung ueber die Blattkante - |
| 794 | * dort war er abgeschnitten. Auf dem Deckblatt wird er deshalb umbrochen und |
| 795 | * NICHT gekuerzt: Die Unterlage muss die Anlage eindeutig bezeichnen. |
| 796 | * |
| 797 | * Und er wird auch in der HOEHE gemessen: Das Deckblatt traegt 23 |
| 798 | * Titelzeilen; darueber lief der Titel in den Fusssteg, ueberdruckte die |
| 799 | * Fusszeile und fiel ab rund 2.700 Zeichen ganz aus dem Blatt - derselbe |
| 800 | * stille Verlust wie bei der Bemerkung (Befund 18) und ebenso ohne |
| 801 | * Laengengrenze am Eingabefeld. |
| 802 | */ |
| 803 | doc.setFontSize(15); |
| 804 | setzeZeilenMitUmbruch( |
| 805 | context, |
| 806 | doc.splitTextToSize( |
| 807 | toWinAnsi(project.meta.name || 'Ohne Titel'), |
| 808 | context.contentWidth, |
| 809 | ) as string[], |
| 810 | TITEL_ZEILENHOEHE, |
| 811 | ); |
| 812 | // Abstand zur ersten Tabelle, von der letzten Grundlinie aus: Der Cursor |
| 813 | // steht danach bereits eine Zeilenhoehe darunter. |
| 814 | context.cursor += 26 - TITEL_ZEILENHOEHE; |
| 815 | |
| 816 | autoTable(doc, { |
| 817 | startY: context.cursor, |
| 818 | margin: TABELLENRAND, |
| 819 | theme: 'plain', |
| 820 | styles: { fontSize: 10, cellPadding: 3, textColor: '#1b2230' }, |
| 821 | columnStyles: { |
| 822 | 0: { fontStyle: 'bold', cellWidth: 150 }, |
| 823 | 2: { fontStyle: 'bold', cellWidth: 150 }, |
| 824 | }, |
| 825 | body: [ |
| 826 | /* |
| 827 | * Die Anlagenart steht an erster Stelle, weil sie sagt, WONACH dieser |
| 828 | * Plan zu beurteilen ist. |
| 829 | * |
| 830 | * Zur verkehrsbehoerdlichen Anordnung nach Paragraf 45 StVO gehoert, |
| 831 | * welche Art Anlage angeordnet wird. Ohne die Angabe bliebe offen, ob |
| 832 | * eine Umlaufzeit von 240 s eine grobe Ueberschreitung der |
| 833 | * Sicherheitsgrenze am Knotenpunkt ist oder die zwangslaeufige Folge |
| 834 | * einer langen Engstelle - dieselbe Zahl, zwei gegensaetzliche Befunde. |
| 835 | * |
| 836 | * Die Benennung kommt aus dem Regelwerksmodul und ist dieselbe wie am |
| 837 | * Bildschirm; eine eigene Formulierung fuer das Papier liefe beim |
| 838 | * naechsten Wortwechsel auseinander. |
| 839 | * |
| 840 | * Auch die Zeile daneben haengt daran: Der Name der Anlage stand hier |
| 841 | * unter der festen Beschriftung "Knotenpunkt". An einer Arbeitsstelle |
| 842 | * gibt es keinen Knotenpunkt, und eine Planunterlage darf keinen |
| 843 | * behaupten - die Beschriftung ist deshalb neutral. |
| 844 | */ |
| 845 | [ |
| 846 | 'Anlagenart', |
| 847 | ANLAGENART_LABELS[project.anlagenart], |
| 848 | 'Bezeichnung der Anlage', |
| 849 | project.intersection.name || '–', |
| 850 | ], |
| 851 | [ |
| 852 | 'Projektnummer', |
| 853 | project.meta.projectNumber || '–', |
| 854 | 'Auftraggeber', |
| 855 | project.meta.client || '–', |
| 856 | ], |
| 857 | ['Ort / Straßenzug', project.meta.location || '–', 'Bearbeiter', project.meta.planner || '–'], |
| 858 | [ |
| 859 | 'Zulässige Höchstgeschwindigkeit', |
| 860 | `${fmt.numShort(project.intersection.vZul)} km/h`, |
| 861 | 'Planungsvariante', |
| 862 | project.meta.variant || '–', |
| 863 | ], |
| 864 | /* |
| 865 | * "Angelegt am" wie in der Ansicht "Projekt" (Fassung 5.9.0): Hier stand |
| 866 | * "Erstellt am" fuer das Anlagedatum, waehrend die Fusszeile derselben |
| 867 | * Seite "Erstellt am" fuer den Ausgabezeitpunkt fuehrte. Eine |
| 868 | * Beschriftung fuer zwei Zeitpunkte in einer Unterlage. |
| 869 | */ |
| 870 | ['Signalprogramm', project.program.name, 'Angelegt am', fmt.dateTime(project.meta.createdAt)], |
| 871 | [ |
| 872 | 'Zuletzt geändert', |
| 873 | fmt.dateTime(project.meta.modifiedAt), |
| 874 | 'Ausgedruckt am', |
| 875 | fmt.dateTime(now.toISOString()), |
| 876 | ], |
| 877 | /* |
| 878 | * DIE HERKUNFT DER UNTERLAGE (neu in 5.26.0). Ohne diese Zeile war zwei |
| 879 | * Ausdrucken desselben Projekts nicht anzusehen, welcher Rechenstand sie |
| 880 | * erzeugt hat - und zwischen 5.21.0 und 5.25.0 liegen der Deckel an der |
| 881 | * zulaessigen Hoechstgeschwindigkeit und die Verteilung des |
| 882 | * Freigabezeitrests, beide mit anderen Zahlen im Plan. Der Schemastand |
| 883 | * daneben sagt, aus welchem Dateiformat die Daten stammen; die |
| 884 | * mitgelieferte Beispieldatei traegt bewusst einen aelteren. |
| 885 | */ |
| 886 | ['Programmfassung', FASSUNGSANGABE, 'Schemastand der Datei', project.schemaVersion], |
| 887 | ].map((row) => row.map((cell) => toWinAnsi(String(cell)))), |
| 888 | }); |
| 889 | |
| 890 | context.cursor = lastTableBottom(doc) + 20; |
| 891 | |
| 892 | autoTable(doc, { |
| 893 | startY: context.cursor, |
| 894 | margin: TABELLENRAND, |
| 895 | theme: 'grid', |
| 896 | head: [['Kennwert', 'Wert', 'Kennwert', 'Wert'].map(toWinAnsi)], |
| 897 | headStyles: { fillColor: '#eef1f6', textColor: '#1b2230', fontStyle: 'bold' }, |
| 898 | styles: { fontSize: 10, cellPadding: 4 }, |
| 899 | body: [ |
| 900 | ...[ |
| 901 | [ |
| 902 | 'Umlaufzeit', |
| 903 | // Mit Vermerk, wenn die Zahl kein Bemessungsergebnis ist (Befund C11). |
| 904 | umlaufzeitText(plan), |
| 905 | 'Verfahren', |
| 906 | methodLabel(project.program.method), |
| 907 | ], |
| 908 | [ |
| 909 | 'Anzahl Phasen', |
| 910 | String(plan.phases.length), |
| 911 | 'Summe Übergangszeiten', |
| 912 | fmt.seconds(plan.transitionTime), |
| 913 | ], |
| 914 | [ |
| 915 | 'Anzahl Signalgruppen', |
| 916 | String(project.signalGroups.length), |
| 917 | 'Verlustzeit je Umlauf', |
| 918 | fmt.seconds(plan.lostTime), |
| 919 | ], |
| 920 | ].map((row) => row.map((cell) => toWinAnsi(String(cell)))), |
| 921 | /* |
| 922 | * NEU (Fassung 5.5.0, Frage E2): Der Ueberfahrzeit-Ansatz |
| 923 | * geht in jede nach Abschnitt 2 gerechnete Zwischenzeit eines |
| 924 | * Kfz-Raeumstroms ein und steht deshalb neben der Umlaufzeit und dem |
| 925 | * Bemessungsverfahren - auf dem Deckblatt, das jeder Leser sieht. |
| 926 | * |
| 927 | * WARUM HIER UND NICHT NUR BEI DEN ZWISCHENZEITEN: Die |
| 928 | * Zwischenzeitenmatrix ist abwaehlbar. Ein Ausdruck ohne sie enthielte |
| 929 | * sonst Freigabe- und Uebergangszeiten, die auf dem Ansatz beruhen, ohne |
| 930 | * ihn zu nennen. Die Begruendung samt Fehlerrichtung steht im Abschnitt |
| 931 | * "Zwischenzeitenmatrix" (ueberfahrzeitAnsatzSatz); hier steht nur die |
| 932 | * Benennung, und zwar dieselbe wie am Bildschirm und im Pruefbericht. |
| 933 | * |
| 934 | * "JEDE" HIESS HIER EINMAL WIRKLICH JEDE (bis 5.42.1), und die Zeile |
| 935 | * stand unbedingt: An einer einstreifigen Verkehrsfuehrung rechnet der |
| 936 | * Fachkern nach RiLSA 2015, Abschnitt 5.2.2 mit einer festen |
| 937 | * Ueberfahrzeit, und wo jede Kfz-Zwischenzeit von Hand vorgegeben ist, |
| 938 | * rechnet er ueberhaupt nicht. Das Deckblatt nannte gleichwohl einen |
| 939 | * Rechenansatz und verwies fuer die Begruendung auf einen Absatz, den |
| 940 | * dieselbe Unterlage dort nicht mehr druckt. Die Zeile bleibt - eine |
| 941 | * Einstellung, die nicht wirkt, gehoert genannt und nicht verschwiegen -, |
| 942 | * aber mit dem Wirkungsvermerk daneben, wie die Vorgabenverwaltung es |
| 943 | * fuer jeden wirkungslosen Kennwert haelt (`wirkungslosigkeit` in |
| 944 | * settings.ts). Die Antwort kommt aus dem Fachkern, damit Deckblatt, |
| 945 | * Matrix, Abweichungstafel und Pruefbericht nicht viererlei sagen. |
| 946 | * |
| 947 | * Die Beschriftung fuellt drei Spalten, weil sie ein Satz ist und kein |
| 948 | * Zahlenwert - in der schmalen Wertspalte stuende sie sechszeilig. |
| 949 | */ |
| 950 | [ |
| 951 | { content: toWinAnsi('Überfahrzeit Kfz (Rechenansatz)'), styles: { fontStyle: 'bold' } }, |
| 952 | { |
| 953 | content: toWinAnsi( |
| 954 | ((): string => { |
| 955 | const benennung = UEBERFAHRZEIT_ANSATZ_LABELS[project.settings.ueberfahrzeitAnsatz]; |
| 956 | const ohneWirkung = ueberfahrzeitAnsatzOhneWirkung(project, plan); |
| 957 | return ohneWirkung === null ? benennung : `${benennung} – ${ohneWirkung.vermerk}`; |
| 958 | })(), |
| 959 | ), |
| 960 | colSpan: 3, |
| 961 | }, |
| 962 | ], |
| 963 | ] as RowInput[], |
| 964 | }); |
| 965 | |
| 966 | context.cursor = lastTableBottom(doc) + 24; |
| 967 | |
| 968 | /* |
| 969 | * Der Kasten wurde unmittelbar an `context.cursor` gezeichnet - ohne |
| 970 | * Umbruchpruefung. Bei einer langen Projektbezeichnung (das Feld hat keine |
| 971 | * Laengengrenze, und auf dem Deckblatt wird sie bewusst umbrochen statt |
| 972 | * gekuerzt) ruecken die beiden Tabellen darueber nach unten; ab rund |
| 973 | * 400 Zeichen lag die farbige Flaeche im Fusssteg, ab rund 480 der |
| 974 | * Vorbehaltssatz, und ab rund 780 fiel er ganz aus dem Blatt - ersatzlos. |
| 975 | * Verloren ging damit die einzige Stelle des Dokuments, an der |
| 976 | * `report.errorCount` steht, samt dem Vorbehalt zu Paragraf 45 StVO. |
| 977 | * |
| 978 | * Der Kasten ist eine Einheit aus Flaeche, Ueberschrift, Ergebniszeile und |
| 979 | * Vorbehalt; er rutscht deshalb geschlossen auf das Folgeblatt. Gemessen wird |
| 980 | * seine volle Hoehe samt demselben Abstand, der ihn oben von der Tabelle |
| 981 | * trennt - Deckblatttitel (Befund 43) und Bemerkung (Befund 18) sind aus |
| 982 | * demselben Grund gemessen gesetzt, nur der Block dazwischen war es nicht. |
| 983 | */ |
| 984 | ensureSpace(context, ERGEBNISKASTEN_HOEHE + 24); |
| 985 | |
| 986 | // Pruefergebnis - sachlich, ohne Empfehlung. |
| 987 | const resultText = |
| 988 | report.errorCount > 0 |
| 989 | ? `Die Prüfung ergab ${report.errorCount} Fehler und ${report.warningCount} Warnungen. ` + |
| 990 | 'Der Signalzeitenplan ist in dieser Form nicht ausführbar.' |
| 991 | : report.warningCount > 0 |
| 992 | ? `Die Prüfung ergab keine Fehler und ${report.warningCount} Warnungen. ` + |
| 993 | 'Die Warnungen sind fachlich zu würdigen.' |
| 994 | : 'Die Prüfung ergab keine Beanstandungen.'; |
| 995 | |
| 996 | doc.setFillColor( |
| 997 | report.errorCount > 0 ? '#fdecea' : report.warningCount > 0 ? '#fff4e0' : '#e8f5ec', |
| 998 | ); |
| 999 | doc.rect(PAGE_MARGIN, context.cursor, context.contentWidth, ERGEBNISKASTEN_HOEHE, 'F'); |
| 1000 | doc.setDrawColor( |
| 1001 | report.errorCount > 0 ? '#b3261e' : report.warningCount > 0 ? '#8a5a00' : '#1f6b3a', |
| 1002 | ); |
| 1003 | doc.setLineWidth(2); |
| 1004 | doc.line(PAGE_MARGIN, context.cursor, PAGE_MARGIN, context.cursor + ERGEBNISKASTEN_HOEHE); |
| 1005 | |
| 1006 | doc.setFont('helvetica', 'bold'); |
| 1007 | doc.setFontSize(11); |
| 1008 | doc.setTextColor('#1b2230'); |
| 1009 | doc.text(toWinAnsi('Ergebnis der Prüfung'), PAGE_MARGIN + 10, context.cursor + 18); |
| 1010 | doc.setFont('helvetica', 'normal'); |
| 1011 | doc.setFontSize(9.5); |
| 1012 | doc.text(toWinAnsi(resultText), PAGE_MARGIN + 10, context.cursor + 33, { |
| 1013 | maxWidth: context.contentWidth - 20, |
| 1014 | }); |
| 1015 | doc.setFontSize(8); |
| 1016 | doc.setTextColor('#5b6577'); |
| 1017 | doc.text( |
| 1018 | toWinAnsi( |
| 1019 | 'Die rechnerische Prüfung ersetzt nicht die fachliche Verantwortung des Planers und keine ' + |
| 1020 | 'verkehrsbehördliche Anordnung nach Paragraf 45 StVO.', |
| 1021 | ), |
| 1022 | PAGE_MARGIN + 10, |
| 1023 | context.cursor + 47, |
| 1024 | { maxWidth: context.contentWidth - 20 }, |
| 1025 | ); |
| 1026 | |
| 1027 | if (project.meta.description.trim() !== '') { |
| 1028 | context.cursor += ERGEBNISKASTEN_HOEHE + ERGEBNISKASTEN_NACHLAUF; |
| 1029 | // Ueberschrift und die ersten Zeilen bleiben beisammen; die Bemerkung |
| 1030 | // selbst bringt ihren eigenen Umbruch mit (siehe writeBemerkung). |
| 1031 | ensureSpace(context, 60); |
| 1032 | doc.setFont('helvetica', 'bold'); |
| 1033 | doc.setFontSize(10); |
| 1034 | doc.setTextColor('#1b2230'); |
| 1035 | doc.text(toWinAnsi('Bemerkungen'), PAGE_MARGIN, context.cursor); |
| 1036 | context.cursor += 14; |
| 1037 | writeBemerkung(context, project.meta.description); |
| 1038 | } |
| 1039 | } |
| 1040 | |
| 1041 | // --- Bestandteile der Anordnungsunterlage ----------------------------------- |
| 1042 | |
| 1043 | /** |
| 1044 | * Die Zeile "Rechenweg der Zwischenzeiten und der Wartezeit" der Uebersicht. |
| 1045 | * |
| 1046 | * Sie nennt nur Abschnitte, die im Dokument auch wirklich stehen. Vor der |
| 1047 | * Fassung 5.4.0 hing sie allein am Schalter `includeCalculationDetails` und |
| 1048 | * druckte "ja - Abschnitte ... und ..." auch dann, wenn Zwischenzeitenmatrix |
| 1049 | * und Leistungsfaehigkeit abgewaehlt waren und keiner der beiden Rechenwege im |
| 1050 | * Dokument vorkam. Der Abschnitt, der die Luecken der Unterlage ausweisen soll, |
| 1051 | * gab damit selbst eine falsche Auskunft. |
| 1052 | */ |
| 1053 | function rechenwegZeile(stand: RechenwegStand): string { |
| 1054 | const zwischenzeiten = zwischenzeitRechenweg(stand); |
| 1055 | const wartezeit = wartezeitRechenweg(stand); |
| 1056 | |
| 1057 | if (zwischenzeiten && wartezeit) { |
| 1058 | return 'ja – Abschnitte „Zwischenzeiten – Rechenweg" und „Leistungsfähigkeit – Rechenweg Wartezeit"'; |
| 1059 | } |
| 1060 | if (zwischenzeiten) { |
| 1061 | return ( |
| 1062 | 'teilweise – Abschnitt „Zwischenzeiten – Rechenweg"; der Rechenweg der Wartezeit entfällt ' + |
| 1063 | 'mit dem abgewählten Abschnitt „Leistungsfähigkeit"' |
| 1064 | ); |
| 1065 | } |
| 1066 | if (wartezeit) { |
| 1067 | return ( |
| 1068 | 'teilweise – Abschnitt „Leistungsfähigkeit – Rechenweg Wartezeit"; der Rechenweg der ' + |
| 1069 | 'Zwischenzeiten entfällt mit der abgewählten Zwischenzeitenmatrix' |
| 1070 | ); |
| 1071 | } |
| 1072 | if (stand.angefordert) { |
| 1073 | return ( |
| 1074 | 'entfällt – angefordert, aber beide Abschnitte, in denen er steht (Zwischenzeitenmatrix, ' + |
| 1075 | 'Leistungsfähigkeit), sind abgewählt' |
| 1076 | ); |
| 1077 | } |
| 1078 | if (stand.matrix) return 'abgewählt – die Werte der Matrix sind ohne ihn nicht nachrechenbar'; |
| 1079 | if (stand.leistung) { |
| 1080 | return 'abgewählt – die Werte der Leistungsfähigkeit sind ohne ihn nicht nachrechenbar'; |
| 1081 | } |
| 1082 | return 'abgewählt – die Abschnitte, in denen er gestanden hätte, sind ebenfalls abgewählt'; |
| 1083 | } |
| 1084 | |
| 1085 | /** |
| 1086 | * Was diese Unterlage enthaelt - und was sie NICHT enthaelt. |
| 1087 | * |
| 1088 | * NEU (Befund C14). Der Ausdruck druckte, was angehakt war, und schwieg ueber |
| 1089 | * alles Uebrige. Das ist der gefaehrliche Fall: Eine Unterlage, die jeden |
| 1090 | * Abschnitt fuellt, den sie kennt, sieht vollstaendig aus. Fehlte der |
| 1091 | * Zwischenzeitennachweis, fehlten die Einsatzzeiten, fehlte der bemasste |
| 1092 | * Signallageplan - der Unterlage sah man das nicht an. Erst die Behoerde |
| 1093 | * stellte fest, dass die Anordnung so nicht ergehen kann. |
| 1094 | * |
| 1095 | * WARUM DER SIGNALLAGEPLAN AUSDRUECKLICH FEHLT: Dieses Programm zeichnet |
| 1096 | * Fahrlinien und rechnet daraus Raeum- und Einfahrwege. Wo die Signalgeber |
| 1097 | * stehen, welche Bauform sie haben und wie sie bemasst sind, kann es nicht |
| 1098 | * sagen - und ein Ausdruck, der einen "Lageplan" zeigt, laedt dazu ein, ihn |
| 1099 | * dafuer zu halten. Die Zeile sagt deshalb im Klartext, dass dieser Plan |
| 1100 | * gesondert beizubringen ist. |
| 1101 | * |
| 1102 | * Der Abschnitt haengt an keinem Schalter: Ein abwaehlbarer Lueckenausweis |
| 1103 | * waere kein Lueckenausweis. |
| 1104 | */ |
| 1105 | function drawAnordnungsunterlage( |
| 1106 | context: Context, |
| 1107 | options: PdfOptions, |
| 1108 | rechenweg: RechenwegStand, |
| 1109 | ): void { |
| 1110 | const { doc, project } = context; |
| 1111 | const einstreifig = project.anlagenart === 'einstreifig'; |
| 1112 | |
| 1113 | newSection(context, 'Bestandteile der Anordnungsunterlage'); |
| 1114 | |
| 1115 | writeParagraph( |
| 1116 | context, |
| 1117 | 'Diese Übersicht sagt, welche Bestandteile die vorliegende Ausgabe enthält und welche nicht. ' + |
| 1118 | 'Abgewählte Abschnitte sind als solche benannt - eine fehlende Angabe ist kein Nachweis, ' + |
| 1119 | 'dass es sie nicht gibt, sondern eine Lücke der Unterlage.', |
| 1120 | ); |
| 1121 | |
| 1122 | /** "ja" mit dem Abschnitt, in dem es steht - sonst der Grund des Fehlens. */ |
| 1123 | const abschnitt = (enthalten: boolean, name: string): string => |
| 1124 | enthalten ? `ja – Abschnitt „${name}"` : 'abgewählt – in dieser Ausgabe nicht enthalten'; |
| 1125 | |
| 1126 | /** Eine Angabe der Projektdaten oder der ausdrueckliche Ausweis ihres Fehlens. */ |
| 1127 | const angabe = (wert: string | undefined): string => { |
| 1128 | const text = String(wert ?? '').trim(); |
| 1129 | return text === '' ? 'nicht angegeben' : text; |
| 1130 | }; |
| 1131 | |
| 1132 | /** Ob eine wahlfreie Angabe der Projektdaten ueberhaupt gefuellt ist. */ |
| 1133 | const angegeben = (wert: string | undefined): boolean => String(wert ?? '').trim() !== ''; |
| 1134 | |
| 1135 | /* |
| 1136 | * Der Verweis auf eine Unterlage, die dieses Programm nicht erzeugt. |
| 1137 | * |
| 1138 | * DIE ZEILE BEGINNT IN BEIDEN FAELLEN MIT "NICHT Bestandteil". Das ist der |
| 1139 | * Kern der Uebersicht seit Befund C14: Sie soll die Luecke ausweisen und |
| 1140 | * nicht verkleinern. Der eingetragene Verweis sagt danach, WORAUF sich diese |
| 1141 | * Unterlage bezieht - aus "fehlt" wird "fehlt hier, liegt als X bei" und |
| 1142 | * nicht "enthalten". Stuende der Verweis vorn, laese ein Pruefer beim |
| 1143 | * Ueberfliegen eine Kennung und hielte den Plan fuer dabei. |
| 1144 | */ |
| 1145 | /* |
| 1146 | * DIE ZEILE ZUM SIGNALLAGEPLAN IST SEIT 5.42.0 DREIWERTIG. |
| 1147 | * |
| 1148 | * Sie war die unangenehme Auskunft der Uebersicht und die Behebung des |
| 1149 | * Befundes C14: "NICHT Bestandteil - gesondert beizubringen". Mit erfassten |
| 1150 | * Signalgebern waere das nicht mehr die ganze Wahrheit, denn ein Blatt mit |
| 1151 | * Standort, Blickrichtung und Kennung liegt dann bei. |
| 1152 | * |
| 1153 | * ES WIRD TROTZDEM KEIN "JA". Was fehlt, ist genau aufgezaehlt - Bauform, |
| 1154 | * Hoehe, Blendenmass, Sichtpruefung, Restfahrbahnbreite -, und der Satz |
| 1155 | * beginnt mit dem, was fehlt, nicht mit dem, was da ist. Ein Pruefer, der |
| 1156 | * die Zeile ueberfliegt, soll nicht "enthalten" lesen: Ein Signallageplan |
| 1157 | * ohne diese fuenf Angaben ist keiner, und ein halber, der sich fuer einen |
| 1158 | * ganzen ausgibt, waere schlechter als das ehrliche Fehlen. |
| 1159 | */ |
| 1160 | const signallageplanZeile = (p: Project): string => { |
| 1161 | const anzahl = p.lageplan.signalgeber.length; |
| 1162 | if (anzahl === 0) return 'NICHT Bestandteil dieser Unterlage – gesondert beizubringen'; |
| 1163 | return ( |
| 1164 | `TEILWEISE – ${String(anzahl)} Signalgeberstandort(e) mit Blickrichtung und Kennung sind ` + |
| 1165 | 'als eigenes Lageplanblatt enthalten. Nicht enthalten und gesondert beizubringen bleiben: ' + |
| 1166 | 'Bauform, Höhe und Blendenmaß der Signalgeber, die Sichtprüfung sowie der Nachweis der ' + |
| 1167 | 'verbleibenden Fahrbahnbreite.' |
| 1168 | ); |
| 1169 | }; |
| 1170 | |
| 1171 | const fremdunterlage = (verweis: string | undefined): string => |
| 1172 | angegeben(verweis) |
| 1173 | ? `NICHT Bestandteil dieser Unterlage – gesondert beizubringen; bezieht sich auf: ${String(verweis).trim()}` |
| 1174 | : 'NICHT Bestandteil dieser Unterlage – gesondert beizubringen'; |
| 1175 | |
| 1176 | autoTable(doc, { |
| 1177 | startY: context.cursor, |
| 1178 | margin: TABELLENRAND, |
| 1179 | theme: 'grid', |
| 1180 | styles: { fontSize: 9, cellPadding: 3, valign: 'top' }, |
| 1181 | headStyles: { fillColor: '#eef1f6', textColor: '#1b2230', fontStyle: 'bold' }, |
| 1182 | columnStyles: { 0: { cellWidth: 260, fontStyle: 'bold' } }, |
| 1183 | head: [['Bestandteil', 'enthalten?'].map(toWinAnsi)], |
| 1184 | body: [ |
| 1185 | [ |
| 1186 | 'Signalzeitenplan (Grafik und Signalzeiten)', |
| 1187 | abschnitt(options.includePlan, 'Signalzeitenplan'), |
| 1188 | ], |
| 1189 | ['Signalgruppen', abschnitt(options.includeSignalGroups, 'Signalgruppen')], |
| 1190 | ['Zwischenzeitenmatrix', abschnitt(options.includeIntergreenMatrix, 'Zwischenzeitenmatrix')], |
| 1191 | // Befund C15: Der Zwischenzeitennachweis ist der Abschnitt, den die |
| 1192 | // Behoerdenpraxis verlangt; seine Abwahl steht deshalb mit ihrer Folge da |
| 1193 | // und nicht als blosser Vermerk "abgewaehlt". |
| 1194 | ['Rechenweg der Zwischenzeiten und der Wartezeit', rechenwegZeile(rechenweg)], |
| 1195 | [ |
| 1196 | 'Phasen und Phasenübergänge', |
| 1197 | abschnitt(options.includePhases, 'Phasen und Phasenübergänge'), |
| 1198 | ], |
| 1199 | [ |
| 1200 | 'Begründung der Umlaufzeit (Vergleich der vier Rechenansätze)', |
| 1201 | abschnitt(options.includeCycleComparison, 'Umlaufzeit – Vergleich der Rechenansätze'), |
| 1202 | ], |
| 1203 | ['Leistungsfähigkeit', abschnitt(options.includePerformance, 'Leistungsfähigkeit')], |
| 1204 | ['Prüfbericht', abschnitt(options.includeReport, 'Prüfbericht')], |
| 1205 | [ |
| 1206 | 'Nachweis der Kennwerte (Rechtsgrundlagen, Schranken der Anlagenart, Fundstellenverzeichnis)', |
| 1207 | abschnitt(options.includeSources, 'Grundlagen der Kennwerte'), |
| 1208 | ], |
| 1209 | ['Nachweis der Vermessungsgrundlage', 'ja – Abschnitt „Planungsgrundlage" (nicht abwählbar)'], |
| 1210 | // DIE WICHTIGSTE ZEILE DIESER UEBERSICHT, wo ein Projekt mehrere |
| 1211 | // Tagesprogramme fuehrt: Was hier gedruckt ist, gilt EINEM Programm - und |
| 1212 | // wer das nicht liest, haelt die Unterlage fuer die ganze Anordnung. |
| 1213 | ['Tagesprogramme dieser Anlage', programmzeile(project)], |
| 1214 | // Der Abschnitt entsteht nur mit erfasstem Strassenzug, und die Zeile |
| 1215 | // sagt beides. |
| 1216 | [ |
| 1217 | 'Koordinierung im Straßenzug (Grüne Welle)', |
| 1218 | project.koordinierung === null |
| 1219 | ? 'nein – für diesen Knotenpunkt ist kein Straßenzug erfasst' |
| 1220 | : 'ja – Abschnitt „Koordinierung" (nicht abwählbar)', |
| 1221 | ], |
| 1222 | // Die Zeile steht neben der des bemassten Signallageplans und nicht an |
| 1223 | // ihrer Stelle: Das Blatt zeigt die Fahrlinien, nicht die Signalgeber. |
| 1224 | [ |
| 1225 | 'Lageplan der Fahrlinien (maßstäblich, ohne Signalgeber)', |
| 1226 | planblatt(project.lageplan, 400, 300) === null |
| 1227 | ? 'nein – kein maßstäblich auswertbarer Lageplan im Projekt' |
| 1228 | : 'ja – Abschnitt „Planungsgrundlage" (nicht abwählbar)', |
| 1229 | ], |
| 1230 | // Die Erklaerung zur Barrierefreiheit steht am Ende der Unterlage; wer |
| 1231 | // hier nachsieht, was drin ist, soll sie finden. |
| 1232 | [ |
| 1233 | 'Erklärung zur Barrierefreiheit dieser Datei', |
| 1234 | 'ja – Abschnitt „Barrierefreiheit dieser Datei" (nicht abwählbar)', |
| 1235 | ], |
| 1236 | ['Einsatzzeiten der Anordnung', angabe(project.meta.einsatzzeiten)], |
| 1237 | [ |
| 1238 | 'Betriebsverantwortlicher (Name, Firma, Erreichbarkeit)', |
| 1239 | angabe(project.meta.betriebsverantwortlicher), |
| 1240 | ], |
| 1241 | ['Bemaßter Signallageplan mit Signalgeberstandorten', signallageplanZeile(project)], |
| 1242 | ['Verkehrszeichenplan', fremdunterlage(project.meta.verkehrszeichenplan)], |
| 1243 | ...(angegeben(project.meta.regelplan) |
| 1244 | ? [['Regelplan der RSA 21 (Angabe des Planers)', String(project.meta.regelplan).trim()]] |
| 1245 | : []), |
| 1246 | ...(angegeben(project.meta.zustimmungBaulasttraeger) |
| 1247 | ? [ |
| 1248 | [ |
| 1249 | 'Zustimmung des Straßenbaulastträgers', |
| 1250 | String(project.meta.zustimmungBaulasttraeger).trim(), |
| 1251 | ], |
| 1252 | ] |
| 1253 | : []), |
| 1254 | ].map((row) => row.map((cell) => toWinAnsi(String(cell)))), |
| 1255 | }); |
| 1256 | |
| 1257 | context.cursor = lastTableBottom(doc) + 14; |
| 1258 | |
| 1259 | /* |
| 1260 | * Der Abwahlvermerk steht sonst an der Stelle des Rechenwegs. Sind beide |
| 1261 | * Stellen abgewaehlt, gibt es sie nicht - dann steht er hier, denn eine |
| 1262 | * Luecke, die nirgends vermerkt ist, sieht wie eine vollstaendige Unterlage |
| 1263 | * aus. |
| 1264 | */ |
| 1265 | if (!rechenweg.angefordert && !rechenweg.matrix && !rechenweg.leistung) { |
| 1266 | writeParagraph(context, rechenwegAbgewaehlt(rechenweg)); |
| 1267 | } |
| 1268 | |
| 1269 | writeParagraph( |
| 1270 | context, |
| 1271 | project.lageplan.signalgeber.length === 0 |
| 1272 | ? 'Ein bemaßter Signallageplan mit den Standorten der Signalgeber ist nicht Bestandteil ' + |
| 1273 | 'dieser Unterlage. Dieses Programm zeichnet Fahrlinien und leitet daraus Räum- und ' + |
| 1274 | 'Einfahrwege ab; wo die Signalgeber stehen, welche Bauform sie haben und wie sie ' + |
| 1275 | 'bemaßt sind, geht daraus nicht hervor. Derselbe Vorbehalt gilt für den ' + |
| 1276 | 'Verkehrszeichenplan. Beide Pläne sind gesondert beizubringen.' |
| 1277 | : 'Diese Unterlage führt ein Lageplanblatt mit den erfassten Signalgeberstandorten: ' + |
| 1278 | 'Standort, Blickrichtung und Kennung, maßstäblich abgegriffen aus derselben Zeichnung ' + |
| 1279 | 'wie die Räum- und Einfahrwege. Ein vollständiger bemaßter Signallageplan ist es ' + |
| 1280 | 'nicht – Bauform, Höhe und Blendenmaß der Signalgeber, die Sichtprüfung und der ' + |
| 1281 | 'Nachweis der verbleibenden Fahrbahnbreite gehen daraus nicht hervor und sind ' + |
| 1282 | 'gesondert beizubringen. Für den Verkehrszeichenplan gilt der Vorbehalt unverändert.', |
| 1283 | ); |
| 1284 | |
| 1285 | writeParagraph( |
| 1286 | context, |
| 1287 | einstreifig |
| 1288 | ? 'Grundlage: RSA 21 Teil A 1.4/1.5 – Signallage- und Signalzeitenplan sind Teil des ' + |
| 1289 | 'Verkehrszeichenplans, den der Unternehmer der Straßenverkehrsbehörde vorlegt; die ' + |
| 1290 | 'verkehrstechnischen Unterlagen nach den ZTV transportable Lichtsignalanlagen 2023 ' + |
| 1291 | '(ARS Nr. 07/2024) nennen Einsatzzeiten und Betriebsverantwortlichen ausdrücklich. ' + |
| 1292 | 'Abschnittsnummern und Wortlaut sind an der eigenen Ausgabe zu prüfen; der Prüfbericht ' + |
| 1293 | 'führt fehlende Angaben als Warnung.' |
| 1294 | : 'Einsatzzeiten und Betriebsverantwortlicher sind üblicher Bestandteil einer ' + |
| 1295 | 'verkehrsbehördlichen Anordnung nach § 45 StVO; ein bezifferter Regelwerkswert steht ' + |
| 1296 | 'dahinter nicht. Der Prüfbericht führt sie deshalb als Hinweis, nicht als Warnung.', |
| 1297 | ); |
| 1298 | } |
| 1299 | |
| 1300 | // --- Barrierefreiheit dieser Datei ------------------------------------------ |
| 1301 | |
| 1302 | /** |
| 1303 | * Erklaerung zur Barrierefreiheit der erzeugten Datei. |
| 1304 | * |
| 1305 | * NEU (Fassung 5.7.0). Der Massstab war EN 301 549 V3.2.1 Kapitel 10 mit WCAG |
| 1306 | * 2.1 Stufe AA. Ergebnis fuer diese Datei: nicht konform. Sie ist ungetaggt - |
| 1307 | * im erzeugten Dokument wurden /StructTreeRoot, /MarkInfo, /Alt, /ActualText |
| 1308 | * und /Artifact ausgezaehlt, alle null -, und mit der eingesetzten Bibliothek |
| 1309 | * (jsPDF 2.5.2) ist das nicht zu beheben. Damit fallen 1.3.1, 1.3.2 und 1.1.1 |
| 1310 | * der Stufe A fuer die Unterlage aus. |
| 1311 | * |
| 1312 | * WARUM DAS IM DOKUMENT SELBST STEHT: Aus demselben Grund, aus dem die |
| 1313 | * Uebersicht "Bestandteile der Anordnungsunterlage" die fehlenden Bestandteile |
| 1314 | * benennt, statt sie wegzulassen. Eine Datei, die aussieht wie jede andere |
| 1315 | * PDF-Datei, wird von einer oeffentlichen Stelle veroeffentlicht wie jede |
| 1316 | * andere - und die Stelle haftet dafuer, nicht der Hersteller. Sie kann ihre |
| 1317 | * Erklaerung nach Paragraf 12b BGG nur abgeben, wenn ihr jemand sagt, wie es |
| 1318 | * um die Datei steht. Dieser Abschnitt sagt es. |
| 1319 | * |
| 1320 | * WAS ER NICHT TUT: Er behauptet keine Konformitaet, keine Teilkonformitaet und |
| 1321 | * keine "weitgehende Barrierefreiheit". Die Zusagen, die er macht - Titel, |
| 1322 | * Sprache, Lesezeichen, auslesbarer Text, Signalzeitentabelle als Textfassung |
| 1323 | * der Zeichnung -, sind einzeln nachpruefbar und in tests/export/ |
| 1324 | * bfUnterlagen.test.ts an der erzeugten Datei geprueft. |
| 1325 | * |
| 1326 | * Ohne Schalter: Ein abwaehlbarer Lueckenausweis waere kein Lueckenausweis. |
| 1327 | */ |
| 1328 | function drawBarrierefreiheit( |
| 1329 | context: Context, |
| 1330 | mitPlanabschnitt: boolean, |
| 1331 | mitPhasenabschnitt: boolean, |
| 1332 | rechenweg: RechenwegStand, |
| 1333 | ): void { |
| 1334 | const { doc } = context; |
| 1335 | |
| 1336 | newSection(context, 'Barrierefreiheit dieser Datei'); |
| 1337 | |
| 1338 | writeParagraph( |
| 1339 | context, |
| 1340 | 'Dieser Abschnitt sagt, wie es um die Barrierefreiheit dieser Datei steht - nicht um die der ' + |
| 1341 | 'geplanten Anlage. Er ist eine Angabe des Herstellers zum Stand der Datei und keine ' + |
| 1342 | 'Rechtsberatung. Maßstab ist EN 301 549 V3.2.1 Kapitel 10 mit den Erfolgskriterien der ' + |
| 1343 | 'WCAG 2.1 Stufe AA; Stand der Prüfung: August 2026.', |
| 1344 | ); |
| 1345 | |
| 1346 | writeParagraph( |
| 1347 | context, |
| 1348 | 'Ergebnis: Diese Datei ist nicht getaggt. Sie erfüllt PDF/UA (DIN ISO 14289-1) nicht, und die ' + |
| 1349 | 'Erfolgskriterien 1.3.1 Info und Beziehungen, 1.3.2 Bedeutungstragende Reihenfolge und ' + |
| 1350 | '1.1.1 Nicht-Text-Inhalte der Stufe A sind für sie nicht erfüllt.', |
| 1351 | ); |
| 1352 | |
| 1353 | autoTable(doc, { |
| 1354 | startY: context.cursor, |
| 1355 | margin: TABELLENRAND, |
| 1356 | theme: 'grid', |
| 1357 | styles: { fontSize: 9, cellPadding: 3, valign: 'top' }, |
| 1358 | headStyles: { fillColor: '#eef1f6', textColor: '#1b2230', fontStyle: 'bold' }, |
| 1359 | columnStyles: { 0: { cellWidth: 260, fontStyle: 'bold' } }, |
| 1360 | head: [['Merkmal', 'Stand in dieser Datei'].map(toWinAnsi)], |
| 1361 | body: [ |
| 1362 | [ |
| 1363 | 'Auszeichnung der Struktur (Tags, Strukturbaum)', |
| 1364 | 'nicht vorhanden – die Datei ist ungetaggt', |
| 1365 | ], |
| 1366 | [ |
| 1367 | 'Überschriften, Absätze, Kopfzellen der Tabellen', |
| 1368 | 'nur optisch gesetzt, nicht programmatisch ausgezeichnet', |
| 1369 | ], |
| 1370 | ['Festgelegte Lesereihenfolge', 'nicht festgelegt'], |
| 1371 | [ |
| 1372 | 'Kopf- und Fußzeile als Seitenrahmen gekennzeichnet', |
| 1373 | 'nein – sie stehen im Lesefluss und werden mitgelesen', |
| 1374 | ], |
| 1375 | [ |
| 1376 | 'Alternativtext der Zeichnung', |
| 1377 | /* |
| 1378 | * FASSUNG 5.9.0: Beide Zweige nannten die Tabelle |
| 1379 | * "Signalzeiten je Signalgruppe" als Textfassung - auch dann, wenn der |
| 1380 | * Abschnitt "Signalzeitenplan" abgewaehlt war und die Datei weder |
| 1381 | * Zeichnung noch Tabelle enthaelt. |
| 1382 | */ |
| 1383 | !mitPlanabschnitt |
| 1384 | ? 'entfällt – der Abschnitt „Signalzeitenplan" wurde für diese Ausgabe abgewählt; ' + |
| 1385 | 'diese Datei enthält keine Zeichnung' |
| 1386 | : mitPhasenabschnitt |
| 1387 | ? 'nicht vorhanden – gleichwertige Textfassung: Tabelle „Signalzeiten je Signalgruppe" ' + |
| 1388 | 'und Abschnitt „Phasen und Phasenübergänge"' |
| 1389 | : 'nicht vorhanden – Textfassung der Signalbilder: Tabelle „Signalzeiten je ' + |
| 1390 | 'Signalgruppe"; das Phasenband ist nicht in Textform enthalten', |
| 1391 | ], |
| 1392 | [ |
| 1393 | 'Dokumenttitel und Sprache (Deutsch)', |
| 1394 | 'vorhanden; der Titel wird im Anzeigeprogramm ausgewiesen', |
| 1395 | ], |
| 1396 | ['Lesezeichen für jeden Abschnitt', 'vorhanden'], |
| 1397 | ['Text maschinell auslesbar (Vektorzeichnung, keine Rastergrafik)', 'ja'], |
| 1398 | ['Schriften in die Datei eingebettet', 'nein – Standardschriften, Zeichenvorrat Latin-1'], |
| 1399 | ].map((row) => row.map((cell) => toWinAnsi(cell))), |
| 1400 | }); |
| 1401 | |
| 1402 | context.cursor = lastTableBottom(doc) + 14; |
| 1403 | |
| 1404 | writeParagraph( |
| 1405 | context, |
| 1406 | 'Was das für Bildschirmleseprogramme bedeutet: Der Text ist auslesbar und die Sprache der ' + |
| 1407 | 'Datei ist als Deutsch ausgewiesen, die Bedeutung der Teile ist es nicht. Überschriften ' + |
| 1408 | 'werden nicht als Überschriften angesagt, es gibt keine Überschriftennavigation. Tabellen ' + |
| 1409 | 'werden ohne Bezug zu Zeilen- und Spaltenköpfen vorgelesen; bei einer Matrix wie der der ' + |
| 1410 | 'Zwischenzeiten bleibt damit offen, welche Zahl zu welchem Paar aus räumender und ' + |
| 1411 | 'einfahrender Signalgruppe gehört. Kopf- und Fußzeile jeder Seite stehen im Lesefluss und ' + |
| 1412 | 'unterbrechen ihn auf jeder Seite.' + |
| 1413 | // Nur, wo es eine Zeichnung gibt (Fassung 5.9.0). |
| 1414 | (mitPlanabschnitt |
| 1415 | ? ' Die Zeichnung des Signalzeitenplans erscheint als lose Folge von Zahlen, ohne ' + |
| 1416 | 'Angabe, dass es sich um eine Zeichnung handelt.' |
| 1417 | : ''), |
| 1418 | ); |
| 1419 | |
| 1420 | /* |
| 1421 | * WARUM DIESER ABSATZ NEU IST (Ausschnittsatz der Zwischenzeitenmatrix). |
| 1422 | * |
| 1423 | * Der Abschnitt ist ein Lueckenausweis. Aendert sich die Darstellung eines |
| 1424 | * Abschnitts so, dass eine Sprachausgabe etwas ANDERES vorliest als zuvor, |
| 1425 | * muss er es sagen - sonst beschreibt er eine Datei, die es nicht mehr gibt. |
| 1426 | * |
| 1427 | * Was sich geaendert hat: Bei langen Signalgruppennamen trug der Kopf der |
| 1428 | * Matrix die Namen in so schmalen Spalten, dass jspdf-autotable sie MITTEN |
| 1429 | * IM WORT umbrach. Gemessen an einem Knotenpunkt mit 16 bis 32 Gruppen und |
| 1430 | * Namen von rund 140 Zeichen stand danach KEIN EINZIGER Name ungeteilt im |
| 1431 | * ausgelesenen Text (0 von 16, 0 von 24, 0 von 32). Die ersten drei |
| 1432 | * Kopfbruchstuecke lauteten dort, aus der Datei zurueckgelesen: |
| 1433 | * "Kraftfahrz" / "euge Nor" / "dzufahrt " - ein Zitat, keine Nachbildung |
| 1434 | * (Befund B7 technisch: hier stand vorher ein erfundenes "Kraftfah rzeuge |
| 1435 | * Nordzuf ahrt"). Jetzt tragen die Spaltenkoepfe Ordnungsnummern, und die |
| 1436 | * Zuordnung ueber der |
| 1437 | * Tabelle fuehrt jeden Namen ungeteilt in einer Zeile - gemessen 16 von 16, |
| 1438 | * 24 von 24, 32 von 32. |
| 1439 | * |
| 1440 | * WAS DIESER ABSATZ NICHT BEHAUPTEN DARF: dass damit 1.3.1 erfuellt waere. |
| 1441 | * Der Bezug zwischen Zelle und Kopfzelle ist weiterhin nur optisch gesetzt. |
| 1442 | * Die Aenderung macht die Namen vorlesbar, nicht die Matrix auswertbar. |
| 1443 | * |
| 1444 | * UND ER STEHT NUR DA, WO ER ZUTRIFFT - aus demselben Grund wie der Verweis |
| 1445 | * auf den Phasenabschnitt weiter unten: Wer die Matrix abwaehlt, bekommt |
| 1446 | * eine Datei ohne sie, und wer kurze Bezeichnungen benutzt, hat die Namen im |
| 1447 | * Kopf. In beiden Faellen beschriebe der Absatz etwas, das es in DIESER |
| 1448 | * Ausgabe nicht gibt. Dasselbe gilt fuer den Verweis auf den Rechenweg. |
| 1449 | * |
| 1450 | * ER SAGT AUCH, WIE WEIT DIE ZUSAGE TRAEGT (Fassung 5.8.0): "ungeteilt in |
| 1451 | * einer Zeile" galt frueher unbedingt, traf aber nur zu, solange der Name in |
| 1452 | * eine Bannerzeile passte. SignalGroup.name ist in der Laenge nicht begrenzt; |
| 1453 | * ab rund 180 Zeichen brach jede Zuordnungszeile um. Jetzt entscheidet der |
| 1454 | * beim Zeichnen ermittelte MatrixSatzbefund, welcher der beiden Saetze hier |
| 1455 | * steht. |
| 1456 | * |
| 1457 | * DASSELBE GILT FUER DEN ORT DER ZUORDNUNG: "auf jeder Seite" stimmt nur, |
| 1458 | * solange sie in den wiederholten Tabellenkopf passt. Ist sie fuer sich schon |
| 1459 | * hoeher als ein Blatt, steht sie ungekuerzt vor dem Ausschnitt - und dann |
| 1460 | * sagt der Lueckenausweis genau das. |
| 1461 | */ |
| 1462 | const satzbefund = context.matrixSatzbefund; |
| 1463 | if (rechenweg.matrix && satzbefund?.mitOrdnungsnummern === true) { |
| 1464 | writeParagraph( |
| 1465 | context, |
| 1466 | 'Zur Zwischenzeitenmatrix in dieser Datei: Die Namen der Signalgruppen taugen nicht als ' + |
| 1467 | 'Spaltenkopf. Die Spaltenköpfe tragen deshalb die Ordnungsnummer der ' + |
| 1468 | 'einfahrenden Signalgruppe, und ' + |
| 1469 | (satzbefund.zuordnungAufJedemBlatt |
| 1470 | ? 'über der Tabelle steht auf jeder Seite, welche Nummer zu welchem Namen gehört - ' |
| 1471 | : 'vor jedem Ausschnitt steht, welche Nummer zu welchem Namen gehört; auf jeder Seite ' + |
| 1472 | 'wiederholt wird die Zuordnung nicht, weil sie länger ist als ein Blatt - ') + |
| 1473 | (satzbefund.namenUngeteilt |
| 1474 | ? 'dort steht jeder Name ungeteilt in einer Zeile und wird am Stück vorgelesen. ' |
| 1475 | : 'dort steht zu jeder Nummer ihr voller Name. Namen, die für eine Zeile zu lang sind, ' + |
| 1476 | 'laufen über mehrere Zeilen weiter; jede Folgezeile wiederholt die Nummer, damit sie ' + |
| 1477 | 'zuzuordnen bleibt. ') + |
| 1478 | 'Das macht die Namen hörbar; den fehlenden programmatischen Bezug zwischen ' + |
| 1479 | 'einer Zahl und ihren beiden Köpfen ersetzt es nicht. ' + |
| 1480 | (zwischenzeitRechenweg(rechenweg) |
| 1481 | ? 'Wer die Zwischenzeiten Beziehung für Beziehung hören will, findet sie im Abschnitt ' + |
| 1482 | '„Zwischenzeiten – Rechenweg", der jede Beziehung als „Name -> Name" mit ihrem ' + |
| 1483 | 'Ergebnis führt.' |
| 1484 | : 'Der Abschnitt, der dieselben Zahlen Beziehung für Beziehung als „Name -> Name" ' + |
| 1485 | 'führt, ist für diese Ausgabe abgewählt; ohne ihn gibt es keine Fassung dieser ' + |
| 1486 | 'Zahlen, die eine Sprachausgabe Paar für Paar wiedergeben kann.'), |
| 1487 | ); |
| 1488 | } |
| 1489 | |
| 1490 | /* |
| 1491 | * WAS "VOLLSTAENDIG" HIER GENAU HEISST. |
| 1492 | * |
| 1493 | * Hier stand: "Der Inhalt der Zeichnung steht vollstaendig in Textform in der |
| 1494 | * Tabelle „Signalzeiten je Signalgruppe"". Das ist zu weit gefasst. Die |
| 1495 | * Zeichnung traegt ueber der Zeitachse ein PHASENBAND mit Phasennamen, |
| 1496 | * Phasenbeginn und Uebergangszeiten (drawPhaseBand in |
| 1497 | * src/render/signalPlanDrawing.ts); diese Angaben stehen in der |
| 1498 | * Signalzeitentabelle nicht, sondern im Abschnitt "Phasen und |
| 1499 | * Phasenuebergaenge". Die gleichwertige Textfassung der GANZEN Zeichnung sind |
| 1500 | * also zwei Tabellen, nicht eine. |
| 1501 | * |
| 1502 | * Der Abschnitt ist ein Lueckenausweis - er darf als einziger Teil der Datei |
| 1503 | * keine Angabe machen, die einer Nachpruefung nicht standhaelt. Deshalb wird |
| 1504 | * die Zusage aufgeteilt und nicht abgeschwaecht. |
| 1505 | * |
| 1506 | * Und deshalb haengt sie am Schalter: Wer die Phasen abwaehlt, bekommt eine |
| 1507 | * Datei ohne diesen Abschnitt - ein Verweis darauf waere dann falsch. Dann |
| 1508 | * wird die Luecke benannt, statt sie zu verschweigen. |
| 1509 | * |
| 1510 | * UND EBENSO AM SCHALTER DES SIGNALZEITENPLANS (Fassung 5.9.0): Zeichnung und |
| 1511 | * Signalzeitentabelle entstehen beide nur unter `options.includePlan`, und |
| 1512 | * der Absatz unter der Zeichnung mit ihnen. Der Satz stand trotzdem unbedingt |
| 1513 | * da und beschrieb bei abgewaehltem Abschnitt drei Dinge, die es in dieser |
| 1514 | * Ausgabe nicht gibt - waehrend die Uebersicht der Bestandteile wenige Seiten |
| 1515 | * davor die Abwahl korrekt ausweist. |
| 1516 | */ |
| 1517 | if (mitPlanabschnitt) { |
| 1518 | writeParagraph( |
| 1519 | context, |
| 1520 | 'Die Signalbilder der Zeichnung stehen vollständig in Textform in der Tabelle „Signalzeiten ' + |
| 1521 | 'je Signalgruppe"; sie führt Freigabefenster, Freigabezeit, Rot-Gelb, Gelb und Rot je ' + |
| 1522 | 'Signalgruppe auf und enthält damit auch die Zeiten, die in der Zeichnung wegen zu ' + |
| 1523 | 'schmaler Abschnitte unbeschriftet bleiben. Das Phasenband über der Zeitachse - ' + |
| 1524 | 'Phasennamen, Phasenbeginn und Übergangszeiten - steht ' + |
| 1525 | (mitPhasenabschnitt |
| 1526 | ? 'in den Tabellen des Abschnitts „Phasen und Phasenübergänge". Diese beiden Tabellen ' + |
| 1527 | 'zusammen sind die gleichwertige Textfassung der Zeichnung' |
| 1528 | : 'nur in der Zeichnung: Der Abschnitt „Phasen und Phasenübergänge" wurde für diese ' + |
| 1529 | 'Ausgabe abgewählt. Ohne ihn ist die Signalzeitentabelle die Textfassung der ' + |
| 1530 | 'Signalbilder, nicht der ganzen Zeichnung') + |
| 1531 | ' - ein Alternativtext im Sinne von 1.1.1 ist sie mangels Auszeichnung nicht. Ein Absatz ' + |
| 1532 | 'unter der Zeichnung verweist auf beides.', |
| 1533 | ); |
| 1534 | } else { |
| 1535 | writeParagraph( |
| 1536 | context, |
| 1537 | 'Zur Zeichnung des Signalzeitenplans: Der Abschnitt „Signalzeitenplan" wurde für diese ' + |
| 1538 | 'Ausgabe abgewählt. Diese Datei enthält deshalb weder die Zeichnung noch die Tabelle ' + |
| 1539 | '„Signalzeiten je Signalgruppe" - über eine Textfassung der Signalbilder ist hier ' + |
| 1540 | 'nichts auszusagen, weil es die Signalbilder in dieser Ausgabe nicht gibt.', |
| 1541 | ); |
| 1542 | } |
| 1543 | |
| 1544 | writeParagraph( |
| 1545 | context, |
| 1546 | 'Was eine öffentliche Stelle tun muss, die diese Unterlage veröffentlicht: Wird sie im ' + |
| 1547 | 'Internet bereitgestellt - etwa als Anlage einer öffentlichen Bekanntmachung nach § 27a ' + |
| 1548 | 'VwVfG -, ist sie ein Dokument dieser Stelle und muss nach EN 301 549 Kapitel 10 ' + |
| 1549 | 'barrierefrei sein. Diese Datei ist es nicht. Sie ist vor einer Veröffentlichung ' + |
| 1550 | 'nachträglich auszuzeichnen (Strukturbaum mit Überschriften, Absätzen und Tabellenkopfzellen, ' + |
| 1551 | 'Alternativtext der Zeichnung, Kopf- und Fußzeile als Seitenrahmen, eingebettete Schriften) ' + |
| 1552 | 'und das Ergebnis mit einem Prüfwerkzeug wie PAC oder veraPDF nachzuprüfen. Ist das nicht ' + |
| 1553 | 'leistbar, ist eine zugängliche Fassung des Inhalts bereitzustellen und die Lücke in der ' + |
| 1554 | 'Erklärung zur Barrierefreiheit der Stelle zu benennen.' + |
| 1555 | // Auch dieser Schlusssatz nannte die Signalzeitentabelle unbedingt |
| 1556 | // (Fassung 5.9.0). |
| 1557 | (mitPlanabschnitt |
| 1558 | ? ' Für die Prüfung durch die Behörde genügt die Signalzeitentabelle als Textfassung; ' + |
| 1559 | 'für eine Veröffentlichung genügt sie nicht.' |
| 1560 | : ''), |
| 1561 | ); |
| 1562 | } |
| 1563 | |
| 1564 | // --- Planungsgrundlage ------------------------------------------------------ |
| 1565 | |
| 1566 | /** |
| 1567 | * Nachweis der Vermessungsgrundlage. |
| 1568 | * |
| 1569 | * Der Ausdruck enthielt bisher weder Lageplan noch Luftbild und auch keine |
| 1570 | * Angabe, worauf die Raeum- und Einfahrwege beruhen. Ein Pruefer konnte die |
| 1571 | * Zwischenzeiten deshalb nicht nachvollziehen: Dieselbe Zahl kann aus einem |
| 1572 | * massstaeblichen Luftbild stammen oder von Hand geschaetzt sein. Alle Werte |
| 1573 | * kommen aus dem Modell; gerechnet wird in src/domain/geometrie/lageplan.ts. |
| 1574 | * |
| 1575 | * Der Abschnitt weist die UNTERLAGE nach, nicht die Herkunft der einzelnen |
| 1576 | * Masse. Seine Saetze sagen, was sich auf dieser Unterlage abgreifen laesst - |
| 1577 | * nicht, woher eine bestimmte Zahl stammt. Das ist die Trennung, auf die es |
| 1578 | * ankommt: Eine Unterlage kann massstaeblich sein, ohne dass ein einziger Weg |
| 1579 | * von ihr abgegriffen wurde. |
| 1580 | * |
| 1581 | * Woher die einzelnen Masse stammen, fuehrt das Modell seit Schemastand 9 mit |
| 1582 | * (`Conflict.wegnachweis`); ausgewiesen wird es in der Spalte "Herkunft der |
| 1583 | * Wege" der Rechenwegtabelle. Bis dahin gab es dafuer nur das freie |
| 1584 | * Bemerkungsfeld, und der Kopf dieser Funktion sagte entsprechend, das Modell |
| 1585 | * fuehre es "nirgends mit" - waehrend dieselbe Funktion weiter unten bereits |
| 1586 | * die Zeile "Nachweis der Wege" druckte. |
| 1587 | * |
| 1588 | * Zur Unterlage gehoert ihre Aufloesung. Ein Luftbild sieht grob wie fein |
| 1589 | * gleich aus, doch der Zeichenfehler geht ueber den Raeumweg unmittelbar in die |
| 1590 | * Zwischenzeit ein - an einer Fussgaengerfurt fast zehnmal so stark wie bei |
| 1591 | * einem Kraftfahrzeugstrom. Ohne die Zeile "Eignung als Messgrundlage" koennte |
| 1592 | * ein Pruefer aus diesen Unterlagen nicht erkennen, ob die Zwischenzeiten auf |
| 1593 | * einer tauglichen Unterlage beruhen. |
| 1594 | */ |
| 1595 | /** |
| 1596 | * Die Zeile "Tagesprogramme" der Bestandteileuebersicht. |
| 1597 | * |
| 1598 | * Sie sagt bei einem Programm, dass es das einzige ist, und bei mehreren, dass |
| 1599 | * diese Unterlage EINES von ihnen zeigt. Der Unterschied ist keine Feinheit: |
| 1600 | * Eine Anordnung nach § 45 StVO gilt der Anlage, und wer vier Programme |
| 1601 | * schaltet, muss vier Signalzeitenplaene beibringen. |
| 1602 | */ |
| 1603 | function programmzeile(project: Project): string { |
| 1604 | const anzahl = programmanzahl(project); |
| 1605 | if (anzahl === 1) return 'ja – dieses Projekt führt genau ein Tagesprogramm'; |
| 1606 | return ( |
| 1607 | `${String(anzahl)} Programme – diese Unterlage zeigt „${project.program.name}"; die übrigen ` + |
| 1608 | 'sind gesondert auszugeben (Abschnitt „Tagesprogramme dieser Anlage")' |
| 1609 | ); |
| 1610 | } |
| 1611 | |
| 1612 | /** |
| 1613 | * Alle Tagesprogramme mit Umlaufzeit und Pruefergebnis. |
| 1614 | * |
| 1615 | * WAS DIESE TAFEL LEISTET UND WAS NICHT: Sie weist nach, dass JEDES Programm |
| 1616 | * geprueft wurde - das ist die sicherheitsrelevante Zusage der |
| 1617 | * Programmbibliothek. Sie ersetzt NICHT den Signalzeitenplan der uebrigen |
| 1618 | * Programme; der steht in dieser Datei nicht, und der Text darunter sagt das. |
| 1619 | */ |
| 1620 | function drawProgrammuebersicht(context: Context, now: Date): void { |
| 1621 | const { doc, project } = context; |
| 1622 | const pruefungen = programmPruefungen(project, now); |
| 1623 | |
| 1624 | autoTable(doc, { |
| 1625 | startY: context.cursor, |
| 1626 | margin: TABELLENRAND, |
| 1627 | theme: 'grid', |
| 1628 | styles: { fontSize: 9, cellPadding: 3 }, |
| 1629 | headStyles: { fillColor: '#eef1f6', textColor: '#1b2230', fontStyle: 'bold' }, |
| 1630 | head: [ |
| 1631 | [ |
| 1632 | 'Tagesprogramm', |
| 1633 | 'Verfahren', |
| 1634 | 'Umlaufzeit', |
| 1635 | 'Fehler', |
| 1636 | 'Warnungen', |
| 1637 | 'in dieser Unterlage', |
| 1638 | ].map(toWinAnsi), |
| 1639 | ], |
| 1640 | body: pruefungen.map((eintrag) => |
| 1641 | [ |
| 1642 | eintrag.name, |
| 1643 | eintrag.verfahren === 'manuell' ? 'feste Vorgabe' : CYCLE_METHOD_LABELS[eintrag.verfahren], |
| 1644 | fmt.seconds(eintrag.umlaufzeit), |
| 1645 | String(eintrag.errorCount), |
| 1646 | String(eintrag.warningCount), |
| 1647 | eintrag.aktiv ? 'ja – vollständig' : 'nein – nur diese Zeile', |
| 1648 | ].map(toWinAnsi), |
| 1649 | ), |
| 1650 | }); |
| 1651 | |
| 1652 | context.cursor = lastTableBottom(doc) + 12; |
| 1653 | |
| 1654 | writeParagraph( |
| 1655 | context, |
| 1656 | `Diese Anlage führt ${String(pruefungen.length)} Tagesprogramme. Signalgruppen, ` + |
| 1657 | 'Konfliktbeziehungen, Zwischenzeiten und die Besetzung der Phasen sind ihnen gemeinsam und ' + |
| 1658 | 'stehen in dieser Unterlage vollständig; eigen sind jedem Programm Umlaufzeit, Verfahren, ' + |
| 1659 | 'Versatz, Verkehrsstärken und die von Hand vorgegebenen Freigabezeiten.', |
| 1660 | ); |
| 1661 | writeParagraph( |
| 1662 | context, |
| 1663 | `AUSGEDRUCKT IST DER SIGNALZEITENPLAN DES PROGRAMMS „${project.program.name}". Für die ` + |
| 1664 | 'übrigen Programme steht in dieser Datei nur die Zeile der Tafel oben – keine Signalzeiten, ' + |
| 1665 | 'keine Grafik, kein Prüfbericht im Einzelnen. Wer die Anlage vollständig anordnen lassen ' + |
| 1666 | 'will, gibt jedes Programm gesondert aus: in der Ansicht „Phasen" umschalten und erneut ' + |
| 1667 | 'ausgeben.', |
| 1668 | ); |
| 1669 | writeParagraph( |
| 1670 | context, |
| 1671 | 'Geprüft wurden alle Programme, und zwar mit denselben Regeln: Die Spalten „Fehler" und ' + |
| 1672 | '„Warnungen" stammen aus je einem eigenen Prüflauf. Solange irgendein Programm einen Fehler ' + |
| 1673 | 'führt, ist die Ausgabe gesperrt – auch dann, wenn das angezeigte Programm fehlerfrei ist.', |
| 1674 | ); |
| 1675 | } |
| 1676 | |
| 1677 | /** |
| 1678 | * Die Gruene Welle im Strassenzug. |
| 1679 | * |
| 1680 | * WAS DER ABSCHNITT LEISTET: Er weist die Abstimmung mit den Nachbaranlagen |
| 1681 | * nach - Abstaende, Reisezeiten, empfohlene Versaetze, Bandbreite in beiden |
| 1682 | * Fahrtrichtungen, dazu das Zeit-Weg-Diagramm. |
| 1683 | * |
| 1684 | * WAS ER AUSDRUECKLICH NICHT LEISTET: Er sagt nichts ueber die Nachbaranlagen |
| 1685 | * selbst. Deren Signalzeitenplaene, Zwischenzeiten und Pruefberichte stehen in |
| 1686 | * IHREN Unterlagen; hier stehen nur ihre Freigabefenster, wie sie der |
| 1687 | * Bearbeiter dieses Projekts eingetragen hat. Der Absatz darunter sagt das. |
| 1688 | */ |
| 1689 | function drawKoordinierung(context: Context): void { |
| 1690 | const { doc, project, plan } = context; |
| 1691 | const lage = koordinierungslage(project, plan); |
| 1692 | |
| 1693 | if (lage.hindernis !== null) { |
| 1694 | writeParagraph(context, HINDERNIS_TEXT[lage.hindernis]); |
| 1695 | return; |
| 1696 | } |
| 1697 | const ergebnis = lage.ergebnis; |
| 1698 | if (ergebnis === null) return; |
| 1699 | |
| 1700 | autoTable(doc, { |
| 1701 | startY: context.cursor, |
| 1702 | margin: TABELLENRAND, |
| 1703 | theme: 'grid', |
| 1704 | styles: { fontSize: 9, cellPadding: 3 }, |
| 1705 | headStyles: { fillColor: '#eef1f6', textColor: '#1b2230', fontStyle: 'bold' }, |
| 1706 | head: [ |
| 1707 | [ |
| 1708 | 'Anlage', |
| 1709 | 'Entfernung', |
| 1710 | 'Reisezeit', |
| 1711 | 'Empfohlener Versatz', |
| 1712 | 'Freigabe Hauptrichtung (von–bis) [s]', |
| 1713 | ].map(toWinAnsi), |
| 1714 | ], |
| 1715 | body: ergebnis.intersections.map((eintrag, index) => { |
| 1716 | const glied = lage.kette[index]; |
| 1717 | return [ |
| 1718 | `${eintrag.name}${glied?.eigener === true ? ' (dieser Knotenpunkt)' : ''}`, |
| 1719 | `${fmt.numShort(eintrag.cumulativeDistance, 0)} m`, |
| 1720 | fmt.seconds(eintrag.travelTime), |
| 1721 | fmt.seconds(eintrag.offset), |
| 1722 | /* |
| 1723 | * IN DEN UMLAUF GEFALTET UND IN DERSELBEN SCHREIBWEISE WIE DIE |
| 1724 | * SIGNALZEITENTABELLE, ueber `freigabefensterZelle` (Fassung 5.43.0): |
| 1725 | * Hier stand "Beginn bis Beginn + Dauer" - "91 s bis 139 s" bei 95 s |
| 1726 | * Umlaufzeit, waehrend die Signalzeitentabelle derselben Unterlage |
| 1727 | * "91–44" fuehrt. Und "Freigabe ab" einer Nachbaranlage darf die |
| 1728 | * Umlaufzeit uebersteigen; die Koordinierung rechnet mit dem gefalteten |
| 1729 | * Beginn, das Diagramm zeichnet ihn, und die Tabelle nannte den |
| 1730 | * eingetragenen. |
| 1731 | * |
| 1732 | * GENANNT IST EIN FENSTER: das, mit dem die Koordinierung rechnet. Ist |
| 1733 | * die Hauptrichtung dieses Knotenpunkts in mehreren Fenstern frei, |
| 1734 | * fuehrt die Signalzeitentabelle alle und diese Zelle nur dieses eine; |
| 1735 | * gleich ist dann die Schreibweise, nicht die Zeichenkette. |
| 1736 | */ |
| 1737 | glied === undefined |
| 1738 | ? '-' |
| 1739 | : freigabefensterZelle( |
| 1740 | [ |
| 1741 | { |
| 1742 | start: modulo(glied.freigabe.start, ergebnis.cycleTime), |
| 1743 | duration: glied.freigabe.duration, |
| 1744 | }, |
| 1745 | ], |
| 1746 | ergebnis.cycleTime, |
| 1747 | ), |
| 1748 | ].map(toWinAnsi); |
| 1749 | }), |
| 1750 | }); |
| 1751 | context.cursor = lastTableBottom(doc) + 12; |
| 1752 | writeParagraph( |
| 1753 | context, |
| 1754 | 'Freigabe Hauptrichtung: das Fenster in Sekunden ab Umlaufbeginn, in den Umlauf gefaltet – ' + |
| 1755 | 'so, wie die Koordinierung rechnet und das Diagramm zeichnet. Reicht es über den ' + |
| 1756 | 'Umlaufbeginn hinaus, liegt sein Ende vor seinem Beginn.', |
| 1757 | ); |
| 1758 | |
| 1759 | const hoehe = Math.max(150, Math.min(300, 70 + lage.kette.length * 34)); |
| 1760 | ensureSpace(context, hoehe + 60); |
| 1761 | doc.setFont('helvetica', 'bold'); |
| 1762 | doc.setFontSize(10); |
| 1763 | doc.setTextColor('#1b2230'); |
| 1764 | doc.text(toWinAnsi('Zeit-Weg-Diagramm'), PAGE_MARGIN, context.cursor); |
| 1765 | context.cursor += 14; |
| 1766 | const surface = new PdfSurface(doc, PAGE_MARGIN, context.cursor, context.contentWidth, hoehe); |
| 1767 | drawZeitWeg(surface, lage, { mitGegenrichtung: true }); |
| 1768 | context.cursor += hoehe + 10; |
| 1769 | |
| 1770 | writeParagraph( |
| 1771 | context, |
| 1772 | `Waagerecht ${String(UMLAEUFE_IM_BILD)} Umläufe, senkrecht der Weg entlang des Straßenzugs. ` + |
| 1773 | 'Die grünen Balken sind die Freigabezeiten der Hauptrichtung, die schräge Fläche ist das ' + |
| 1774 | 'Band: die Startzeiten, mit denen ein Fahrzeug ohne Halt durchkommt. Die blaue Fläche ist ' + |
| 1775 | 'die Gegenrichtung, wo sie erfasst ist.', |
| 1776 | ); |
| 1777 | |
| 1778 | writeParagraph( |
| 1779 | context, |
| 1780 | `Bandbreite in Hauptrichtung ${fmt.seconds(ergebnis.hinrichtung.breite)} ` + |
| 1781 | `(${fmt.ratio(ergebnis.hinrichtung.anteil, 2)} der Umlaufzeit, Beurteilung ` + |
| 1782 | `„${ergebnis.hinrichtung.quality}"). ` + |
| 1783 | (ergebnis.rueckrichtung === null |
| 1784 | ? 'Für die Gegenrichtung ist an mindestens einer Anlage kein Freigabefenster erfasst; ' + |
| 1785 | 'ein zweites Band wird deshalb NICHT gerechnet und auch nicht angenommen. Der ' + |
| 1786 | 'Zielkonflikt zwischen Hin- und Rückrichtung ist der eigentliche Gegenstand einer ' + |
| 1787 | 'Koordinierung und bleibt damit unbeurteilt.' |
| 1788 | : `Bandbreite in der Gegenrichtung ${fmt.seconds(ergebnis.rueckrichtung.breite)} ` + |
| 1789 | `(${fmt.ratio(ergebnis.rueckrichtung.anteil, 2)}, Beurteilung ` + |
| 1790 | `„${ergebnis.rueckrichtung.quality}").`), |
| 1791 | ); |
| 1792 | |
| 1793 | writeParagraph( |
| 1794 | context, |
| 1795 | 'DER VERSATZ IST EINE EMPFEHLUNG FÜR DIE NACHBARANLAGEN und verschiebt den Signalzeitenplan ' + |
| 1796 | 'dieses Knotenpunkts nicht: Er gilt zwischen Anlagen, nicht innerhalb einer. Die ' + |
| 1797 | 'Freigabezeiten der Nachbarn sind Eingaben dieses Projekts – ihre Signalzeitenpläne, ' + |
| 1798 | 'Zwischenzeiten und Prüfberichte stehen in ihren eigenen Unterlagen und sind hier weder ' + |
| 1799 | 'wiedergegeben noch geprüft. Eine Grüne Welle setzt zudem eine gemeinsame Umlaufzeit aller ' + |
| 1800 | `beteiligten Anlagen voraus; gerechnet ist mit ${fmt.seconds(ergebnis.cycleTime)} und einer ` + |
| 1801 | `Progressionsgeschwindigkeit von ${fmt.numShort(ergebnis.speed, 1)} km/h.`, |
| 1802 | ); |
| 1803 | |
| 1804 | for (const note of ergebnis.notes) { |
| 1805 | writeParagraph( |
| 1806 | context, |
| 1807 | `${note.severity === 'fehler' ? 'Fehler' : 'Hinweis'}: ${note.message}`, |
| 1808 | ); |
| 1809 | } |
| 1810 | } |
| 1811 | |
| 1812 | function drawPlanungsgrundlage(context: Context, rechenweg: RechenwegStand): void { |
| 1813 | const { doc, plan, project } = context; |
| 1814 | // Mit den Kennwerten des Projekts, nicht mit denen des Regelwerks: Der |
| 1815 | // Anwender darf die Raeumgeschwindigkeiten anpassen, und daran haengt die |
| 1816 | // Eignung. Sonst stuende im Ausdruck eine andere Einstufung als im |
| 1817 | // Pruefbericht - und keiner der beiden waere nachrechenbar. |
| 1818 | const grundlage = planungsgrundlage(project.lageplan, plan.defaults); |
| 1819 | |
| 1820 | newSection(context, 'Planungsgrundlage'); |
| 1821 | |
| 1822 | if (!grundlage.hatBild) { |
| 1823 | writeParagraph( |
| 1824 | context, |
| 1825 | 'Diesen Unterlagen liegt kein Lageplan und kein Luftbild bei. Die Räum- und Einfahrwege der ' + |
| 1826 | 'Zwischenzeitenberechnung stammen daher nicht aus diesen Unterlagen; auf welcher Grundlage ' + |
| 1827 | 'sie ermittelt wurden, geht aus ihnen nicht hervor und ist vom Planer gesondert nachzuweisen.', |
| 1828 | ); |
| 1829 | writeParagraph(context, wegnachweisVerteilung(project)); |
| 1830 | return; |
| 1831 | } |
| 1832 | |
| 1833 | writeParagraph( |
| 1834 | context, |
| 1835 | 'Die folgenden Angaben weisen die Unterlage aus, auf der die Fahrlinien dieses Projekts ' + |
| 1836 | 'gezeichnet sind und auf der sich Räum- und Einfahrwege abgreifen lassen. Stammt die ' + |
| 1837 | 'Unterlage aus einem Kartendienst, nennt die Herkunft Herausgeber, Ausschnittsmitte und ' + |
| 1838 | 'Kantenlänge; damit lässt sich derselbe Ausschnitt wieder beschaffen und nachmessen. Bei ' + |
| 1839 | 'einer eingelesenen Datei steht dort ihr Name.', |
| 1840 | ); |
| 1841 | |
| 1842 | autoTable(doc, { |
| 1843 | startY: context.cursor, |
| 1844 | margin: TABELLENRAND, |
| 1845 | theme: 'grid', |
| 1846 | styles: { fontSize: 9, cellPadding: 3, valign: 'top' }, |
| 1847 | headStyles: { fillColor: '#eef1f6', textColor: '#1b2230', fontStyle: 'bold' }, |
| 1848 | columnStyles: { 0: { cellWidth: 175, fontStyle: 'bold' } }, |
| 1849 | head: [['Angabe', 'Wert'].map(toWinAnsi)], |
| 1850 | body: [ |
| 1851 | ['Herkunft der Unterlage', grundlage.herkunft], |
| 1852 | [ |
| 1853 | 'Geladen am', |
| 1854 | grundlage.ladedatumBekannt ? fmt.dateTime(grundlage.geladenAm) : 'nicht überliefert', |
| 1855 | ], |
| 1856 | [ |
| 1857 | 'Bildgröße', |
| 1858 | `${fmt.numShort(grundlage.breiteBildpunkte, 0)} × ` + |
| 1859 | `${fmt.numShort(grundlage.hoeheBildpunkte, 0)} Bildpunkte`, |
| 1860 | ], |
| 1861 | [ |
| 1862 | 'Kantenlänge am Boden', |
| 1863 | grundlage.kantenlaengeMeter === null |
| 1864 | ? 'nicht bestimmbar - der Maßstab fehlt' |
| 1865 | : fmt.meters(grundlage.kantenlaengeMeter, 0), |
| 1866 | ], |
| 1867 | [ |
| 1868 | 'Maßstab', |
| 1869 | grundlage.meterJeBildpunkt === null |
| 1870 | ? 'nicht festgelegt' |
| 1871 | : `${fmt.numShort(grundlage.meterJeBildpunkt, 3)} m je Bildpunkt`, |
| 1872 | ], |
| 1873 | ['Herkunft des Maßstabs', massstabsherkunft(grundlage)], |
| 1874 | ['Nachweis der Wege', wegnachweisVerteilung(project)], |
| 1875 | // Derselbe Befund und dieselbe Zahl wie im Pruefbericht; der Satz kommt |
| 1876 | // aus dem Regelwerksmodul, damit beide nicht auseinanderlaufen koennen. |
| 1877 | ['Eignung als Messgrundlage', messeignungSatz(grundlage.messgenauigkeit)], |
| 1878 | [ |
| 1879 | 'Gezeichnet', |
| 1880 | `${grundlage.anzahlHaltlinien} Haltlinie(n), ${grundlage.anzahlFahrlinien} Fahrlinie(n)`, |
| 1881 | ], |
| 1882 | ].map((row) => row.map((cell) => toWinAnsi(String(cell)))), |
| 1883 | }); |
| 1884 | |
| 1885 | context.cursor = lastTableBottom(doc) + 12; |
| 1886 | |
| 1887 | writeParagraph( |
| 1888 | context, |
| 1889 | 'Die Zeile "Eignung als Messgrundlage" beurteilt, wofür die Auflösung dieser Unterlage ' + |
| 1890 | `ausreicht. Angesetzt ist ein Zeichenfehler von ${ZEICHENFEHLER_BILDPUNKTE} Bildpunkten auf ` + |
| 1891 | 'dem Räum- und noch einmal auf dem Einfahrweg. Hinnehmbar ist ein daraus folgender Fehler ' + |
| 1892 | `der Zwischenzeit von ${fmt.seconds(MESSFEHLER_SCHRANKE, 2)}: Die Zwischenzeit wird auf ganze ` + |
| 1893 | 'Sekunden aufgerundet, und bis zu diesem Betrag bleibt die Sicherheitswirkung des Aufrundens ' + |
| 1894 | 'erhalten. Weil die Räumgeschwindigkeit im Nenner steht, kostet derselbe Zeichenfehler an ' + |
| 1895 | 'einer Fußgängerfurt ein Vielfaches dessen, was er bei einem Kraftfahrzeugstrom kostet. Die ' + |
| 1896 | 'Angabe sagt, was sich auf dieser Unterlage abgreifen lässt - nicht, welche Wege tatsächlich ' + |
| 1897 | 'hier abgegriffen wurden.', |
| 1898 | ); |
| 1899 | |
| 1900 | /* |
| 1901 | * Der zweite Halbsatz verwies unbedingt auf die Spalte "Herkunft der Wege" im |
| 1902 | * Rechenweg der Zwischenzeiten. Die Spalte entsteht aber nur in |
| 1903 | * `drawIntergreenDetails`, also nur unter |
| 1904 | * `includeCalculationDetails && includeIntergreenMatrix`. Wer eines von |
| 1905 | * beiden abwaehlt, wurde in einen Abschnitt geschickt, den diese Ausgabe |
| 1906 | * nicht enthaelt - gemessen blieb "Herkunft der Wege" dann genau einmal im |
| 1907 | * Dokument stehen, naemlich in diesem Verweis. Und das ausgerechnet im |
| 1908 | * Abschnitt, der die Vermessungsgrundlage der sicherheitsrelevanten Masse |
| 1909 | * nachweist. |
| 1910 | * |
| 1911 | * Die Zusage "führt das Projekt je Beziehung mit" bleibt: Sie gilt fuer das |
| 1912 | * Modell (`Conflict.wegnachweis`) und nicht fuer diese Ausgabe. Nur der |
| 1913 | * Zeiger auf den Ort haengt daran, ob es den Ort gibt. |
| 1914 | */ |
| 1915 | const wegherkunftNachweis = zwischenzeitRechenweg(rechenweg) |
| 1916 | ? 'ausgewiesen wird es in der Spalte "Herkunft der Wege" im Rechenweg der Zwischenzeiten.' |
| 1917 | : 'der Rechenweg der Zwischenzeiten, der es in der Spalte "Herkunft der Wege" Beziehung für ' + |
| 1918 | 'Beziehung ausweist, ist für diese Ausgabe abgewählt.'; |
| 1919 | |
| 1920 | writeParagraph( |
| 1921 | context, |
| 1922 | grundlage.auswertbar |
| 1923 | ? 'Der Lageplan ist maßstäblich auswertbar; Räum- und Einfahrwege lassen sich daraus ' + |
| 1924 | 'abgreifen. Ob die Wege einer einzelnen Konfliktbeziehung tatsächlich von hier stammen, ' + |
| 1925 | `führt das Projekt je Beziehung mit; ${wegherkunftNachweis}` |
| 1926 | : 'Der Lageplan ist nicht auswertbar - es fehlt der Maßstab, oder es sind keine Fahrlinien ' + |
| 1927 | 'gezeichnet. In diesem Zustand lassen sich Räum- und Einfahrwege daraus nicht abgreifen; ' + |
| 1928 | 'als Nachweis der Vermaßung taugt die Unterlage daher nicht.', |
| 1929 | ); |
| 1930 | |
| 1931 | drawLageplanblattAbschnitt(context); |
| 1932 | } |
| 1933 | |
| 1934 | /** |
| 1935 | * Das massstaebliche Lageplanblatt. |
| 1936 | * |
| 1937 | * WOZU: Die Zwischenzeiten dieser Anlage beruhen auf gezeichneten Fahrlinien, |
| 1938 | * und von diesen Linien stand bis hierher keine einzige in der Unterlage. Ein |
| 1939 | * Pruefer konnte einen Raeumweg von 34,2 m nicht nachvollziehen, sondern nur |
| 1940 | * glauben. Der Abschnitt "Planungsgrundlage" wies die Unterlage nach, auf der |
| 1941 | * gezeichnet wurde - nicht die Zeichnung selbst. |
| 1942 | * |
| 1943 | * WAS ES NICHT IST: ein Signallageplan. Keine Signalgeberstandorte, keine |
| 1944 | * Bemassung. Die Bestandteileuebersicht fuehrt den bemassten Signallageplan |
| 1945 | * unveraendert als "NICHT Bestandteil dieser Unterlage"; dieses Blatt tritt |
| 1946 | * nicht an seine Stelle, und der Absatz darunter sagt es. |
| 1947 | */ |
| 1948 | function drawLageplanblattAbschnitt(context: Context): void { |
| 1949 | const { project } = context; |
| 1950 | const bild = project.lageplan.bild; |
| 1951 | const blatt = planblatt(project.lageplan, context.contentWidth, LAGEPLANBLATT_HOEHE); |
| 1952 | if (bild === null || blatt === null) return; |
| 1953 | |
| 1954 | /* |
| 1955 | * NORDPFEIL NUR BEI EINEM GEOREFERENZIERTEN AUSSCHNITT. Er wird in |
| 1956 | * ETRS89/UTM angefordert, dort liegt Norden oben, und die Abbildung auf das |
| 1957 | * Blatt dreht nicht. Bei einem eingelesenen Bild - Katasterauszug, Foto |
| 1958 | * einer Zeichnung, gescannter Bestandsplan - ist die Ausrichtung unbekannt. |
| 1959 | * Ein geratener Nordpfeil sieht aus wie eine Messung. |
| 1960 | * |
| 1961 | * DIE FUSSZEILE STEHT AUF BEIDEN BLAETTERN. Bei einem Kartendienst nennt die |
| 1962 | * Herkunft Herausgeber, Ausschnittsmitte und Kantenlaenge; damit laesst sich |
| 1963 | * derselbe Ausschnitt wieder beschaffen. Bei einem eingelesenen Bild ist sie |
| 1964 | * ein Dateiname, und der verortet nichts - das sagt die Zeile dann auch, |
| 1965 | * statt es dem Leser zu ueberlassen. |
| 1966 | * |
| 1967 | * OHNE BILD GIBT ES DIESES BLATT NICHT, und das ist keine neue Entscheidung: |
| 1968 | * `drawPlanungsgrundlage` bricht den ganzen Abschnitt schon vorher ab. Die |
| 1969 | * Bedingung steht trotzdem hier - so haengt sie sichtbar an der Stelle, die |
| 1970 | * sie braucht, statt zwei Ebenen hoeher stillschweigend zu gelten. |
| 1971 | */ |
| 1972 | const nordpfeil = project.lageplan.kalibrierung?.herkunft === 'georeferenziert'; |
| 1973 | const fusszeile = nordpfeil |
| 1974 | ? `Grundlage: ${bild.herkunft}` |
| 1975 | : `Grundlage: ${bild.herkunft} – eingelesenes Bild, ohne Verortung und ohne Nordrichtung.`; |
| 1976 | |
| 1977 | /* |
| 1978 | * DIE MASSKETTEN KOMMEN AUS DEM FACHKERN und stehen auf beiden Blaettern. |
| 1979 | * |
| 1980 | * Bemasst wird nur, was das Programm ohnehin fuehrt: die Laenge jeder |
| 1981 | * Haltlinie und Laenge wie Breite jedes Arbeitsbereichs. Was es NICHT |
| 1982 | * bemasst - den Abstand einer Haltlinie zum Arbeitsbereich und den Abstand |
| 1983 | * zweier Haltlinien -, begruendet `masskette.ts` an Ort und Stelle; kurz: |
| 1984 | * das eine benennt ein Bandende, das die Auskunft bewusst offenlaesst, das |
| 1985 | * andere waere eine zweite Zahl fuer die Engstellenlaenge. |
| 1986 | * |
| 1987 | * KEIN SCHALTER. Die Ketten treffen keine neue Aussage, sie schreiben eine |
| 1988 | * vorhandene an die Stelle, an der sie gilt. Ein abwaehlbarer Nachweis waere |
| 1989 | * dieselbe Bauart Fehler wie eine abwaehlbare Bestandteileuebersicht. |
| 1990 | */ |
| 1991 | const ketten = massketten(project.lageplan, blatt); |
| 1992 | |
| 1993 | zeichneBlatt(context, blatt, 'Lageplan der Fahrlinien', { |
| 1994 | mitNamen: true, |
| 1995 | nordpfeil, |
| 1996 | fusszeile, |
| 1997 | massketten: ketten, |
| 1998 | }); |
| 1999 | |
| 2000 | writeParagraph( |
| 2001 | context, |
| 2002 | 'Die Zeichnung gibt die Fahr- und Haltlinien wieder, aus denen Räum- und Einfahrwege dieses ' + |
| 2003 | 'Projekts abgegriffen sind – ohne das Luftbild darunter, damit kein Bildinhalt eine Linie ' + |
| 2004 | 'verdeckt. ' + |
| 2005 | (blatt.massstabsnenner === null |
| 2006 | ? '' |
| 2007 | : `Der Blattmaßstab beträgt rund 1:${String(blatt.massstabsnenner)}. `) + |
| 2008 | 'MASSGEBEND IST DER MASSSTABSBALKEN unten links, nicht das Verhältnis: Wird dieses Blatt ' + |
| 2009 | 'verkleinert kopiert oder auf ein anderes Format gedruckt, stimmt das Verhältnis nicht ' + |
| 2010 | 'mehr, der Balken schrumpft mit und bleibt richtig. Wer aus einem falsch skalierten Blatt ' + |
| 2011 | 'einen Räumweg abgreift, bekommt eine zu kurze Zwischenzeit. ' + |
| 2012 | 'Die Maßketten schreiben an, was das Projekt vermessen führt: die Länge jeder Haltlinie ' + |
| 2013 | 'sowie Länge und Breite jedes Arbeitsbereichs. Sie sind keine zweite Rechnung – die Zahlen ' + |
| 2014 | 'stammen aus derselben Vermessung wie jede andere Länge dieser Unterlage. Nicht bemaßt ' + |
| 2015 | 'sind der Abstand einer Haltlinie zur Arbeitsstelle und der Abstand zweier Haltlinien: Die ' + |
| 2016 | 'erste Angabe nennt bewusst nicht, zu welchem Ende sie misst, die zweite wäre eine zweite ' + |
| 2017 | 'Zahl für die Engstellenlänge, und die ist hier der größte erfasste Räumweg. ' + |
| 2018 | 'Das Blatt ist KEIN Signallageplan: Es führt keine Signalgeberstandorte und keine ' + |
| 2019 | 'Bemaßung der Aufstellung; beides ist gesondert beizubringen. Und es sagt nicht, ob die Wege einer ' + |
| 2020 | 'einzelnen Konfliktbeziehung wirklich von hier stammen – das führt das Projekt je ' + |
| 2021 | 'Beziehung mit.', |
| 2022 | ); |
| 2023 | |
| 2024 | if (context.blattbild === null) return; |
| 2025 | |
| 2026 | zeichneBlatt( |
| 2027 | context, |
| 2028 | blatt, |
| 2029 | 'Derselbe Lageplan über dem Luftbild', |
| 2030 | { mitNamen: true, nordpfeil, fusszeile, aufBild: true, massketten: ketten }, |
| 2031 | context.blattbild, |
| 2032 | ); |
| 2033 | |
| 2034 | writeParagraph( |
| 2035 | context, |
| 2036 | 'Dasselbe Blatt, im selben Maßstab und mit denselben Linien, über der Bildgrundlage. Es ' + |
| 2037 | 'dient der VERORTUNG: Das Blatt darüber zeigt Linien ohne Umgebung und lässt sich für ' + |
| 2038 | 'sich allein keinem Ort zuordnen. ' + |
| 2039 | 'Die Aufnahme ist auf den gezeigten Ausschnitt zugeschnitten und auf die Auflösung dieses ' + |
| 2040 | 'Blattes verkleinert; ihre Herkunft steht unten rechts und ausführlich im Abschnitt ' + |
| 2041 | '„Planungsgrundlage“. ' + |
| 2042 | 'GEMESSEN WIRD NICHT AM BILD: Maßgebend bleiben der Maßstabsbalken und die Linien. Die ' + |
| 2043 | 'verkleinerte Aufnahme trägt die Lage, nicht die Länge. ' + |
| 2044 | 'Wo die Zeichnung über den Rand der Aufnahme hinausreicht, bleibt der Grund weiß – dort ' + |
| 2045 | 'liegt keine Bildgrundlage vor.', |
| 2046 | ); |
| 2047 | |
| 2048 | zeichneSignalgeberblatt(context, blatt, nordpfeil, fusszeile, ketten); |
| 2049 | } |
| 2050 | |
| 2051 | /** |
| 2052 | * Das dritte Blatt: Lageplan mit den Signalgeberstandorten. |
| 2053 | * |
| 2054 | * ES ENTSTEHT NUR, WENN WELCHE ERFASST SIND. Ein leeres drittes Blatt waere |
| 2055 | * die Behauptung, es gebe hier nichts aufzustellen - und das ist etwas anderes |
| 2056 | * als "nicht erfasst". |
| 2057 | * |
| 2058 | * WARUM EIN EIGENES BLATT UND KEINE ZUSAETZLICHE EBENE AUF DEM ERSTEN: Ein |
| 2059 | * Signalgeber steht neben der Haltlinie, also genau dort, wo Raeum- und |
| 2060 | * Einfahrweg beginnen. Auf dem Nachweisblatt verdeckte er die Stelle, um die |
| 2061 | * es dort geht. Zwei Aussagen, zwei Blaetter - dieselbe Ueberlegung wie beim |
| 2062 | * Luftbildblatt. |
| 2063 | * |
| 2064 | * UND ES IST WEITERHIN KEIN SIGNALLAGEPLAN. Es zeigt Standort, Blickrichtung |
| 2065 | * und Kennung; Bauform, Hoehe, Blendenmass und die Sichtpruefung kennt dieses |
| 2066 | * Programm nicht, und die Uebersicht der Bestandteile sagt das mit denselben |
| 2067 | * Worten. |
| 2068 | */ |
| 2069 | function zeichneSignalgeberblatt( |
| 2070 | context: Context, |
| 2071 | blatt: Planblatt, |
| 2072 | nordpfeil: boolean, |
| 2073 | fusszeile: string, |
| 2074 | ketten: readonly Masskette[], |
| 2075 | ): void { |
| 2076 | if (blatt.signalgeber.length === 0) return; |
| 2077 | |
| 2078 | zeichneBlatt(context, blatt, 'Lageplan mit Signalgeberstandorten', { |
| 2079 | mitNamen: false, |
| 2080 | nordpfeil, |
| 2081 | fusszeile, |
| 2082 | massketten: ketten, |
| 2083 | mitSignalgebern: true, |
| 2084 | }); |
| 2085 | |
| 2086 | writeParagraph( |
| 2087 | context, |
| 2088 | `Dasselbe Blatt mit den ${String(blatt.signalgeber.length)} erfassten Signalgebern: Kreis ` + |
| 2089 | 'mit Kennung am Standort, Pfeil in Blickrichtung. Die Fahrlinien sind ohne Beschriftung ' + |
| 2090 | 'gezeichnet, damit die Kennungen der Signalgeber eindeutig bleiben. ' + |
| 2091 | 'DAS SINNBILD IST NICHT MASSSTÄBLICH: Ein Ampelkopf ist rund einen Viertelmeter breit und ' + |
| 2092 | 'wäre auf diesem Blatt kaum sichtbar; Kreis und Pfeil haben deshalb eine feste Größe. ' + |
| 2093 | 'Maßstäblich und abgreifbar ist der Mittelpunkt des Kreises. ' + |
| 2094 | 'EIN BEMASSTER SIGNALLAGEPLAN IST DIESES BLATT NICHT: Es führt Standort, Blickrichtung und ' + |
| 2095 | 'Kennung, aber keine Bauform, keine Höhe, kein Blendenmaß, keine Sichtprüfung und keinen ' + |
| 2096 | 'Nachweis der verbleibenden Fahrbahnbreite. Die Übersicht der Bestandteile sagt, was damit ' + |
| 2097 | 'enthalten ist und was weiterhin gesondert beizubringen bleibt. ' + |
| 2098 | 'Die Standorte gehen in keine Rechnung ein – Zwischenzeit, Freigabezeit und Umlaufzeit ' + |
| 2099 | 'kennen sie nicht.', |
| 2100 | ); |
| 2101 | } |
| 2102 | |
| 2103 | /** |
| 2104 | * Ein Lageplanblatt mit Ueberschrift, wahlweise ueber einer Bildgrundlage. |
| 2105 | * |
| 2106 | * DAS BILD GEHT UNMITTELBAR AN jsPDF UND NICHT UEBER `Surface`. Die |
| 2107 | * Zeichenflaeche beschreibt Strich, Flaeche und Schrift; eine Rastergrafik ist |
| 2108 | * keines davon, und der Bildschirm zeichnet sein Luftbild ohnehin selbst |
| 2109 | * (`ui/lageplan/leinwand.ts`). Eine sechste Grundoperation, die nur eine der |
| 2110 | * beiden Flaechen ausfuehren koennte, waere eine Zusage, die eine von beiden |
| 2111 | * nicht haelt. |
| 2112 | */ |
| 2113 | function zeichneBlatt( |
| 2114 | context: Context, |
| 2115 | blatt: Planblatt, |
| 2116 | ueberschrift: string, |
| 2117 | optionen: LageplanZeichnungOptionen, |
| 2118 | bild: Blattbild | null = null, |
| 2119 | ): void { |
| 2120 | const { doc } = context; |
| 2121 | ensureSpace(context, blatt.hoehe + 60); |
| 2122 | doc.setFont('helvetica', 'bold'); |
| 2123 | doc.setFontSize(10); |
| 2124 | doc.setTextColor('#1b2230'); |
| 2125 | doc.text(toWinAnsi(ueberschrift), PAGE_MARGIN, context.cursor); |
| 2126 | context.cursor += 14; |
| 2127 | |
| 2128 | if (bild !== null) { |
| 2129 | doc.addImage( |
| 2130 | bild.datenUrl, |
| 2131 | 'JPEG', |
| 2132 | PAGE_MARGIN + bild.zielX, |
| 2133 | context.cursor + bild.zielY, |
| 2134 | bild.zielBreite, |
| 2135 | bild.zielHoehe, |
| 2136 | ); |
| 2137 | } |
| 2138 | |
| 2139 | const surface = new PdfSurface( |
| 2140 | doc, |
| 2141 | PAGE_MARGIN, |
| 2142 | context.cursor, |
| 2143 | context.contentWidth, |
| 2144 | blatt.hoehe, |
| 2145 | ); |
| 2146 | drawLageplanblatt(surface, blatt, optionen); |
| 2147 | context.cursor += blatt.hoehe + 10; |
| 2148 | } |
| 2149 | |
| 2150 | /** Wie der Massstab zustande kam - in einem Satz fuer den Ausdruck. */ |
| 2151 | function massstabsherkunft(grundlage: Planungsgrundlage): string { |
| 2152 | switch (grundlage.massstabHerkunft) { |
| 2153 | case 'georeferenziert': |
| 2154 | return 'aus dem Kartendienst übernommen (georeferenzierter Ausschnitt, nicht nachgemessen)'; |
| 2155 | case 'gemessen': |
| 2156 | return ( |
| 2157 | 'an einer Strecke bekannter Länge abgegriffen: ' + |
| 2158 | `${fmt.meters(grundlage.bezugslaengeMeter ?? 0, 1)} auf ` + |
| 2159 | `${fmt.numShort(grundlage.bezugslaengeBildpunkte ?? 0, 0)} Bildpunkten` |
| 2160 | ); |
| 2161 | case null: |
| 2162 | return 'nicht festgelegt'; |
| 2163 | } |
| 2164 | } |
| 2165 | |
| 2166 | // --- Tabellen --------------------------------------------------------------- |
| 2167 | |
| 2168 | function drawSignalGroupTable(context: Context): void { |
| 2169 | const { doc, project, plan } = context; |
| 2170 | |
| 2171 | autoTable(doc, { |
| 2172 | startY: context.cursor, |
| 2173 | margin: TABELLENRAND, |
| 2174 | theme: 'grid', |
| 2175 | styles: { fontSize: 8.5, cellPadding: 3 }, |
| 2176 | headStyles: { fillColor: '#eef1f6', textColor: '#1b2230', fontStyle: 'bold' }, |
| 2177 | head: [ |
| 2178 | [ |
| 2179 | /* |
| 2180 | * DIE ORDNUNGSNUMMER: Traegt die Zwischenzeitenmatrix Nummern in den |
| 2181 | * Spaltenkoepfen - was sie bei langen Bezeichnungen tut -, verweist |
| 2182 | * der Absatz darueber auf "dieselbe Reihenfolge wie in der Tabelle |
| 2183 | * Signalgruppen". Ohne diese Spalte war die Aussage zwar zutreffend, |
| 2184 | * aber nur durch Abzaehlen von Zeilen nachpruefbar; mit ihr steht die |
| 2185 | * Nummer da, auf die verwiesen wird. |
| 2186 | * |
| 2187 | * Sie ist keine Eigenschaft der Signalgruppe, sondern ihre Stellung |
| 2188 | * in DIESER Unterlage - deshalb wird sie hier gezaehlt und nicht am |
| 2189 | * Modell gefuehrt. |
| 2190 | */ |
| 2191 | 'Nr.', |
| 2192 | 'SG', |
| 2193 | 'Verkehrsart', |
| 2194 | 'Fahrbeziehung', |
| 2195 | 'V zul', |
| 2196 | 'Fahrstr.', |
| 2197 | 'Fahrzeugart', |
| 2198 | // Merkmale der Fussgaengerfurt, die in die Mindestfreigabezeit eingehen |
| 2199 | // (Befund C1): Zusatzeinrichtung fuer Blinde und Sehbehinderte (ganze |
| 2200 | // statt halbe Furt) und erhoehter Zeitbedarf (1,0 statt 1,2 m/s). Ohne |
| 2201 | // die Spalte stuende "6 s" links in "Min./Vorgabe" neben einem |
| 2202 | // Regelwert von 5 s, und niemand saehe, woher die Sekunde kommt. |
| 2203 | 'Furt', |
| 2204 | 'Rot-Gelb', |
| 2205 | 'Gelb', |
| 2206 | // MINDESTFREIGABEZEIT UND VORGABE IN EINER SPALTE: Seit der |
| 2207 | // Fassung 5.10.0 setzt die Freigabezeitverteilung fuer eine Phase die |
| 2208 | // kleinste Hoechstfreigabezeit ihrer Signalgruppen an statt des |
| 2209 | // Regelwerts. Stand sie nirgends, liess sich die gedruckte Phasendauer |
| 2210 | // aus der Unterlage nicht mehr herleiten. |
| 2211 | // |
| 2212 | // RECHTS STEHT DIE VORGABE, NICHT DIE GELTENDE SCHRANKE: Was gilt, |
| 2213 | // entscheidet `phaseMaxGreen` je PHASE; ohne Eintrag steht "–". Der |
| 2214 | // Absatz unter der Tabelle sagt es, und der Spaltenkopf sagt es so |
| 2215 | // weit, wie seine Breite reicht - siehe `hoechstfreigabezeitVorgabe` in |
| 2216 | // csv.ts. |
| 2217 | // |
| 2218 | // WARUM EINE SPALTE UND WARUM SO KURZ BENANNT: Diese Tabelle ist quer |
| 2219 | // voll. Die Breite, die ihre dreizehn Spalten brauchen, damit keine |
| 2220 | // Zelle umbricht, ist bis auf ein Zehntel dieselbe, die der |
| 2221 | // Satzspiegel hergibt (nachgemessen mit doc.getTextWidth: 770,9 pt |
| 2222 | // gegen 769,9 pt). Eine vierzehnte Spalte oder ein laengerer Kopf |
| 2223 | // nimmt den Nachbarn die Breite: Signalgruppennamen und Fahrzeugarten |
| 2224 | // brechen um, die Zeilen werden hoeher, die Tabelle bricht frueher auf |
| 2225 | // das naechste Blatt um - dann steht in der Spalte "Nr." NaN statt der |
| 2226 | // Nummer (nachgestellt mit einer eigenen Spalte "Max. Freigabe": |
| 2227 | // tests/export/pdfSignalgruppenNummer.test.ts faellt mit 3 Faellen, |
| 2228 | // waehrend pdfBeschriftungen.test.ts und pdfSatzspiegel.test.ts gruen |
| 2229 | // bleiben). Gemessen mit Zellenabstand: "Min./Max." 44,2 pt, |
| 2230 | // "Min./Vorgabe" 58,4 pt - das traegt; "Min./max. Vorg." 67,3 pt |
| 2231 | // traegt nicht mehr. Die Zelle darunter nennt die Einheit; die |
| 2232 | // Tabellenausgabe fuehrt beide Angaben als eigene Spalten mit vollem |
| 2233 | // Namen - sie wird gerechnet, diese hier gelesen. |
| 2234 | 'Min./Vorgabe', |
| 2235 | // "(Summe)" wie in der Signalzeitentabelle (Befund C22): Die Zelle |
| 2236 | // nennt `totalGreen`, also die Summe aller Freigabefenster. Die |
| 2237 | // Fussnote dazu steht auf einer anderen Seite; ohne den Zusatz liest |
| 2238 | // sich die Spalte an einer Gruppe mit zwei Fenstern wie die Dauer eines |
| 2239 | // einzelnen. |
| 2240 | 'Freigabe im Plan (Summe)', |
| 2241 | 'Verkehrsstärke', |
| 2242 | ].map(toWinAnsi), |
| 2243 | ], |
| 2244 | body: project.signalGroups.map((group, index) => { |
| 2245 | const planned = plan.groups.find((g) => g.groupId === group.id); |
| 2246 | const demand = project.demands.find((d) => d.signalGroupId === group.id); |
| 2247 | const maxVorgabe = hoechstfreigabezeitVorgabe(group); |
| 2248 | return [ |
| 2249 | String(index + 1), |
| 2250 | group.name, |
| 2251 | TRAFFIC_MODE_LABELS[group.mode], |
| 2252 | // Die Schluessel selbst gehoeren nicht in die Unterlage: "keine" liest |
| 2253 | // sich in einer Tabellenzelle wie eine fehlende Angabe, gemeint ist |
| 2254 | // "ohne Laengenzuschlag". Der Anwender hat die Werte in der Ansicht |
| 2255 | // "Signalgruppen" aus einer Auswahlliste mit ebendiesen Beschriftungen |
| 2256 | // gewaehlt und muss sie im Ausdruck wiederfinden. |
| 2257 | MOVEMENT_LABELS[group.movement], |
| 2258 | `${fmt.numShort(group.vZul)} km/h`, |
| 2259 | // Ueber fmt und nicht ueber String (Fassung 5.10.0): `num` in schema.ts |
| 2260 | // prueft die Fahrstreifenzahl nur gegen 1 bis 8 und rundet nicht auf |
| 2261 | // ganze Zahlen. Eine Projektdatei mit "lanes": 2.5 wird uebernommen, |
| 2262 | // geht mit 2,5 in die Saettigungsverkehrsstaerke ein - und stand hier |
| 2263 | // als "2.5" mit englischem Punkt, als einzige Zahl der Unterlage, die |
| 2264 | // nicht ueber fmt lief. In der Tabellenausgabe ist dieselbe Zelle |
| 2265 | // bereits nachgezogen (csv.ts). Bei ganzen Zahlen aendert sich nichts: |
| 2266 | // numShort laesst nachlaufende Nullen weg. |
| 2267 | fmt.numShort(group.lanes), |
| 2268 | fahrzeugartBeschriftung(group.vehicleClass, plan.defaults), |
| 2269 | furtmerkmale(group), |
| 2270 | fmt.seconds(planned?.times.redYellow ?? 0), |
| 2271 | fmt.seconds(planned?.times.yellow ?? 0), |
| 2272 | // Die Vorgabe steht auch ohne `planned`: Sie ist eine Eingangsgroesse |
| 2273 | // der Gruppe und kein Ergebnis des Plans. "–" heisst "keine |
| 2274 | // eingetragen"; was dann gilt, sagt der Absatz unter der Tabelle. |
| 2275 | `${fmt.seconds(planned?.times.minGreen ?? 0)} / ` + |
| 2276 | (maxVorgabe === null ? '–' : fmt.seconds(maxVorgabe)), |
| 2277 | planned === undefined || planned.greens.length === 0 |
| 2278 | ? '–' |
| 2279 | : fmt.seconds(planned.totalGreen), |
| 2280 | // Fuer Fussgaenger und Radverkehr auch dann "–", wenn eine Zahl |
| 2281 | // gespeichert ist (Altdatei aus der Zeit vor Fassung 5.4.0, |
| 2282 | // Befund B4): Sie geht in keine Rechnung ein, und der |
| 2283 | // Pruefbericht sagt das. Eine gedruckte Verkehrsstaerke behauptete |
| 2284 | // eine Eingangsgroesse, die es nicht ist. |
| 2285 | // |
| 2286 | // UND "–" AUCH BEI 0 (Fassung 5.10.0): Ein Datensatz ist noch keine |
| 2287 | // Zaehlung. Wird in der Signalgruppentabelle allein der |
| 2288 | // Schwerverkehrsanteil ausgefuellt, legt die Oberflaeche einen |
| 2289 | // Datensatz mit `volume: 0` an; der Fachkern liest ihn seit den |
| 2290 | // Befunden 12 und 18 als "keine Angabe" (signalPlan.ts, |
| 2291 | // erfassteVerkehrsstaerke), und der Bildschirm zeichnet das Feld seit |
| 2292 | // Befund 58 leer. Hier stand daneben "0 Fz/h" - dieselbe Zeile, zwei |
| 2293 | // Aussagen. Dieselbe Bedingung wie in `schwerverkehrZelle`. |
| 2294 | demand === undefined || demand.volume <= 0 || !hatSaettigungsverkehrsstaerke(group.mode) |
| 2295 | ? '–' |
| 2296 | : fmt.vehiclesPerHour(demand.volume), |
| 2297 | ].map(toWinAnsi); |
| 2298 | }), |
| 2299 | }); |
| 2300 | |
| 2301 | context.cursor = lastTableBottom(doc) + 10; |
| 2302 | |
| 2303 | // Rechenweg der Furtregel unter der Tabelle: Eine Mindestfreigabezeit ueber |
| 2304 | // dem Regelwert muss sich aus gedruckten Groessen nachrechnen lassen. Ohne |
| 2305 | // den Absatz waere "6 s" eine Behauptung. |
| 2306 | const furtregel = furtregelAbsatz(plan); |
| 2307 | if (furtregel !== null) writeParagraph(context, furtregel); |
| 2308 | |
| 2309 | /* |
| 2310 | * Was in der Spalte "Min./Vorgabe" rechts steht - und was dort NICHT steht. |
| 2311 | * |
| 2312 | * Der Absatz steht unbedingt da, weil die Spalte unbedingt da steht |
| 2313 | * (dieselbe Begruendung wie beim Absatz zur Spalte "Enger Innenradius"): |
| 2314 | * Ein Kopf "Min./Vorgabe" ohne Erlaeuterung liesse offen, wovon die zweite |
| 2315 | * Zahl die Vorgabe ist, und "–" liesse offen, was ohne Eintrag gilt. |
| 2316 | * |
| 2317 | * Er sagt nur zu, was das Programm haelt: Gemeldet werden die beiden Faelle, |
| 2318 | * in denen `phaseMaxGreen` eine Vorgabe IN IHRER PHASE verwirft. Eine |
| 2319 | * Vorgabe ueber einer festen Freigabezeit kommt ebenfalls nicht zur Wirkung |
| 2320 | * und wird nicht gemeldet; eine Gruppe ohne Phase erreicht die Stelle gar |
| 2321 | * nicht. Beides ist hier deshalb auch nicht zugesagt - der Satz nennt die |
| 2322 | * Phase ausdruecklich. |
| 2323 | */ |
| 2324 | writeParagraph( |
| 2325 | context, |
| 2326 | 'Die Spalte "Min./Vorgabe" nennt links die maßgebende Mindestfreigabezeit und rechts die an ' + |
| 2327 | 'der Signalgruppe eingetragene Höchstfreigabezeit; "–" heißt: an dieser Signalgruppe ist ' + |
| 2328 | 'keine eingetragen. Die rechte Zahl ist eine Eingangsgröße und nicht die im Plan wirksame ' + |
| 2329 | 'obere Schranke: Wirksam wird sie je Phase, und zwar als kleinste Vorgabe ihrer ' + |
| 2330 | `Signalgruppen. Trägt keine von ihnen eine, gilt der Regelwert von ` + |
| 2331 | `${fmt.seconds(plan.defaults.maxGreen)} aus den Projektvorgaben; eine feste Freigabezeit ` + |
| 2332 | 'der Phase und die Mindestfreigabezeit gehen der Vorgabe ohnehin vor. Eine Vorgabe, die in ' + |
| 2333 | 'ihrer Phase unter der Mindestfreigabezeit oder unter einer festen Freigabezeit liegt, ' + |
| 2334 | 'meldet der Prüfbericht.', |
| 2335 | ); |
| 2336 | context.cursor += 6; |
| 2337 | } |
| 2338 | |
| 2339 | /** |
| 2340 | * Merkmale einer Fussgaengerfurt fuer die Signalgruppentabelle - Beschriftung |
| 2341 | * wie am Bildschirm ("Blindenzusatz", "erhoehter Zeitbedarf"). Fuer andere |
| 2342 | * Verkehrsarten "–": Die Merkmale gibt es dort nicht. |
| 2343 | */ |
| 2344 | export function furtmerkmale(group: SignalGroup): string { |
| 2345 | if (group.mode !== 'fuss') return '–'; |
| 2346 | const merkmale: string[] = []; |
| 2347 | if (group.blindenzusatz === true) merkmale.push('Blindenzusatz (ganze Furt)'); |
| 2348 | if (group.reducedMobility) merkmale.push('erhöhter Zeitbedarf'); |
| 2349 | return merkmale.length === 0 ? 'halbe Furt' : merkmale.join(', '); |
| 2350 | } |
| 2351 | |
| 2352 | /** |
| 2353 | * Absatz zur Mindestfreigabezeit der Fussgaengergruppen: je Gruppe der |
| 2354 | * Rechenweg der Furtregel (Furtlaenge, halbe/ganze Furt, vr, Ergebnis) oder der |
| 2355 | * Vermerk, dass die Furtlaenge unbekannt ist. `null`, wenn es keine |
| 2356 | * Fussgaengergruppe gibt - dann gibt es auch nichts zu erklaeren. |
| 2357 | */ |
| 2358 | export function furtregelAbsatz(plan: SignalPlan): string | null { |
| 2359 | const saetze: string[] = []; |
| 2360 | for (const g of plan.groups) { |
| 2361 | if (g.mode !== 'fuss') continue; |
| 2362 | const mfz = g.mindestfreigabezeit; |
| 2363 | if (mfz.furt === null) { |
| 2364 | saetze.push( |
| 2365 | `${g.name}: Furtlänge unbekannt (kein Räumweg an einer Konfliktbeziehung, in der die Gruppe ` + |
| 2366 | `räumt) – Furtregel nicht prüfbar, es gilt der Regelwert von ${fmt.seconds(mfz.regelwert)}.`, |
| 2367 | ); |
| 2368 | continue; |
| 2369 | } |
| 2370 | const furt = mfz.furt; |
| 2371 | const anteil = furt.ganzeFurt ? 'ganze Furt (Blindenzusatz)' : 'halbe Furt'; |
| 2372 | // Gerundet im Fachkern (furtFreigabebedarf), nicht hier noch einmal. |
| 2373 | const bedarf = furt.zeitbedarfGerundet; |
| 2374 | const ergebnis = |
| 2375 | mfz.massgebend === 'furt' |
| 2376 | ? `maßgebend ${fmt.seconds(mfz.wert)}` |
| 2377 | : mfz.massgebend === 'vorgabe' |
| 2378 | ? `maßgebend die Vorgabe von ${fmt.seconds(mfz.wert)}` |
| 2379 | : `maßgebend der Regelwert von ${fmt.seconds(mfz.wert)}`; |
| 2380 | // Furtweg mit zwei, Zeitbedarf mit drei Nachkommastellen - dieselbe |
| 2381 | // Genauigkeit wie im Hinweis des Plans (signalPlan.ts, furtNotizen): |
| 2382 | // "6,01 m / 1,2 m/s = 5,004 s, aufgerundet 6 s" ist nachrechenbar, |
| 2383 | // "6,0 m / 1,2 m/s = 5,00 s, aufgerundet 6 s" nicht. |
| 2384 | saetze.push( |
| 2385 | `${g.name}: Furt ${fmt.meters(furt.furtlaenge, 2)}, ${anteil} ${fmt.meters(furt.furtweg, 2)} / ` + |
| 2386 | `${fmt.metersPerSecond(furt.vr)} = ${fmt.seconds(furt.zeitbedarf, 3)}, aufgerundet ` + |
| 2387 | `${fmt.seconds(bedarf)}; ${ergebnis}.`, |
| 2388 | ); |
| 2389 | } |
| 2390 | if (saetze.length === 0) return null; |
| 2391 | return ( |
| 2392 | 'Mindestfreigabezeit der Fußgängergruppen nach der Furtregel der RiLSA (Freigabe mindestens für ' + |
| 2393 | 'die halbe Furt, mit Zusatzeinrichtung für Blinde und Sehbehinderte für die ganze Furt; ' + |
| 2394 | 'Furtlänge = längster erfasster Räumweg der Gruppe, vr = Räumgeschwindigkeit der Gruppe; ' + |
| 2395 | 'der größere Wert aus Regelwert, Vorgabe und Furtbedarf gilt): ' + |
| 2396 | saetze.join(' ') + |
| 2397 | ' ' + |
| 2398 | MITTELINSEL_VERMERK |
| 2399 | ); |
| 2400 | } |
| 2401 | |
| 2402 | /** |
| 2403 | * Was der Ausdruck ueber Furten mit Mittelinsel sagen muss. |
| 2404 | * |
| 2405 | * NEU (Befund D2): Das Programm kennt keine zweistufige Furt. Es ist eine |
| 2406 | * Einschraenkung des Leistungsumfangs, und eine Planunterlage, die sie |
| 2407 | * verschweigt, laesst den Pruefer glauben, die Zweistufigkeit sei geprueft |
| 2408 | * worden. Der Vermerk steht dort, wo die Fuss-Kennwerte stehen (unter der |
| 2409 | * Signalgruppentabelle), nicht in einer allgemeinen Vorbemerkung. Kein |
| 2410 | * Rechenwechsel: Es aendert sich nichts an den Zahlen, nur daran, was ueber sie |
| 2411 | * behauptet wird. |
| 2412 | * |
| 2413 | * BERICHTIGT 5.38.0, NACHGEBESSERT 5.38.1 - EINE ZUSICHERUNG UEBER EINE |
| 2414 | * EINGABE IST KEINE ZUSICHERUNG. |
| 2415 | * Hier stand: "Furten mit Mittelinsel gehen mit ihrer ganzen Laenge in diese |
| 2416 | * Rechnung ein ... konservativ, weil Raeumzeit und Furtbedarf ueber die volle |
| 2417 | * Breite angesetzt werden." Das behauptet etwas ueber DIESEN Plan, das dieser |
| 2418 | * Plan nicht wissen kann: Gerechnet wird mit dem je Konfliktbeziehung |
| 2419 | * ERFASSTEN Raeumweg, und ob der bis zur gegenueberliegenden Bordkante reicht |
| 2420 | * oder an der Insel endet, steht nirgends. Wer die Furt nur bis zur Insel |
| 2421 | * vermisst, bekam die kuerzeren Zeiten UND einen gedruckten Satz, der sie |
| 2422 | * konservativ nannte - in einer Unterlage, die zur Anordnung nach § 45 StVO |
| 2423 | * geht. Der Vermerk sagt jetzt, WOMIT gerechnet wurde, und verweist auf die |
| 2424 | * Furtlaenge, die derselbe Absatz zwei Zeilen darueber je Gruppe nennt. |
| 2425 | * |
| 2426 | * DREI NACHBESSERUNGEN IN 5.38.1. Erstens sagte der Vermerk, die Furtlaenge |
| 2427 | * stehe "oben bei jeder Gruppe" - fuer eine Gruppe ohne erfassten Raeumweg |
| 2428 | * druckt derselbe Absatz zwei Zeilen darueber aber "Furtlaenge unbekannt". Eine |
| 2429 | * unbelegbare Zusicherung war durch eine zweite ersetzt worden. Zweitens fiel |
| 2430 | * die RAEUMZEIT aus dem Text: Der alte Wortlaut nannte "Raeumzeit und |
| 2431 | * Furtbedarf", der neue verband den erfassten Weg nur noch mit der |
| 2432 | * Mindestfreigabezeit - dabei traegt derselbe halbierte Weg auch jede |
| 2433 | * Zwischenzeit dieser Gruppe. Drittens fehlte die RICHTUNG: Wer nur das PDF vor |
| 2434 | * sich hat, konnte nicht mehr entnehmen, ob die Vereinfachung zu seinen Gunsten |
| 2435 | * oder zu seinen Lasten geht. Sie steht jetzt wieder da, aber an die Bedingung |
| 2436 | * geknuepft, unter der sie gilt. |
| 2437 | */ |
| 2438 | export const MITTELINSEL_VERMERK = |
| 2439 | 'Eine progressive (zweistufige) Signalisierung der Furtabschnitte mit der Insel als ' + |
| 2440 | 'Aufstellfläche bildet das Programm nicht ab und weist sie nicht nach: Es kennt keine Furt als ' + |
| 2441 | 'Gebilde und rechnet je Signalgruppe, mit dem an der jeweiligen Konfliktbeziehung erfassten ' + |
| 2442 | 'Räumweg. Aus dessen größtem Wert entsteht die Furtlänge; welche das je Gruppe ist – oder dass ' + |
| 2443 | 'sie unbekannt bleibt –, sagen die Sätze davor. Ob der erfasste Weg von Bordkante zu Bordkante ' + |
| 2444 | 'reicht, kann dieser Plan nicht feststellen: Eine Furt mit Mittelinsel ist über die Insel ' + |
| 2445 | 'hinweg zu vermessen. Geschieht das, liegen Räumzeit, Zwischenzeit und Furtbedarf auf der ' + |
| 2446 | 'sicheren Seite; wird nur bis zur Insel gemessen, fallen alle drei zu kurz aus.'; |
| 2447 | |
| 2448 | /* -- Zwischenzeitenmatrix: Satzmasse --------------------------------------- |
| 2449 | * |
| 2450 | * WARUM DIESE MASSE UEBERHAUPT FESTGELEGT WERDEN. |
| 2451 | * |
| 2452 | * jspdf-autotable verteilt ohne feste Breite die Satzbreite ANTEILIG auf alle |
| 2453 | * Spalten. Die Matrix hat n+1 Spalten, also faellt die Breite jeder Spalte mit |
| 2454 | * wachsendem n - und mit ihr die Zahl der Zeichen, die in eine Kopfzelle |
| 2455 | * passen. Der Umbruch geht dann nicht mehr an Wortgrenzen, sondern durch die |
| 2456 | * Woerter hindurch; die Kopfzeile wird bei langen Namen hoeher als das Blatt, |
| 2457 | * und autotable zerlegt sie in printFullRow in immer neue Bruchstuecke. |
| 2458 | * Gemessen an einem Knotenpunkt mit vollstaendig besetzter Konfliktmatrix und |
| 2459 | * Namen von rund 140 Zeichen: bei 32 Gruppen 1,3 s / 1 406 kB / 113 Seiten, |
| 2460 | * bei 40 Gruppen ABBRUCH. Der Ausdruck ist dort nicht langsam, sondern nicht |
| 2461 | * mehr herstellbar. |
| 2462 | * |
| 2463 | * WIE GENAU ER ABBRICHT, HAENGT AM FALL - der Kommentar nannte frueher nur den |
| 2464 | * Aufrufstapel, belegbar sind zwei Abbrucharten: 40 Gruppen mit |
| 2465 | * 140-Zeichen-Namen sterben an der Halde ("Ineffective mark-compacts near heap |
| 2466 | * limit", rund 4 GB, nach 71 s), 16 Gruppen mit 485-Zeichen-Namen am |
| 2467 | * Aufrufstapel ("Maximum call stack size exceeded", nach 42,5 s bei 2 791 MB). |
| 2468 | * Beidem geht dieselbe Meldung von autotable voraus: "Will not be able to print |
| 2469 | * row -1 correctly since it's minimum height is larger than page height". Fuer |
| 2470 | * die Sache ist der Unterschied gleichgueltig - fuer eine Fehlersuche nicht, |
| 2471 | * deshalb steht er hier. |
| 2472 | * |
| 2473 | * Die Masse unten schneiden das ab, indem sie die Spaltenbreite von n |
| 2474 | * ABKOPPELN. Was dann nicht mehr nebeneinander passt, steht in einem weiteren |
| 2475 | * Ausschnitt - nicht in einer schmaleren Spalte. |
| 2476 | */ |
| 2477 | |
| 2478 | /** Schriftgrad der Matrix - wie bisher. */ |
| 2479 | const MATRIX_SCHRIFTGRAD = 8.5; |
| 2480 | /** Zellpolsterung der Matrix - wie bisher; sie faellt zweimal je Spalte an. */ |
| 2481 | const MATRIX_POLSTER = 3; |
| 2482 | /** Zeilenabstand, den jspdf-autotable ansetzt (FONT_ROW_RATIO). */ |
| 2483 | const MATRIX_ZEILENFAKTOR = 1.15; |
| 2484 | /** Beschriftung der Kopfecke - Leserichtung der Matrix. */ |
| 2485 | const MATRIX_ECKE = 'räumt \\ fährt ein'; |
| 2486 | |
| 2487 | /** |
| 2488 | * Bis zu welcher Breite ein Signalgruppenname noch als SPALTENKOPF taugt. |
| 2489 | * |
| 2490 | * 34 pt tragen bei 8,5 pt Helvetica fett sechs Ziffernzeichen: "K12345" misst |
| 2491 | * 29,49 pt, "K123456" schon 34,17 pt und faellt heraus (nachgemessen; die |
| 2492 | * frueher hier stehende Angabe "rund acht Zeichen" war zu grosszuegig, |
| 2493 | * berichtigt mit Fassung 5.8.0). Damit deckt der Wert genau die Bezeichnung ab, |
| 2494 | * die das Modell erwartet (SignalGroup.name: "Bezeichnung im Plan, z. B. K1, |
| 2495 | * F2, R3"). Wer stattdessen einen Satz in das Namensfeld schreibt, bekommt im |
| 2496 | * Kopf die Ordnungsnummer und darueber die Zuordnung; erfunden wird dabei |
| 2497 | * nichts (siehe matrixSatz). |
| 2498 | * |
| 2499 | * DASS DIE GRENZE FRUEH LIEGT, IST BEABSICHTIGT, aber es ist keine Kleinigkeit: |
| 2500 | * Schon "Fussgaenger12" (13 Zeichen, 61,97 pt fett) faellt darunter. Solche |
| 2501 | * Plaene bekommen den Nummernsatz, obwohl ihre Namen vorher sauber an |
| 2502 | * Wortgrenzen umbrachen. Das ist vertretbar, weil das Banner die Zuordnung auf |
| 2503 | * jedem Blatt mitfuehrt - aber es ist mehr als "wer kurze Bezeichnungen |
| 2504 | * benutzt, sieht keine Aenderung" (Befund B6 fachlich), und deshalb steht es |
| 2505 | * hier und in den Unterlagen. |
| 2506 | * |
| 2507 | * Der Wert ist bewusst etwas groesser als die breiteste Zahlenspalte |
| 2508 | * ("188*" = 17,34 pt zuzueglich 2 x 3 pt Polster = 23,34 pt): Eine |
| 2509 | * Bezeichnung, die in eine Zahlenspalte passt, kostet keine einzige zusaetzliche |
| 2510 | * Spaltenbreite. |
| 2511 | */ |
| 2512 | const MATRIX_KOPFNAME_HOECHSTBREITE = 34; |
| 2513 | |
| 2514 | /** Hoechstbreite der Namensspalte (Zeilenkopf) - der Rest gehoert den Zahlen. */ |
| 2515 | const MATRIX_NAMENSSPALTE_HOECHSTBREITE = 180; |
| 2516 | |
| 2517 | /** |
| 2518 | * Die Breite, unter die jspdf-autotable eine Spalte nie verkleinert. |
| 2519 | * |
| 2520 | * In calculateWidths ist das `defaultMinWidth = 10 / scaleFactor`; die Einheit |
| 2521 | * dieser Ausgabe ist Punkt, also scaleFactor 1. Der Wert gehoert in die |
| 2522 | * Schranke, weil eine Spalte mit sehr schmalem Inhalt trotzdem diese Breite |
| 2523 | * belegt und den anderen Spalten damit Platz nimmt. |
| 2524 | */ |
| 2525 | const MATRIX_MINDESTBREITE = 10; |
| 2526 | |
| 2527 | /** |
| 2528 | * Textbreite in der Schrift der Matrix, ohne den Zustand des Dokuments zu |
| 2529 | * hinterlassen: Die Funktion wird zwischen Absaetzen aufgerufen, die ihre |
| 2530 | * eigene Schrift gesetzt haben. |
| 2531 | */ |
| 2532 | function matrixBreite(doc: jsPDF, text: string, fett: boolean): number { |
| 2533 | const schrift = doc.getFont() as { fontName: string; fontStyle: string }; |
| 2534 | const grad = doc.getFontSize(); |
| 2535 | doc.setFont('helvetica', fett ? 'bold' : 'normal'); |
| 2536 | doc.setFontSize(MATRIX_SCHRIFTGRAD); |
| 2537 | const breite = doc.getTextWidth(toWinAnsi(text)); |
| 2538 | doc.setFont(schrift.fontName, schrift.fontStyle); |
| 2539 | doc.setFontSize(grad); |
| 2540 | return breite; |
| 2541 | } |
| 2542 | |
| 2543 | /** Zeilenzahl, die ein Text in einer Zelle dieser Breite belegt. */ |
| 2544 | function matrixZeilen(doc: jsPDF, text: string, breite: number, fett: boolean): number { |
| 2545 | const schrift = doc.getFont() as { fontName: string; fontStyle: string }; |
| 2546 | const grad = doc.getFontSize(); |
| 2547 | doc.setFont('helvetica', fett ? 'bold' : 'normal'); |
| 2548 | doc.setFontSize(MATRIX_SCHRIFTGRAD); |
| 2549 | const zeilen = doc.splitTextToSize(toWinAnsi(text), Math.max(1, breite)) as string[]; |
| 2550 | doc.setFont(schrift.fontName, schrift.fontStyle); |
| 2551 | doc.setFontSize(grad); |
| 2552 | return Math.max(1, zeilen.length); |
| 2553 | } |
| 2554 | |
| 2555 | /** Hoehe einer Zelle mit so vielen Zeilen - dieselbe Rechnung wie autotable. */ |
| 2556 | function matrixZellhoehe(zeilen: number): number { |
| 2557 | return zeilen * MATRIX_SCHRIFTGRAD * MATRIX_ZEILENFAKTOR + 2 * MATRIX_POLSTER; |
| 2558 | } |
| 2559 | |
| 2560 | /** Der Inhalt einer Zelle der Matrix - eine Stelle fuer beide Satzarten. */ |
| 2561 | function matrixWert(plan: SignalPlan, from: SignalGroup, to: SignalGroup): string { |
| 2562 | if (from.id === to.id) return ''; |
| 2563 | const resolved = plan.intergreens.get(intergreenKey(from.id, to.id)); |
| 2564 | if (!resolved) return '·'; |
| 2565 | return resolved.source === 'vorgegeben' |
| 2566 | ? `${fmt.numShort(resolved.value, 0)}*` |
| 2567 | : fmt.numShort(resolved.value, 0); |
| 2568 | } |
| 2569 | |
| 2570 | /** |
| 2571 | * Das Groesste ueber die Zellen der ganzen Matrix - ueber eine Faltung, NICHT |
| 2572 | * ueber den Ausbreitungsoperator. |
| 2573 | * |
| 2574 | * Beide Satzarten bildeten das Maximum vor der Fassung 5.11.0 als |
| 2575 | * `Math.max(0, ...zellen.flat().map(...))`. Die Matrix hat n^2 Zellen; V8 nimmt |
| 2576 | * nur rund 125.000 Argumente an, und ab 356 Signalgruppen (356^2 = 126.736) |
| 2577 | * brach der Aufruf mit "RangeError: Maximum call stack size exceeded" ab - |
| 2578 | * ohne Konfliktbeziehung, ohne Fehler im Pruefbericht und ohne dass eine |
| 2579 | * Unterlage entstand. Weder `parseSignalGroups` noch `addSignalGroup` kennen |
| 2580 | * eine Obergrenze; wer solche Anlagen annimmt, muss sie auch drucken. |
| 2581 | * |
| 2582 | * Der Wert ist derselbe wie zuvor - nur der Rechenweg dorthin fasst die Liste |
| 2583 | * nicht mehr als Argumentliste an. |
| 2584 | */ |
| 2585 | function matrixHoechsteZellbreite( |
| 2586 | zellen: readonly (readonly string[])[], |
| 2587 | breite: (zelle: string) => number, |
| 2588 | ): number { |
| 2589 | let hoechste = 0; |
| 2590 | for (const zeile of zellen) { |
| 2591 | for (const zelle of zeile) hoechste = Math.max(hoechste, breite(zelle)); |
| 2592 | } |
| 2593 | return hoechste; |
| 2594 | } |
| 2595 | |
| 2596 | /** Die laengsten Woerter eines Textes - sie bestimmen, ob er umbrechbar ist. */ |
| 2597 | function laengstesWort(doc: jsPDF, text: string, fett: boolean): number { |
| 2598 | const woerter = text.split(/\s+/).filter((w) => w !== ''); |
| 2599 | if (woerter.length === 0) return 0; |
| 2600 | return Math.max(...woerter.map((w) => matrixBreite(doc, w, fett))); |
| 2601 | } |
| 2602 | |
| 2603 | /** Was in der Zuordnung steht, wenn eine Signalgruppe keinen Namen traegt. */ |
| 2604 | const MATRIX_OHNE_NAMEN = '(ohne Bezeichnung)'; |
| 2605 | |
| 2606 | /** |
| 2607 | * Was im Banner steht, wenn die Zuordnung selbst hoeher ist als ein Blatt. |
| 2608 | * |
| 2609 | * Sie wird dann nicht gekuerzt, sondern verlaesst den wiederholten |
| 2610 | * Tabellenkopf. Der Banner sagt, wo sie steht - sonst suchte der Leser eines |
| 2611 | * einzelnen Blattes sie dort, wo sie sonst immer ist. |
| 2612 | */ |
| 2613 | const MATRIX_ZUORDNUNG_VOR_TABELLE = |
| 2614 | 'Die Zuordnung der Nummern zu den Namen steht unmittelbar vor dieser Tabelle; sie ist länger ' + |
| 2615 | 'als ein Blatt und lässt sich deshalb nicht auf jedem Blatt wiederholen.'; |
| 2616 | |
| 2617 | /** |
| 2618 | * Der Name einer Signalgruppe fuer Zuordnung und Zeilenkopf - nie leer. |
| 2619 | * |
| 2620 | * Ein vorhandener Name geht UNVERAENDERT durch (auch mit Rand): Diese Funktion |
| 2621 | * soll die Ausgabe nicht schoener machen, sondern nur die Luecke schliessen, in |
| 2622 | * der bisher gar nichts stand. |
| 2623 | */ |
| 2624 | function matrixAnzeigename(group: SignalGroup | undefined): string { |
| 2625 | const name = group?.name ?? ''; |
| 2626 | return name.trim() === '' ? MATRIX_OHNE_NAMEN : name; |
| 2627 | } |
| 2628 | |
| 2629 | /** |
| 2630 | * Taugen die Namen ueberhaupt als Spaltenmarken? |
| 2631 | * |
| 2632 | * Eine Spaltenmarke muss die Signalgruppe EINDEUTIG benennen; sonst laesst sich |
| 2633 | * eine Zahl nicht mehr ihrem Paar zuordnen, und die Matrix ist als Nachweis |
| 2634 | * wertlos. Zwei Faelle nehmen ihr das: |
| 2635 | * |
| 2636 | * - Ein LEERER Name laesst den Kopf unbeschriftet. Sind alle leer, stand die |
| 2637 | * Matrix bisher ganz ohne Beschriftung da - bei 12 Gruppen 132 Zahlen, zu |
| 2638 | * denen die Unterlage nicht sagt, wozu sie gehoeren. |
| 2639 | * - ZWEI GLEICHE Namen setzen zweimal dieselbe Marke ueber zwei verschiedene |
| 2640 | * Spalten. Die Pruefung meldet das als Fehler (signalgruppen.name-doppelt), |
| 2641 | * aber die Unterlage wird trotzdem erzeugt - sie ist ja gerade das, woran |
| 2642 | * der Planer den Fehler sieht. Dann darf sie ihn nicht durch eine |
| 2643 | * mehrdeutige Beschriftung verstecken. |
| 2644 | * |
| 2645 | * Verglichen wird wie in der Pruefung (engine.ts): ohne Rand und ohne |
| 2646 | * Gross-/Kleinschreibung. Sonst nennte die Unterlage zwei Spalten gleich, die |
| 2647 | * die Pruefung als gleich beanstandet - oder umgekehrt. |
| 2648 | * |
| 2649 | * EINEN LEEREN NAMEN MELDET DIE PRUEFUNG INZWISCHEN AUCH: seit 5.8.0 gibt es |
| 2650 | * signalgruppen.name-leer, Schweregrad Fehler wie bei der Doppelung. Die Luecke |
| 2651 | * im Fachkern, von der hier frueher stand, sie bleibe eine, ist geschlossen - |
| 2652 | * die Ursache wird an ihrer Stelle behandelt. |
| 2653 | * |
| 2654 | * DIE VORKEHRUNG HIER BLEIBT TROTZDEM NOETIG, denn buildProjectPdf sperrt sich |
| 2655 | * nicht selbst: Der Pruefbericht wird ihm als INHALT gereicht, nicht als |
| 2656 | * Freigabe, und report.exportBlocked fragt es an keiner Stelle ab. Die Sperre |
| 2657 | * sitzt allein in der Ansicht "Ausgabe" (exportView.ts). Wer an ihr vorbei |
| 2658 | * ausgibt, bekommt die Matrix - und dann muss sie lesbar sein. |
| 2659 | * |
| 2660 | * In beiden Faellen treten Ordnungsnummern an die Stelle der Namen: Die Nummer |
| 2661 | * ist immer da und immer eindeutig, und die Zuordnung ueber der Tabelle nennt |
| 2662 | * den Namen dazu - auch den doppelten, auch den fehlenden. Erfunden wird nichts. |
| 2663 | */ |
| 2664 | /** |
| 2665 | * Warum die Spaltenkoepfe Ordnungsnummern tragen - der KONKRETE Grund. |
| 2666 | * |
| 2667 | * "eine fehlt oder zwei lauten gleich" waere in einer Anordnungsunterlage eine |
| 2668 | * Zumutung: Der Leser muesste selbst nachsehen, was von beidem zutrifft. Der |
| 2669 | * Absatz nennt deshalb den Fall, der wirklich vorliegt - notfalls beide. |
| 2670 | */ |
| 2671 | function matrixKopfgrund(groups: readonly SignalGroup[]): string { |
| 2672 | const leer = groups.some((g) => g.name.trim() === ''); |
| 2673 | const gesehen = new Set<string>(); |
| 2674 | let doppelt = false; |
| 2675 | for (const group of groups) { |
| 2676 | const marke = group.name.trim().toLowerCase(); |
| 2677 | if (marke === '') continue; |
| 2678 | if (gesehen.has(marke)) doppelt = true; |
| 2679 | gesehen.add(marke); |
| 2680 | } |
| 2681 | |
| 2682 | if (leer && doppelt) { |
| 2683 | return ( |
| 2684 | 'weil in diesem Plan mindestens eine Signalgruppe keine Bezeichnung trägt und zwei ' + |
| 2685 | 'weitere dieselbe tragen' |
| 2686 | ); |
| 2687 | } |
| 2688 | if (leer) return 'weil in diesem Plan mindestens eine Signalgruppe keine Bezeichnung trägt'; |
| 2689 | if (doppelt) return 'weil in diesem Plan zwei Signalgruppen dieselbe Bezeichnung tragen'; |
| 2690 | return 'weil die Namen dieses Plans für einen Spaltenkopf zu lang sind'; |
| 2691 | } |
| 2692 | |
| 2693 | function matrixMarkenSindEindeutig(groups: readonly SignalGroup[]): boolean { |
| 2694 | const gesehen = new Set<string>(); |
| 2695 | for (const group of groups) { |
| 2696 | const marke = group.name.trim().toLowerCase(); |
| 2697 | if (marke === '') return false; |
| 2698 | if (gesehen.has(marke)) return false; |
| 2699 | gesehen.add(marke); |
| 2700 | } |
| 2701 | return true; |
| 2702 | } |
| 2703 | |
| 2704 | interface MatrixSatz { |
| 2705 | /** Traegt der Kopf Ordnungsnummern statt der Namen? */ |
| 2706 | readonly mitNummern: boolean; |
| 2707 | /** Breite der Nummernspalte des Zeilenkopfs (0, wenn ohne Nummern). */ |
| 2708 | readonly nummernspalte: number; |
| 2709 | /** Breite der Namensspalte des Zeilenkopfs. */ |
| 2710 | readonly namensspalte: number; |
| 2711 | /** Wie viele einfahrende Signalgruppen ein Ausschnitt hoechstens traegt. */ |
| 2712 | readonly spaltenJeBlock: number; |
| 2713 | /** |
| 2714 | * Wie viele Zeilen der Zeilenkopf hoechstens traegt - laengere Namen werden |
| 2715 | * dort mit Auslassungszeichen gekuerzt. |
| 2716 | */ |
| 2717 | readonly zeilenkopfZeilen: number; |
| 2718 | /** Musste dafuer wirklich gekuerzt werden? Dann sagt es der Ausdruck. */ |
| 2719 | readonly zeilenkopfGekuerzt: boolean; |
| 2720 | /** |
| 2721 | * Steht die Zuordnung der Nummern im wiederholten Tabellenkopf - also auf |
| 2722 | * jedem Blatt des Ausschnitts? |
| 2723 | * |
| 2724 | * Sie steht dort, so lange sie dorthin passt. Ist sie fuer sich schon hoeher |
| 2725 | * als ein Blatt, laesst sie sich nicht wiederholen: Eine Kopfzelle, die |
| 2726 | * hoeher ist als die Seite, bringt jspdf-autotable zum Absturz. Dann steht |
| 2727 | * sie ungekuerzt als Fliesstext vor dem Ausschnitt. |
| 2728 | */ |
| 2729 | readonly zuordnungImKopf: boolean; |
| 2730 | } |
| 2731 | |
| 2732 | /** |
| 2733 | * Aufteilung der Matrix auf Ausschnitte - GLEICHMAESSIG, nicht "voll, voll, |
| 2734 | * Rest". |
| 2735 | * |
| 2736 | * Bei 13 Signalgruppen und 12 moeglichen Spalten je Blatt entstuende sonst ein |
| 2737 | * Ausschnitt mit einer einzigen Spalte; das sieht wie ein Druckfehler aus und |
| 2738 | * laesst den Leser suchen, ob er ein Blatt verloren hat. |
| 2739 | */ |
| 2740 | function matrixAufteilung(anzahl: number, hoechstens: number): number[] { |
| 2741 | const bloecke = Math.max(1, Math.ceil(anzahl / Math.max(1, hoechstens))); |
| 2742 | const grundmass = Math.floor(anzahl / bloecke); |
| 2743 | const rest = anzahl % bloecke; |
| 2744 | return Array.from({ length: bloecke }, (_, i) => grundmass + (i < rest ? 1 : 0)); |
| 2745 | } |
| 2746 | |
| 2747 | /** |
| 2748 | * Satzmass der Matrix: Spaltenbreiten und Zahl der Ausschnitte. |
| 2749 | * |
| 2750 | * Der Kopf traegt die NAMEN, solange sie als Bezeichnung taugen; Ordnungsnummern |
| 2751 | * treten an ihre Stelle, wenn einer breiter ist als |
| 2752 | * MATRIX_KOPFNAME_HOECHSTBREITE ODER wenn die Namen nicht eindeutig sind (leer |
| 2753 | * oder doppelt, siehe matrixMarkenSindEindeutig). Die Nummer ist keine |
| 2754 | * erfundene Kurzbezeichnung, sondern die Stellung der Gruppe in dieser |
| 2755 | * Unterlage: dieselbe Reihenfolge wie in der Tabelle "Signalgruppen" und in den |
| 2756 | * Zeilen dieser Matrix, in denen Nummer und voller Name nebeneinander stehen. |
| 2757 | * Ein ausgedachtes Kuerzel ("K3") koennte dagegen auf den Plannamen "K3" einer |
| 2758 | * ANDEREN Gruppe treffen - in einer Anordnungsunterlage ist das der Fehler, den |
| 2759 | * man nicht machen darf. |
| 2760 | */ |
| 2761 | function matrixSatz( |
| 2762 | context: Context, |
| 2763 | groups: readonly SignalGroup[], |
| 2764 | zellen: readonly (readonly string[])[], |
| 2765 | ): MatrixSatz { |
| 2766 | const { doc } = context; |
| 2767 | |
| 2768 | const breitesteZahl = matrixHoechsteZellbreite(zellen, (z) => matrixBreite(doc, z, false)); |
| 2769 | const breitesterName = Math.max(...groups.map((g) => matrixBreite(doc, g.name, true))); |
| 2770 | const mitNummern = |
| 2771 | breitesterName > MATRIX_KOPFNAME_HOECHSTBREITE || !matrixMarkenSindEindeutig(groups); |
| 2772 | |
| 2773 | const kopfmarke = mitNummern ? matrixBreite(doc, String(groups.length), true) : breitesterName; |
| 2774 | const wertbreite = Math.max(breitesteZahl, kopfmarke) + 2 * MATRIX_POLSTER; |
| 2775 | |
| 2776 | const nummernspalte = mitNummern |
| 2777 | ? Math.max(matrixBreite(doc, 'Nr.', true), matrixBreite(doc, String(groups.length), true)) + |
| 2778 | 2 * MATRIX_POLSTER |
| 2779 | : 0; |
| 2780 | |
| 2781 | // Die Namensspalte traegt den Zeilenkopf und die Kopfecke; sie ist so breit |
| 2782 | // wie noetig, aber nie breiter als MATRIX_NAMENSSPALTE_HOECHSTBREITE - der |
| 2783 | // Rest des Blattes gehoert den Zahlen. |
| 2784 | const eckenbreite = matrixBreite(doc, MATRIX_ECKE, true) + 2 * MATRIX_POLSTER; |
| 2785 | const namensspalte = Math.min( |
| 2786 | MATRIX_NAMENSSPALTE_HOECHSTBREITE, |
| 2787 | Math.max(eckenbreite, breitesterName + 2 * MATRIX_POLSTER), |
| 2788 | ); |
| 2789 | |
| 2790 | const vorspalten = nummernspalte + namensspalte; |
| 2791 | const fuerWerte = context.contentWidth - vorspalten; |
| 2792 | |
| 2793 | /* |
| 2794 | * Zweite Schranke, und die eigentlich wichtige: Kopf UND eine Koerperzeile |
| 2795 | * muessen zusammen auf ein Blatt passen. Genau daran scheitert der heutige |
| 2796 | * Satz - autotable meldet dann "minimum height is larger than page height" |
| 2797 | * und laeuft sich in printFullRow tot. Die Zuordnungsliste im Kopf waechst |
| 2798 | * mit der Zahl der Spalten je Ausschnitt; passt sie nicht mehr, wird der |
| 2799 | * Ausschnitt schmaler, nicht die Zeile hoeher. |
| 2800 | * |
| 2801 | * GEPRUEFT WIRD, WAS GESETZT WIRD (Fassung 5.8.0). Die Probe rechnete bis |
| 2802 | * dahin mit dem ersten Ausschnitt und mit "bloecke = 1". Gesetzt wird aber |
| 2803 | * mit der wirklichen Zahl der Ausschnitte, und deren Banner traegt eine Zeile |
| 2804 | * mehr ("Ausschnitt k von m ...") sowie den laengeren Text "Nummern der |
| 2805 | * Spaltenkoepfe IN DIESEM AUSSCHNITT:". Der gepruefte Kopf war dadurch |
| 2806 | * durchweg 9,78 pt niedriger als der gedruckte - bei n=40 mit langen Namen |
| 2807 | * 211,27 pt statt 221,05 pt. Es blieb in allen gemessenen Faellen Reserve, |
| 2808 | * aber es ist die eine Rechnung, die den Einsturz verhindern soll; sie muss |
| 2809 | * mit dem rechnen, was auf dem Blatt landet. |
| 2810 | */ |
| 2811 | const nutzhoehe = nutzhoeheJeBlatt(context); |
| 2812 | const zeilenkopfNoetig = Math.max( |
| 2813 | ...groups.map((g) => |
| 2814 | matrixZeilen(doc, matrixAnzeigename(g), namensspalte - 2 * MATRIX_POLSTER, false), |
| 2815 | ), |
| 2816 | ); |
| 2817 | let zeilenkopfZeilen = zeilenkopfNoetig; |
| 2818 | let zuordnungImKopf = true; |
| 2819 | |
| 2820 | let spaltenJeBlock = Math.max(1, Math.floor(fuerWerte / wertbreite)); |
| 2821 | while (spaltenJeBlock > 1) { |
| 2822 | const kopfhoehe = matrixHoechsteKopfhoehe(context, groups, spaltenJeBlock, mitNummern, true); |
| 2823 | if (kopfhoehe + matrixZellhoehe(zeilenkopfZeilen) <= nutzhoehe) break; |
| 2824 | spaltenJeBlock -= 1; |
| 2825 | } |
| 2826 | |
| 2827 | /* |
| 2828 | * Hier war vor der Fassung 5.11.0 Schluss - die Schleife verliess die |
| 2829 | * Schranke bei einer einzigen Spalte je Ausschnitt UNGEPRUEFT. Genau diesen |
| 2830 | * Fall konnte sie mit ihrem Hebel gar nicht erreichen: Die Koerperzeile ist so |
| 2831 | * hoch, wie der Zeilenkopf den Namen in der Namensspalte umbricht, und weniger |
| 2832 | * Spalten machen ihn nicht niedriger. Ein Name von 7.500 Zeichen ergibt dort |
| 2833 | * rund 1.800 pt gegen 451 pt Nutzhoehe; jspdf-autotable lief damit in |
| 2834 | * printFullRow in die Endlosteilung und die ganze Ausgabe brach mit |
| 2835 | * "RangeError: Maximum call stack size exceeded" ab - ohne Unterlage, ohne |
| 2836 | * Pruefbericht, ohne verwertbaren Hinweis. |
| 2837 | * |
| 2838 | * ZWEI HEBEL, IN DIESER REIHENFOLGE, denn nur einer von beiden darf etwas |
| 2839 | * wegnehmen: |
| 2840 | * |
| 2841 | * 1. Der ZEILENKOPF wird gekuerzt. Dort steht der Name ein zweites Mal - die |
| 2842 | * volle Fassung bleibt in der Zuordnung, und der Ausdruck sagt, dass |
| 2843 | * gekuerzt wurde. |
| 2844 | * 2. Reicht das nicht, weil schon die Zuordnung allein hoeher ist als ein |
| 2845 | * Blatt, verlaesst sie den Tabellenkopf und steht als Fliesstext vor dem |
| 2846 | * Ausschnitt - ungekuerzt. Sie zu kuerzen waere der eine Weg, der nicht |
| 2847 | * gangbar ist: Sie ist der Nachweis, mit dem sich ein einzelnes Blatt ohne |
| 2848 | * ein zweites aufloesen laesst. |
| 2849 | */ |
| 2850 | const mitZuordnung = matrixHoechsteKopfhoehe(context, groups, spaltenJeBlock, mitNummern, true); |
| 2851 | if (mitZuordnung + matrixZellhoehe(zeilenkopfZeilen) > nutzhoehe) { |
| 2852 | zuordnungImKopf = mitZuordnung + matrixZellhoehe(1) <= nutzhoehe; |
| 2853 | const kopfhoehe = zuordnungImKopf |
| 2854 | ? mitZuordnung |
| 2855 | : matrixHoechsteKopfhoehe(context, groups, spaltenJeBlock, mitNummern, false); |
| 2856 | zeilenkopfZeilen = Math.max( |
| 2857 | 1, |
| 2858 | Math.floor( |
| 2859 | (nutzhoehe - kopfhoehe - 2 * MATRIX_POLSTER) / (MATRIX_SCHRIFTGRAD * MATRIX_ZEILENFAKTOR), |
| 2860 | ), |
| 2861 | ); |
| 2862 | } |
| 2863 | |
| 2864 | return { |
| 2865 | mitNummern, |
| 2866 | nummernspalte, |
| 2867 | namensspalte, |
| 2868 | spaltenJeBlock, |
| 2869 | zeilenkopfZeilen, |
| 2870 | zeilenkopfGekuerzt: zeilenkopfZeilen < zeilenkopfNoetig, |
| 2871 | zuordnungImKopf, |
| 2872 | }; |
| 2873 | } |
| 2874 | |
| 2875 | /** |
| 2876 | * Der Name im Zeilenkopf, auf die zulaessige Zahl von Zeilen gekuerzt. |
| 2877 | * |
| 2878 | * Halbierend statt zeichenweise - der Name hat keine Laengengrenze. Gekuerzt |
| 2879 | * wird mit sichtbarer Auslassung; ein stillschweigend abgeschnittener Name |
| 2880 | * waere in einer Anordnungsunterlage das Gegenteil eines Hinweises. |
| 2881 | */ |
| 2882 | function matrixZeilenkopf( |
| 2883 | context: Context, |
| 2884 | name: string, |
| 2885 | breite: number, |
| 2886 | hoechstzeilen: number, |
| 2887 | ): string { |
| 2888 | const { doc } = context; |
| 2889 | if (matrixZeilen(doc, name, breite, false) <= hoechstzeilen) return name; |
| 2890 | |
| 2891 | const auslassung = '...'; |
| 2892 | let unten = 0; |
| 2893 | let oben = name.length; |
| 2894 | while (unten < oben) { |
| 2895 | const mitte = Math.ceil((unten + oben) / 2); |
| 2896 | const probe = `${name.slice(0, mitte)}${auslassung}`; |
| 2897 | if (matrixZeilen(doc, probe, breite, false) <= hoechstzeilen) unten = mitte; |
| 2898 | else oben = mitte - 1; |
| 2899 | } |
| 2900 | return `${name.slice(0, unten).trimEnd()}${auslassung}`; |
| 2901 | } |
| 2902 | |
| 2903 | /** |
| 2904 | * Die hoechste Kopfhoehe, die bei dieser Aufteilung vorkommt. |
| 2905 | * |
| 2906 | * Nicht der erste Ausschnitt, sondern der SCHLIMMSTE: Die Banner der |
| 2907 | * Ausschnitte tragen verschiedene Namen und verschieden breite Nummern, und |
| 2908 | * die Schranke muss fuer jedes Blatt halten, nicht nur fuer das erste. |
| 2909 | */ |
| 2910 | function matrixHoechsteKopfhoehe( |
| 2911 | context: Context, |
| 2912 | groups: readonly SignalGroup[], |
| 2913 | spaltenJeBlock: number, |
| 2914 | mitNummern: boolean, |
| 2915 | mitZuordnung: boolean, |
| 2916 | ): number { |
| 2917 | const groessen = matrixAufteilung(groups.length, spaltenJeBlock); |
| 2918 | let hoechste = 0; |
| 2919 | let von = 0; |
| 2920 | for (let block = 0; block < groessen.length; block += 1) { |
| 2921 | const anzahl = groessen[block] ?? 0; |
| 2922 | hoechste = Math.max( |
| 2923 | hoechste, |
| 2924 | matrixKopfhoehe( |
| 2925 | context, |
| 2926 | groups, |
| 2927 | von, |
| 2928 | anzahl, |
| 2929 | mitNummern, |
| 2930 | block + 1, |
| 2931 | groessen.length, |
| 2932 | mitZuordnung, |
| 2933 | ), |
| 2934 | ); |
| 2935 | von += anzahl; |
| 2936 | } |
| 2937 | return hoechste; |
| 2938 | } |
| 2939 | |
| 2940 | /** |
| 2941 | * Der Banner ueber einem Ausschnitt - er steht als KOPFZEILE in der Tabelle |
| 2942 | * und wird deshalb von autotable auf JEDER Seite des Ausschnitts wiederholt. |
| 2943 | * |
| 2944 | * Das ist die Antwort auf die Auflage, die Zuordnung im Dokument selbst |
| 2945 | * aufloesbar zu halten: Wer ein einzelnes Blatt in der Hand haelt, liest ueber |
| 2946 | * der Tabelle, welcher Ausschnitt es ist und - bei Ordnungsnummern - welche |
| 2947 | * Nummer zu welchem Namen gehoert. Ein Verweis auf eine Liste an anderer Stelle |
| 2948 | * verlangte, ein zweites Blatt danebenzulegen. |
| 2949 | * |
| 2950 | * `mitZuordnung = false` ist der eine Fall, in dem das nicht geht: Ist die |
| 2951 | * Zuordnung fuer sich schon hoeher als ein Blatt, laesst sie sich nicht auf |
| 2952 | * jedem Blatt wiederholen - eine Kopfzelle ueber Blatthoehe bringt |
| 2953 | * jspdf-autotable zum Absturz. Sie steht dann ungekuerzt als Fliesstext vor dem |
| 2954 | * Ausschnitt, und der Banner sagt das. |
| 2955 | */ |
| 2956 | function matrixBanner( |
| 2957 | context: Context, |
| 2958 | groups: readonly SignalGroup[], |
| 2959 | von: number, |
| 2960 | anzahl: number, |
| 2961 | mitNummern: boolean, |
| 2962 | block: number, |
| 2963 | bloecke: number, |
| 2964 | mitZuordnung: boolean, |
| 2965 | ): { text: string; namenUngeteilt: boolean } { |
| 2966 | const zeilen: string[] = []; |
| 2967 | let namenUngeteilt = true; |
| 2968 | |
| 2969 | if (bloecke > 1) { |
| 2970 | zeilen.push( |
| 2971 | `Ausschnitt ${block} von ${bloecke} · einfahrende Signalgruppen ${von + 1} bis ` + |
| 2972 | `${von + anzahl} von ${groups.length}`, |
| 2973 | ); |
| 2974 | } |
| 2975 | if (mitNummern && !mitZuordnung) { |
| 2976 | zeilen.push(MATRIX_ZUORDNUNG_VOR_TABELLE); |
| 2977 | } else if (mitNummern) { |
| 2978 | zeilen.push( |
| 2979 | bloecke > 1 ? 'Nummern der Spaltenköpfe in diesem Ausschnitt:' : 'Nummern der Spaltenköpfe:', |
| 2980 | ); |
| 2981 | for (let i = von; i < von + anzahl; i += 1) { |
| 2982 | const teile = matrixZuordnungszeilen(context, i + 1, matrixAnzeigename(groups[i])); |
| 2983 | if (teile.length > 1) namenUngeteilt = false; |
| 2984 | zeilen.push(...teile); |
| 2985 | } |
| 2986 | } |
| 2987 | // Erst umschreiben, dann verbinden: toWinAnsi laesst den Zeilenumbruch |
| 2988 | // stehen (untere Grenze der Klasse ist \x00), die Zeilen bleiben also |
| 2989 | // getrennt. |
| 2990 | return { text: zeilen.map(toWinAnsi).join('\n'), namenUngeteilt }; |
| 2991 | } |
| 2992 | |
| 2993 | /** |
| 2994 | * Eine Zeile der Zuordnung "7 = <Name>" - und, wenn der Name dafuer zu lang |
| 2995 | * ist, ihre Fortsetzungszeilen. |
| 2996 | * |
| 2997 | * WARUM DIESE FUNKTION EXISTIERT (Fassung 5.8.0): Bis dahin entstand die Zeile |
| 2998 | * als eine Zeichenkette, und jspdf-autotable brach sie um, wenn sie nicht in |
| 2999 | * die Bannerbreite passte - gemessen ab rund 180 Zeichen Namenslaenge. Die |
| 3000 | * Fortsetzungszeile trug dann KEINE Nummer mehr: Auf dem Blatt stand ein |
| 3001 | * Namensrest, der zu jeder der aufgefuehrten Nummern gehoeren konnte. Damit war |
| 3002 | * die Zuordnung genau in dem Fall nicht mehr aufloesbar, fuer den sie da ist. |
| 3003 | * |
| 3004 | * Der Umbruch geschieht deshalb hier und nicht in der Bibliothek, und jede |
| 3005 | * Fortsetzungszeile wiederholt die Nummer mit Fortsetzungszeichen ("7 … Rest"). |
| 3006 | * Gebrochen wird an Wortgrenzen, solange es welche gibt; ein einzelnes Wort, |
| 3007 | * das laenger ist als die Zeile, teilt splitTextToSize weiterhin - dann ist der |
| 3008 | * Name zerteilt, aber er bleibt seiner Nummer zugeordnet, und der Abschnitt |
| 3009 | * "Barrierefreiheit dieser Datei" sagt es (siehe MatrixSatzbefund). |
| 3010 | */ |
| 3011 | function matrixZuordnungszeilen(context: Context, nummer: number, name: string): string[] { |
| 3012 | const { doc } = context; |
| 3013 | const breite = context.contentWidth - 2 * MATRIX_POLSTER; |
| 3014 | const marke = `${nummer} = `; |
| 3015 | const fortsetzung = `${nummer} … `; |
| 3016 | |
| 3017 | const schrift = doc.getFont() as { fontName: string; fontStyle: string }; |
| 3018 | const grad = doc.getFontSize(); |
| 3019 | doc.setFont('helvetica', 'normal'); |
| 3020 | doc.setFontSize(MATRIX_SCHRIFTGRAD); |
| 3021 | const markenbreite = doc.getTextWidth(toWinAnsi(marke)); |
| 3022 | const fortsetzungsbreite = doc.getTextWidth(toWinAnsi(fortsetzung)); |
| 3023 | // Die erste Zeile traegt "7 = ", jede weitere "7 … " - beide Marken kosten |
| 3024 | // Platz, und der Rest des Namens muss in den Rest der Zeile passen. |
| 3025 | const rest = Math.max(1, breite - Math.max(markenbreite, fortsetzungsbreite)); |
| 3026 | const stuecke = doc.splitTextToSize(toWinAnsi(name), rest) as string[]; |
| 3027 | doc.setFont(schrift.fontName, schrift.fontStyle); |
| 3028 | doc.setFontSize(grad); |
| 3029 | |
| 3030 | if (stuecke.length <= 1) return [`${marke}${name}`]; |
| 3031 | return stuecke.map((teil, i) => (i === 0 ? `${marke}${teil}` : `${fortsetzung}${teil}`)); |
| 3032 | } |
| 3033 | |
| 3034 | /** Hoehe des Banners samt Kopfzeile - fuer die Schranke und fuer ensureSpace. */ |
| 3035 | function matrixKopfhoehe( |
| 3036 | context: Context, |
| 3037 | groups: readonly SignalGroup[], |
| 3038 | von: number, |
| 3039 | anzahl: number, |
| 3040 | mitNummern: boolean, |
| 3041 | block: number, |
| 3042 | bloecke: number, |
| 3043 | mitZuordnung: boolean, |
| 3044 | ): number { |
| 3045 | const banner = matrixBanner( |
| 3046 | context, |
| 3047 | groups, |
| 3048 | von, |
| 3049 | anzahl, |
| 3050 | mitNummern, |
| 3051 | block, |
| 3052 | bloecke, |
| 3053 | mitZuordnung, |
| 3054 | ).text; |
| 3055 | const bannerhoehe = |
| 3056 | banner === '' |
| 3057 | ? 0 |
| 3058 | : matrixZellhoehe( |
| 3059 | matrixZeilen(context.doc, banner, context.contentWidth - 2 * MATRIX_POLSTER, false), |
| 3060 | ); |
| 3061 | const kopfmarken = mitNummern |
| 3062 | ? 1 |
| 3063 | : Math.max( |
| 3064 | ...groups |
| 3065 | .slice(von, von + anzahl) |
| 3066 | .map((g) => matrixZeilen(context.doc, g.name, MATRIX_KOPFNAME_HOECHSTBREITE, true)), |
| 3067 | ); |
| 3068 | return bannerhoehe + matrixZellhoehe(Math.max(kopfmarken, 1)); |
| 3069 | } |
| 3070 | |
| 3071 | /** |
| 3072 | * Traegt der heutige Satz - eine Tabelle ueber die ganze Breite - die Matrix |
| 3073 | * noch? |
| 3074 | * |
| 3075 | * Vier Fragen, und alle vier sind Fragen nach dem SCHADEN, nicht nach dem |
| 3076 | * Geschmack. Solange keine davon mit Nein beantwortet wird, bleibt der Satz |
| 3077 | * Zeichen fuer Zeichen der bisherige. |
| 3078 | * |
| 3079 | * WO DER UMSCHLAGPUNKT LIEGT, HAENGT AN DEN DATEN, nicht an einer festen Zahl |
| 3080 | * (Fassung 5.8.0 - hier stand vorher "bis 31 unveraendert, ab 32", die |
| 3081 | * Unterlagen nannten 34/35, gemessen war beides nicht dasselbe Projekt). Er |
| 3082 | * haengt an der breitesten vorkommenden Zwischenzeit und an der breitesten |
| 3083 | * Bezeichnung, denn beide bestimmen die Mindestbreite einer Spalte. Fuer |
| 3084 | * Plannamen "K1"…"Kn" und zweistellige Zwischenzeiten liegt er bei dieser |
| 3085 | * Fassung zwischen n = 35 und n = 36; ein Projekt mit einer dreistelligen |
| 3086 | * Vorgabe schlaegt frueher um. Wer eine Zahl braucht, misst sie fuer SEIN |
| 3087 | * Projekt - die Schranke rechnet sie ohnehin selbst aus. |
| 3088 | */ |
| 3089 | function matrixPasstAufEinBlatt( |
| 3090 | context: Context, |
| 3091 | groups: readonly SignalGroup[], |
| 3092 | zellen: readonly (readonly string[])[], |
| 3093 | ): boolean { |
| 3094 | const { doc } = context; |
| 3095 | |
| 3096 | /* |
| 3097 | * (0) Die Spaltenmarken muessen die Signalgruppe eindeutig benennen. Sind sie |
| 3098 | * leer oder doppelt, traegt der heutige Satz eine mehrdeutige - oder gar |
| 3099 | * keine - Beschriftung, und keine Breitenrechnung repariert das. Dann |
| 3100 | * uebernimmt der Ausschnittsatz mit Ordnungsnummern. |
| 3101 | */ |
| 3102 | if (!matrixMarkenSindEindeutig(groups)) return false; |
| 3103 | |
| 3104 | /* |
| 3105 | * (1) und (2): KEINE ZAHL UND KEIN NAME DARF MITTEN IM WORT UMBRECHEN. |
| 3106 | * Eine umgebrochene Zwischenzeit ist keine schlechtere Darstellung, sie |
| 3107 | * ist eine andere Zahl ("188*" als "18" ueber "8*"). |
| 3108 | * |
| 3109 | * GERECHNET WIRD MIT DEM VERFAHREN DER BIBLIOTHEK, nicht mit einem |
| 3110 | * Anteil. jspdf-autotable |
| 3111 | * bestimmt die Spaltenbreiten in calculateWidths in zwei Durchgaengen: |
| 3112 | * Durchgang 1 verkleinert nur bis max(minReadableWidth, minWidth), wobei |
| 3113 | * minReadableWidth = laengstes Wort + Polster und minWidth = 10 pt ist; |
| 3114 | * erst Durchgang 2 unterschreitet minReadableWidth - und GENAU DORT |
| 3115 | * bricht ein Wort. Kein Bruch im Wort heisst also: |
| 3116 | * |
| 3117 | * Summe ueber alle Spalten von max(laengstesWort + 2*Polster, 10 pt) |
| 3118 | * <= Satzbreite |
| 3119 | * |
| 3120 | * Das ist eine Bedingung an die SUMME, nicht an die einzelne Spalte. |
| 3121 | * Frueher stand hier je Spalte ein Vergleich gegen contentWidth/(n+1) - |
| 3122 | * also gegen den gleichen Anteil fuer alle. Beides faellt auseinander, |
| 3123 | * sobald eine Spalte mehr als ihren Anteil braucht, und die Kopfecke tut |
| 3124 | * das: Ihr laengstes Wort ("räumt", 23,55 pt) kostet mit Polster 29,55 pt, |
| 3125 | * waehrend der Anteil ab n = 26 darunter faellt. Der alte Vergleich liess |
| 3126 | * die Ecke ausdruecklich aussen vor - aber autotable verteilt den Fehlbetrag |
| 3127 | * auf ALLE Spalten, nicht nur auf die Ecke. Gemessen bei n=40 mit einem |
| 3128 | * Namen 0,03 pt unter dem Anteil: Die Wertespalten wurden 18,53 pt breit |
| 3129 | * statt der noetigen 18,75 pt - 0,22 pt zu schmal. Es brach dort noch |
| 3130 | * nichts, aber es hing an einer Rundung, nicht an der Rechnung. |
| 3131 | */ |
| 3132 | const breitesteZahl = matrixHoechsteZellbreite(zellen, (z) => laengstesWort(doc, z, false)); |
| 3133 | |
| 3134 | /* |
| 3135 | * Die erste Spalte traegt ZWEIERLEI: oben die Kopfecke (fett), darunter die |
| 3136 | * Namen der raeumenden Signalgruppen (mager). autotable nimmt je Spalte das |
| 3137 | * Groesste ueber alle ihre Zellen - beides gehoert also hinein. |
| 3138 | * |
| 3139 | * Die Wertespalten rechnen mit der breitesten Zahl der GANZEN Matrix statt |
| 3140 | * mit der breitesten ihrer eigenen Spalte. Das ist grosszuegiger als noetig |
| 3141 | * und damit die sichere Seite. |
| 3142 | */ |
| 3143 | const namensspaltenwort = Math.max( |
| 3144 | laengstesWort(doc, MATRIX_ECKE, true), |
| 3145 | ...groups.map((g) => laengstesWort(doc, g.name, false)), |
| 3146 | ); |
| 3147 | const mindestbreiten = [ |
| 3148 | Math.max(namensspaltenwort + 2 * MATRIX_POLSTER, MATRIX_MINDESTBREITE), |
| 3149 | ...groups.map((g) => |
| 3150 | Math.max( |
| 3151 | Math.max(laengstesWort(doc, g.name, true), breitesteZahl) + 2 * MATRIX_POLSTER, |
| 3152 | MATRIX_MINDESTBREITE, |
| 3153 | ), |
| 3154 | ), |
| 3155 | ]; |
| 3156 | const noetig = mindestbreiten.reduce((summe, b) => summe + b, 0); |
| 3157 | if (noetig > context.contentWidth) return false; |
| 3158 | |
| 3159 | /* |
| 3160 | * (3) Die Kopfzeile darf nicht das halbe Blatt fuellen. Waechst sie ueber die |
| 3161 | * Blatthoehe, laeuft jspdf-autotable in printFullRow in eine Endlosteilung |
| 3162 | * und der Ausdruck bricht ab. |
| 3163 | * |
| 3164 | * Gerechnet wird mit der MINDESTBREITE der Spalte, nicht mit dem Anteil: |
| 3165 | * Steht (1)/(2) fest, bekommt jede Spalte mindestens diese Breite, meist |
| 3166 | * mehr. Die Zeilenzahl ist damit eine obere Schranke - die Rechnung irrt |
| 3167 | * also in die sichere Richtung, waehrend der Anteil auch zu gross sein |
| 3168 | * konnte. |
| 3169 | */ |
| 3170 | const nutzhoehe = nutzhoeheJeBlatt(context); |
| 3171 | const kopfzeilen = Math.max( |
| 3172 | ...groups.map((g, i) => |
| 3173 | matrixZeilen(doc, g.name, (mindestbreiten[i + 1] ?? 0) - 2 * MATRIX_POLSTER, true), |
| 3174 | ), |
| 3175 | ); |
| 3176 | return matrixZellhoehe(kopfzeilen) <= nutzhoehe / 2; |
| 3177 | } |
| 3178 | |
| 3179 | /** Der bisherige Satz - unveraendert, damit kurze Bezeichnungen es bleiben. */ |
| 3180 | function zeichneMatrixHeute( |
| 3181 | context: Context, |
| 3182 | groups: readonly SignalGroup[], |
| 3183 | zellen: readonly (readonly string[])[], |
| 3184 | ): void { |
| 3185 | const { doc } = context; |
| 3186 | |
| 3187 | autoTable(doc, { |
| 3188 | startY: context.cursor, |
| 3189 | margin: TABELLENRAND, |
| 3190 | theme: 'grid', |
| 3191 | styles: { fontSize: MATRIX_SCHRIFTGRAD, cellPadding: MATRIX_POLSTER, halign: 'center' }, |
| 3192 | headStyles: { fillColor: '#eef1f6', textColor: '#1b2230', fontStyle: 'bold' }, |
| 3193 | columnStyles: { 0: { fontStyle: 'bold', halign: 'left' } }, |
| 3194 | head: [[MATRIX_ECKE, ...groups.map((g) => g.name)].map(toWinAnsi)], |
| 3195 | body: groups.map((from, i) => [from.name, ...(zellen[i] ?? [])].map(toWinAnsi)), |
| 3196 | }); |
| 3197 | |
| 3198 | context.cursor = lastTableBottom(doc) + 10; |
| 3199 | } |
| 3200 | |
| 3201 | /** |
| 3202 | * Der Ausschnittsatz: feste Spaltenbreiten, waagerechte Aufteilung, und - wenn |
| 3203 | * die Namen fuer einen Spaltenkopf zu lang sind - Ordnungsnummern im Kopf mit |
| 3204 | * der Zuordnung im wiederholten Tabellenkopf. |
| 3205 | * |
| 3206 | * WAS ER DER UNTERLAGE NIMMT, damit es niemand uebersieht: Der Blick auf die |
| 3207 | * ganze Matrix auf einem Blatt geht verloren, sobald sie geteilt wird. Er war |
| 3208 | * allerdings schon vorher nur nominell da - der heutige Satz verteilt dieselbe |
| 3209 | * Matrix bei 32 Gruppen mit langen Namen auf 113 Blatt und bei 40 auf gar |
| 3210 | * keines mehr. |
| 3211 | */ |
| 3212 | function zeichneMatrixAusschnitte( |
| 3213 | context: Context, |
| 3214 | groups: readonly SignalGroup[], |
| 3215 | zellen: readonly (readonly string[])[], |
| 3216 | mitSignalgruppentabelle: boolean, |
| 3217 | ): void { |
| 3218 | const { doc } = context; |
| 3219 | const satz = matrixSatz(context, groups, zellen); |
| 3220 | const groessen = matrixAufteilung(groups.length, satz.spaltenJeBlock); |
| 3221 | const bloecke = groessen.length; |
| 3222 | let namenUngeteilt = true; |
| 3223 | |
| 3224 | if (bloecke > 1) { |
| 3225 | writeParagraph( |
| 3226 | context, |
| 3227 | `Die Matrix ist für die Blattbreite zu breit und steht deshalb in ${bloecke} Ausschnitten. ` + |
| 3228 | `Jeder Ausschnitt führt alle ${groups.length} räumenden Signalgruppen in derselben ` + |
| 3229 | 'Reihenfolge und einen anderen Teil der einfahrenden; welchen, steht über jedem ' + |
| 3230 | 'Ausschnitt auf jeder Seite. Jede Zwischenzeit steht in genau einem Ausschnitt - erst ' + |
| 3231 | `alle ${bloecke} Ausschnitte zusammen sind die vollständige Matrix.`, |
| 3232 | ); |
| 3233 | } |
| 3234 | |
| 3235 | if (satz.mitNummern) { |
| 3236 | /* |
| 3237 | * Der Grund gehoert dazu, und es sind zwei verschiedene (Befunde B3/B4 |
| 3238 | * fachlich): zu lange Namen - oder Namen, die als Marke nicht taugen, weil |
| 3239 | * einer fehlt oder zwei gleich lauten. Im zweiten Fall ist der Absatz |
| 3240 | * zugleich der Hinweis, dass mit den Daten etwas nicht stimmt; die Pruefung |
| 3241 | * sagt es an ihrer Stelle noch einmal deutlicher. |
| 3242 | */ |
| 3243 | const grund = matrixKopfgrund(groups); |
| 3244 | /* |
| 3245 | * Der Verweis auf die Tabelle "Signalgruppen" steht nur da, wenn es sie in |
| 3246 | * DIESER Ausgabe gibt (Befund B5 technisch): Sonst schickt der Absatz den |
| 3247 | * Leser zu einem Abschnitt, den er nicht hat. Die Aufloesung selbst braucht |
| 3248 | * ihn nicht - sie steht in den Zeilen der Matrix und ueber jeder Tabelle. |
| 3249 | */ |
| 3250 | const herkunft = mitSignalgruppentabelle |
| 3251 | ? 'sondern die Stellung der Signalgruppe in dieser Unterlage: dieselbe Nummer, die in der ' + |
| 3252 | 'Tabelle „Signalgruppen" in der Spalte „Nr." steht, und dieselbe Reihenfolge wie in den ' + |
| 3253 | 'Zeilen dieser Matrix, die Nummer und vollen Namen nebeneinander führen.' |
| 3254 | : 'sondern die Stellung der Signalgruppe in dieser Unterlage: dieselbe Reihenfolge wie in ' + |
| 3255 | 'den Zeilen dieser Matrix, die Nummer und vollen Namen nebeneinander führen.'; |
| 3256 | writeParagraph( |
| 3257 | context, |
| 3258 | `Die Spaltenköpfe tragen die Ordnungsnummer der einfahrenden Signalgruppe, ${grund}. ` + |
| 3259 | `Die Nummer ist keine neu vergebene Kurzbezeichnung, ${herkunft} ` + |
| 3260 | (satz.zuordnungImKopf |
| 3261 | ? 'Welche Nummer zu welchem Namen gehört, steht über der Tabelle auf jeder Seite. ' |
| 3262 | : 'Welche Nummer zu welchem Namen gehört, steht vor jedem Ausschnitt; die Zuordnung ' + |
| 3263 | 'ist länger als ein Blatt und lässt sich deshalb nicht auf jedem Blatt wiederholen. ') + |
| 3264 | 'Die Tabellenausgabe (CSV) führt dieselbe Matrix mit den vollen Namen in beiden Achsen.', |
| 3265 | ); |
| 3266 | } |
| 3267 | |
| 3268 | /* |
| 3269 | * Die Kuerzung des Zeilenkopfs wird gesagt, nicht stillschweigend vollzogen. |
| 3270 | * Ohne den Satz stuende in der Anordnungsunterlage eine Bezeichnung mit |
| 3271 | * Auslassungszeichen, und der Leser wuesste nicht, wo die vollstaendige |
| 3272 | * steht. |
| 3273 | */ |
| 3274 | if (satz.zeilenkopfGekuerzt) { |
| 3275 | writeParagraph( |
| 3276 | context, |
| 3277 | 'Die Zeilenköpfe dieser Matrix sind gekürzt: Ein Name, der in der Namensspalte höher ' + |
| 3278 | 'umbricht als ein Blatt trägt, würde die Tabelle unsetzbar machen. Gekürzte Namen enden ' + |
| 3279 | 'mit "..."; vollständig stehen sie in der Zuordnung der Spaltenköpfe und in der ' + |
| 3280 | 'Tabellenausgabe (CSV).', |
| 3281 | ); |
| 3282 | } |
| 3283 | |
| 3284 | let von = 0; |
| 3285 | for (let block = 0; block < bloecke; block += 1) { |
| 3286 | const anzahl = groessen[block] ?? 0; |
| 3287 | if (anzahl === 0) continue; |
| 3288 | const spalten = groups.slice(von, von + anzahl); |
| 3289 | const wertbreite = (context.contentWidth - satz.nummernspalte - satz.namensspalte) / anzahl; |
| 3290 | |
| 3291 | const banner = matrixBanner( |
| 3292 | context, |
| 3293 | groups, |
| 3294 | von, |
| 3295 | anzahl, |
| 3296 | satz.mitNummern, |
| 3297 | block + 1, |
| 3298 | bloecke, |
| 3299 | satz.zuordnungImKopf, |
| 3300 | ); |
| 3301 | if (!banner.namenUngeteilt) namenUngeteilt = false; |
| 3302 | |
| 3303 | // Passt die Zuordnung nicht in den wiederholten Kopf, steht sie ungekuerzt |
| 3304 | // hier - als Fliesstext, der so viele Folgeblaetter bekommt wie noetig. |
| 3305 | if (satz.mitNummern && !satz.zuordnungImKopf) { |
| 3306 | const zeilen: string[] = []; |
| 3307 | for (let i = von; i < von + anzahl; i += 1) { |
| 3308 | const teile = matrixZuordnungszeilen(context, i + 1, matrixAnzeigename(groups[i])); |
| 3309 | if (teile.length > 1) namenUngeteilt = false; |
| 3310 | zeilen.push(...teile); |
| 3311 | } |
| 3312 | writeParagraph( |
| 3313 | context, |
| 3314 | `Zuordnung der Spaltenköpfe für Ausschnitt ${block + 1} von ${bloecke}:`, |
| 3315 | ); |
| 3316 | doc.setFont('helvetica', 'normal'); |
| 3317 | doc.setFontSize(MATRIX_SCHRIFTGRAD); |
| 3318 | doc.setTextColor('#1b2230'); |
| 3319 | setzeZeilenMitUmbruch( |
| 3320 | context, |
| 3321 | zeilen.map(toWinAnsi), |
| 3322 | MATRIX_SCHRIFTGRAD * MATRIX_ZEILENFAKTOR, |
| 3323 | ); |
| 3324 | context.cursor += 8; |
| 3325 | } |
| 3326 | const kopf: RowInput[] = []; |
| 3327 | if (banner.text !== '') { |
| 3328 | const zelle: CellDef = { |
| 3329 | content: banner.text, |
| 3330 | colSpan: anzahl + (satz.mitNummern ? 2 : 1), |
| 3331 | styles: { halign: 'left', fontStyle: 'normal' }, |
| 3332 | }; |
| 3333 | kopf.push([zelle]); |
| 3334 | } |
| 3335 | kopf.push( |
| 3336 | [ |
| 3337 | ...(satz.mitNummern ? ['Nr.'] : []), |
| 3338 | MATRIX_ECKE, |
| 3339 | ...spalten.map((g, i) => (satz.mitNummern ? String(von + i + 1) : g.name)), |
| 3340 | ].map(toWinAnsi), |
| 3341 | ); |
| 3342 | |
| 3343 | const koerper: RowInput[] = groups.map((from, i) => |
| 3344 | [ |
| 3345 | ...(satz.mitNummern ? [String(i + 1)] : []), |
| 3346 | // Auch der Zeilenkopf sagt lieber "(ohne Bezeichnung)" als nichts: Eine |
| 3347 | // leere Zelle liest sich wie ein Druckfehler, und eine Sprachausgabe |
| 3348 | // ueberspringt sie ganz. Zu hohe Namen werden hier gekuerzt - siehe |
| 3349 | // matrixSatz. |
| 3350 | matrixZeilenkopf( |
| 3351 | context, |
| 3352 | matrixAnzeigename(from), |
| 3353 | satz.namensspalte - 2 * MATRIX_POLSTER, |
| 3354 | satz.zeilenkopfZeilen, |
| 3355 | ), |
| 3356 | ...(zellen[i] ?? []).slice(von, von + anzahl), |
| 3357 | ].map(toWinAnsi), |
| 3358 | ); |
| 3359 | |
| 3360 | const spaltenmasse: Record<number, Partial<Styles>> = {}; |
| 3361 | let s = 0; |
| 3362 | if (satz.mitNummern) { |
| 3363 | spaltenmasse[s] = { cellWidth: satz.nummernspalte, fontStyle: 'bold', halign: 'right' }; |
| 3364 | s += 1; |
| 3365 | } |
| 3366 | spaltenmasse[s] = { cellWidth: satz.namensspalte, fontStyle: 'bold', halign: 'left' }; |
| 3367 | s += 1; |
| 3368 | for (let k = 0; k < anzahl; k += 1) { |
| 3369 | spaltenmasse[s + k] = { cellWidth: wertbreite }; |
| 3370 | } |
| 3371 | |
| 3372 | // Der Ausschnitt soll nicht mit seinem Kopf am Blattfuss beginnen. |
| 3373 | ensureSpace( |
| 3374 | context, |
| 3375 | matrixKopfhoehe( |
| 3376 | context, |
| 3377 | groups, |
| 3378 | von, |
| 3379 | anzahl, |
| 3380 | satz.mitNummern, |
| 3381 | block + 1, |
| 3382 | bloecke, |
| 3383 | satz.zuordnungImKopf, |
| 3384 | ) + 24, |
| 3385 | ); |
| 3386 | |
| 3387 | autoTable(doc, { |
| 3388 | startY: context.cursor, |
| 3389 | margin: TABELLENRAND, |
| 3390 | theme: 'grid', |
| 3391 | styles: { |
| 3392 | fontSize: MATRIX_SCHRIFTGRAD, |
| 3393 | cellPadding: MATRIX_POLSTER, |
| 3394 | halign: 'center', |
| 3395 | overflow: 'linebreak', |
| 3396 | }, |
| 3397 | headStyles: { fillColor: '#eef1f6', textColor: '#1b2230', fontStyle: 'bold' }, |
| 3398 | columnStyles: spaltenmasse, |
| 3399 | /* |
| 3400 | * Eine Zeile bleibt auf einem Blatt. Ohne das zerreisst der Blattwechsel |
| 3401 | * den Namen der raeumenden Signalgruppe mitten im Wort, und auf dem |
| 3402 | * Folgeblatt steht eine Zeile Zahlen ueber einem Namensrest. |
| 3403 | */ |
| 3404 | rowPageBreak: 'avoid', |
| 3405 | head: kopf, |
| 3406 | body: koerper, |
| 3407 | }); |
| 3408 | |
| 3409 | context.cursor = lastTableBottom(doc) + 10; |
| 3410 | von += anzahl; |
| 3411 | } |
| 3412 | |
| 3413 | context.matrixSatzbefund = { |
| 3414 | mitOrdnungsnummern: satz.mitNummern, |
| 3415 | namenUngeteilt, |
| 3416 | zuordnungAufJedemBlatt: satz.zuordnungImKopf, |
| 3417 | }; |
| 3418 | } |
| 3419 | |
| 3420 | function drawIntergreenMatrix(context: Context, mitSignalgruppentabelle: boolean): void { |
| 3421 | const { project, plan } = context; |
| 3422 | const groups = project.signalGroups; |
| 3423 | |
| 3424 | if (groups.length === 0) { |
| 3425 | writeParagraph(context, 'Es sind keine Signalgruppen erfasst.'); |
| 3426 | return; |
| 3427 | } |
| 3428 | |
| 3429 | const zellen = groups.map((from) => groups.map((to) => matrixWert(plan, from, to))); |
| 3430 | |
| 3431 | if (matrixPasstAufEinBlatt(context, groups, zellen)) { |
| 3432 | zeichneMatrixHeute(context, groups, zellen); |
| 3433 | // Der heutige Satz traegt die Namen selbst im Kopf; eine Zuordnungsliste |
| 3434 | // gibt es dort nicht, und "ungeteilt" ist fuer sie keine Frage. |
| 3435 | context.matrixSatzbefund = { |
| 3436 | mitOrdnungsnummern: false, |
| 3437 | namenUngeteilt: true, |
| 3438 | zuordnungAufJedemBlatt: true, |
| 3439 | }; |
| 3440 | } else { |
| 3441 | zeichneMatrixAusschnitte(context, groups, zellen, mitSignalgruppentabelle); |
| 3442 | } |
| 3443 | |
| 3444 | writeParagraph( |
| 3445 | context, |
| 3446 | 'Angaben in Sekunden. Ein Punkt bedeutet: die Signalgruppen sind verträglich, es ist keine ' + |
| 3447 | 'Konfliktbeziehung erfasst. Mit Stern gekennzeichnete Werte sind von Hand vorgegeben und ersetzen ' + |
| 3448 | 'die Berechnung.', |
| 3449 | ); |
| 3450 | |
| 3451 | /* |
| 3452 | * Der Rechenansatz der Ueberfahrzeit gehoert UNTER DIE MATRIX und nicht nur |
| 3453 | * in den Rechenweg (Fassung 5.5.0, Frage E2): Der Rechenweg |
| 3454 | * ist abwaehlbar, die Matrix steht dann allein da - und dieselbe Zahl hat je |
| 3455 | * nach Ansatz eine andere Ueberfahrzeit hinter sich. Der Satz kommt aus |
| 3456 | * bewertung.ts, damit Ausdruck und Bildschirm nicht auseinanderlaufen, und |
| 3457 | * rechnet mit den Vorgaben DIESES Plans (Befund C21). |
| 3458 | * |
| 3459 | * NUR, WO ER IN EINE ZAHL DIESER MATRIX EINGEHT (seit 5.43.0): Der Satz stand |
| 3460 | * unbedingt da - auch an einer einstreifigen Verkehrsfuehrung, wo |
| 3461 | * `buildSignalPlan` jeden raeumenden Kfz-Strom nach dem Verfahren der |
| 3462 | * Engstellensignalisierung rechnet und die Ueberfahrzeit fest bei tue = 4 s |
| 3463 | * liegt. Die Unterlage nannte dort eine Rechnung, die nicht stattgefunden |
| 3464 | * hat, neben den Zahlen, die der Pruefer nachrechnet. Weglassen allein waere |
| 3465 | * zu wenig: Ohne Rechenweg stuende die Matrix dann ohne jede Angabe zur |
| 3466 | * Ueberfahrzeit da - genau der Grund, aus dem der Satz hier steht. An seine |
| 3467 | * Stelle tritt das Verfahren, das gerechnet hat. |
| 3468 | * |
| 3469 | * UND DER DRITTE FALL, den keine Anlagenart hergibt: Ist JEDE Zwischenzeit |
| 3470 | * eines raeumenden Kraftfahrzeugs von Hand vorgegeben - oder raeumt an keiner |
| 3471 | * Beziehung ein Kraftfahrzeug -, hat der Ansatz ebenfalls nicht gerechnet, |
| 3472 | * und hier steht dann keiner der beiden Saetze. Das ist gewollt: Dass die |
| 3473 | * Zahlen vorgegeben sind, sagt die Sternfussnote darueber, und den Ansatz |
| 3474 | * nennt in diesem Ausdruck allein das Deckblatt - dort mit dem |
| 3475 | * Wirkungsvermerk daneben. Ein zweiter Absatz ueber eine Rechnung, die es |
| 3476 | * nicht gab, steht besser nicht unter der Matrix. |
| 3477 | * |
| 3478 | * DIE ANTWORT KOMMT AUS DEM FACHKERN (ansaetze.ts), weil Deckblatt, |
| 3479 | * Abweichungstafel und Pruefbericht dieselbe brauchen; die Bedingung hier |
| 3480 | * nachzubauen war der Anfang der vier auseinanderlaufenden Fassungen. Bewacht |
| 3481 | * von tests/export/matrixVerfahrenssatz.test.ts. |
| 3482 | */ |
| 3483 | const ohneWirkung = ueberfahrzeitAnsatzOhneWirkung(project, plan); |
| 3484 | |
| 3485 | if (ohneWirkung === null) { |
| 3486 | writeParagraph( |
| 3487 | context, |
| 3488 | ueberfahrzeitAnsatzSatz(project.settings.ueberfahrzeitAnsatz, plan.defaults), |
| 3489 | ); |
| 3490 | } else if (verfahrenDerKfzRaeumstroeme(project, plan).has('engstelle')) { |
| 3491 | writeParagraph(context, ohneWirkung.begruendung); |
| 3492 | } |
| 3493 | } |
| 3494 | |
| 3495 | function drawIntergreenDetails(context: Context, mitSignalgruppentabelle: boolean): void { |
| 3496 | const { doc, project, plan } = context; |
| 3497 | const rows: string[][] = []; |
| 3498 | let mitAnfahransatz = false; |
| 3499 | /* |
| 3500 | * ZWEI WEITERE FUSSNOTEN, BEIDE AUS DEMSELBEN GRUND (Fassung 5.27.0): Der |
| 3501 | * Rechenweg muss sich aus den gedruckten Groessen nachrechnen lassen. |
| 3502 | * |
| 3503 | * `mitEngstellenverfahren`: An einer Engstelle gilt eine andere Formel |
| 3504 | * (RiLSA 2015, Abschnitt 5.2.2) - andere Ueberfahrzeit, Raeumweg ohne |
| 3505 | * Fahrzeuglaenge, keine Einfahrzeit. Wer die Zeile mit der |
| 3506 | * Knotenpunktformel nachrechnet, kommt auf eine andere Zahl und haelt den |
| 3507 | * Plan fuer falsch. |
| 3508 | * |
| 3509 | * `mitUntergrenze`: Wo tue + tr die Untergrenze tG + 1 s unterschreiten, |
| 3510 | * steht in der Spalte tz NICHT tue + tr - te, sondern die angehobene Summe |
| 3511 | * abzueglich te. Ohne die Fussnote waere die Zeile aus ihren eigenen |
| 3512 | * Spalten nicht nachzurechnen. |
| 3513 | */ |
| 3514 | let mitEngstellenverfahren = false; |
| 3515 | const untergrenzenZeilen: string[] = []; |
| 3516 | /* |
| 3517 | * Die raeumenden Signalgruppen des Anfahransatzes mit ihrer zulaessigen |
| 3518 | * Hoechstgeschwindigkeit - fuer den Fall, dass die Signalgruppentabelle |
| 3519 | * abgewaehlt ist und die Fussnote sie selbst nennen muss (Fassung 5.9.0). |
| 3520 | */ |
| 3521 | const anfahrGruppen = new Map<string, string>(); |
| 3522 | |
| 3523 | for (const conflict of project.conflicts) { |
| 3524 | const from = project.signalGroups.find((g) => g.id === conflict.fromId); |
| 3525 | const to = project.signalGroups.find((g) => g.id === conflict.toId); |
| 3526 | const resolved = plan.intergreens.get(intergreenKey(conflict.fromId, conflict.toId)); |
| 3527 | if (!from || !to || !resolved) continue; |
| 3528 | const calc = resolved.calculation; |
| 3529 | if (calc !== null && calc.raeumansatz === 'anfahren') { |
| 3530 | mitAnfahransatz = true; |
| 3531 | // Der Name in Anfuehrungszeichen, damit Name und Zahl in der Aufzaehlung |
| 3532 | // nicht unverbunden hintereinander stehen. |
| 3533 | anfahrGruppen.set(from.id, `„${from.name}" ${fmt.numShort(from.vZul)} km/h`); |
| 3534 | } |
| 3535 | if (calc !== null && calc.verfahren === 'engstelle') mitEngstellenverfahren = true; |
| 3536 | if (calc !== null && calc.ueberfahrRaeumzeitUntergrenze !== null) { |
| 3537 | untergrenzenZeilen.push( |
| 3538 | `${from.name} -> ${to.name} (${fmt.seconds(calc.ueberfahrRaeumzeitUntergrenze, 1)} s)`, |
| 3539 | ); |
| 3540 | } |
| 3541 | rows.push( |
| 3542 | [ |
| 3543 | `${from.name} -> ${to.name}`, |
| 3544 | calc === null ? 'vorgegeben' : fmt.meters(calc.clearingPath, 1), |
| 3545 | /* |
| 3546 | * Beim Anfahransatz (OePNV mit Halt vor dem Knotenpunkt) gibt es |
| 3547 | * keine konstante Raeumgeschwindigkeit - aus einer gedruckten vr |
| 3548 | * liesse sich tr nicht nachrechnen. In der Spalte steht deshalb die |
| 3549 | * Anfahrbeschleunigung; die Formel nennt die Fussnote unter der |
| 3550 | * Tabelle. |
| 3551 | */ |
| 3552 | calc === null |
| 3553 | ? '–' |
| 3554 | : calc.raeumansatz === 'anfahren' |
| 3555 | ? `a = ${fmt.num(calc.anfahrbeschleunigung ?? 0, 1)} m/s²` |
| 3556 | : fmt.metersPerSecond(calc.clearingSpeed, 1), |
| 3557 | calc === null ? '–' : fmt.seconds(calc.clearingTime, 2), |
| 3558 | calc === null ? '–' : fmt.meters(calc.enteringPath, 1), |
| 3559 | calc === null ? '–' : fmt.metersPerSecond(calc.enteringSpeed, 1), |
| 3560 | calc === null ? '–' : fmt.seconds(calc.enteringTime, 2), |
| 3561 | calc === null ? '–' : fmt.seconds(calc.crossingTime, 1), |
| 3562 | calc === null ? '–' : fmt.seconds(calc.raw, 2), |
| 3563 | fmt.seconds(resolved.value), |
| 3564 | /* |
| 3565 | * NEU (Fassung 5.5.0, Frage E1): das Merkmal "enger |
| 3566 | * Innenradius" dieser Beziehung. Ohne die Spalte stuende in der Zeile |
| 3567 | * eine Raeumgeschwindigkeit von 5,0 m/s neben einem Regelwert von |
| 3568 | * 7,0 m/s, ohne dass die Unterlage sagt, woher der Unterschied kommt - |
| 3569 | * und ein Praxisansatz, der in der RiLSA 2015 nicht nachweisbar ist, |
| 3570 | * saehe aus wie ein Regelwerkswert. |
| 3571 | * |
| 3572 | * Die Spalte bleibt auch dann stehen, wenn keine Beziehung das Merkmal |
| 3573 | * traegt: Zwei Ausdrucke desselben Knotenpunkts sollen dieselbe |
| 3574 | * Tabellenform haben, sonst laesst sich der eine nicht neben den |
| 3575 | * anderen legen. |
| 3576 | */ |
| 3577 | engerRadiusZelle(conflict.engerRadius), |
| 3578 | // Zwei getrennte Spalten, und das mit Absicht. "Herkunft der Wege" |
| 3579 | // kommt aus dem Modell und wird von jeder Aenderung an den Wegen, den |
| 3580 | // Geschwindigkeiten, der Ueberfahrzeit oder der Zwischenzeit auf "Von |
| 3581 | // Hand eingetragen" zurueckgesetzt - der Anwender kann das Feld nicht |
| 3582 | // beschreiben. Die Bemerkung daneben ist freier Text und kann aelter |
| 3583 | // sein als der Wert neben ihr; sie war frueher der einzige Anhalt und |
| 3584 | // hat als Nachweis genau daran versagt. |
| 3585 | wegherkunftZelle(conflict), |
| 3586 | conflict.note === '' ? '–' : conflict.note, |
| 3587 | ].map(toWinAnsi), |
| 3588 | ); |
| 3589 | } |
| 3590 | |
| 3591 | autoTable(doc, { |
| 3592 | startY: context.cursor, |
| 3593 | margin: TABELLENRAND, |
| 3594 | theme: 'grid', |
| 3595 | styles: { fontSize: 8, cellPadding: 2.5, valign: 'top' }, |
| 3596 | headStyles: { fillColor: '#eef1f6', textColor: '#1b2230', fontStyle: 'bold' }, |
| 3597 | columnStyles: { 10: { cellWidth: 84 }, 11: { cellWidth: 100 }, 12: { cellWidth: 130 } }, |
| 3598 | head: [ |
| 3599 | [ |
| 3600 | 'Beziehung', |
| 3601 | 'sr', |
| 3602 | 'vr', |
| 3603 | 'tr', |
| 3604 | 'se', |
| 3605 | 've', |
| 3606 | 'te', |
| 3607 | 'tü', |
| 3608 | 'tü+tr-te', |
| 3609 | 'tz', |
| 3610 | 'Enger Innenradius', |
| 3611 | 'Herkunft der Wege', |
| 3612 | 'Bemerkung', |
| 3613 | ].map(toWinAnsi), |
| 3614 | ], |
| 3615 | body: rows, |
| 3616 | }); |
| 3617 | |
| 3618 | context.cursor = lastTableBottom(doc) + 10; |
| 3619 | writeParagraph( |
| 3620 | context, |
| 3621 | 'tz = tü + tr - te, aufgerundet auf ganze Sekunden. Die Spalte "Herkunft der Wege" weist nach, ' + |
| 3622 | 'worauf Räum- und Einfahrweg der Zeile beruhen: "Aus dem Lageplan vermessen" nennt den ' + |
| 3623 | 'Zeitpunkt der Übernahme aus dem maßstäblichen Lageplan, "Von Hand eingetragen" bezeichnet ' + |
| 3624 | 'vom Bearbeiter erfasste Maße, und "Herkunft nicht nachgewiesen" steht für Werte aus einem ' + |
| 3625 | 'Altprojekt, einer Vorlage oder einer Datei älterer Fassung, über deren Ermittlung nichts ' + |
| 3626 | 'bekannt ist. Das Programm führt die Angabe selbst und lässt sie nicht beschreiben: Der ' + |
| 3627 | 'Nachweis aus dem Lageplan entfällt, sobald einer der beiden Wege, eine Räum- oder ' + |
| 3628 | 'Einfahrgeschwindigkeit, die Überfahrzeit oder die Zwischenzeit von Hand geändert wird. ' + |
| 3629 | 'Die Spalte "Bemerkung" gibt daneben den freien Vermerk des Bearbeiters wieder; ein Strich ' + |
| 3630 | 'bedeutet, dass keine Bemerkung erfasst ist. Sie ist kein Nachweis der Herkunft und kann aus ' + |
| 3631 | 'der Zeit vor einer späteren Änderung der Wege stammen.', |
| 3632 | ); |
| 3633 | |
| 3634 | /* |
| 3635 | * Was die neue Spalte bedeutet, und was sie NICHT bedeutet (Fassung 5.5.0, Frage E1). |
| 3636 | * |
| 3637 | * Der Absatz steht unbedingt da, weil die Spalte unbedingt da steht: Eine |
| 3638 | * Spaltenueberschrift "Enger Innenradius" ohne Erlaeuterung liest sich wie |
| 3639 | * ein Regelwerksmerkmal. Die beiden Geschwindigkeiten kommen aus den |
| 3640 | * Vorgaben dieses Plans, nicht aus dem Satz (Befund C21). |
| 3641 | * |
| 3642 | * WELCHER STROM ABBIEGT, GEHOERT DAZU: Eine Sammelsignalgruppe fuehrt |
| 3643 | * geradeaus UND rechts; der Raeumweg einer einzelnen Beziehung gehoert dann |
| 3644 | * dem Strom, der bei der Uebernahme aus dem Lageplan den Ausschlag gab. Ohne |
| 3645 | * diesen Satz steht in der Unterlage eine Zeile mit gesenkter |
| 3646 | * Raeumgeschwindigkeit neben einer Signalgruppentabelle, die "geradeaus" |
| 3647 | * fuehrt, und nichts erklaert den Unterschied. |
| 3648 | */ |
| 3649 | writeParagraph( |
| 3650 | context, |
| 3651 | `Die Spalte "Enger Innenradius" gibt das Merkmal der einzelnen Konfliktbeziehung wieder. Ist ` + |
| 3652 | `es gesetzt, räumt ein abbiegender Kraftfahrzeugstrom ` + |
| 3653 | `${engerRadiusRichtungSatzteil(plan.defaults)}. Bei allen übrigen Strömen bleibt das ` + |
| 3654 | 'Merkmal ohne Wirkung - ist es dort gesetzt, meldet der Prüfbericht eine Warnung. ' + |
| 3655 | 'Ob ein Strom abbiegt, entscheidet dabei die Fahrbeziehung des Stroms, dem der Räumweg ' + |
| 3656 | 'dieser Zeile gehört, und nicht die der räumenden Signalgruppe: Führt eine Signalgruppe ' + |
| 3657 | 'geradeaus und rechts aus einem Fahrstreifen, so gehört der Räumweg dem Strom, der bei ' + |
| 3658 | 'der Übernahme aus dem Lageplan den Ausschlag gab. ' + |
| 3659 | // DER RUECKFALL GEHOERT IN DENSELBEN ABSATZ: Festgehalten wird der |
| 3660 | // massgebende Strom allein bei der Uebernahme aus dem Lageplan. Ohne |
| 3661 | // diesen Satz entnimmt der Pruefer der Unterlage eine Zusicherung, die |
| 3662 | // das Programm im Regelfall - Altprojekt, Vorlage, von Hand erfasst - |
| 3663 | // nicht einhaelt, und haelt die Zeile faelschlich fuer nachgewiesen. |
| 3664 | // Wortlaut wie im Pruefbericht (validation/rules.ts, |
| 3665 | // 'zwischenzeiten.raeumbeziehung-nicht-nachgewiesen'). |
| 3666 | 'Ist nicht festgehalten, welcher Strom den Räumweg gefahren ist, wird mit der ' + |
| 3667 | 'Fahrbeziehung der Signalgruppe gerechnet, und der Prüfbericht meldet das. ' + |
| 3668 | 'Mit welcher Räumgeschwindigkeit gerechnet wurde, steht in der Spalte "vr". ' + |
| 3669 | // Vorrang der Handeingabe (Fassung 5.5.0): Er stand bis dahin nirgends - |
| 3670 | // weder hier noch in der Hilfe noch im Pruefbericht -, und die Rechnung |
| 3671 | // verwarf das Merkmal ohne jede Notiz. |
| 3672 | 'Steht an derselben Beziehung eine Räumgeschwindigkeit von Hand in der Spalte "vr", so ' + |
| 3673 | 'hat sie Vorrang: Gerechnet wird dann mit ihr, das Merkmal geht in keine Zahl ein, und ' + |
| 3674 | 'der Prüfbericht warnt. Auch die Schwelle von 10 m prüft dieses ' + |
| 3675 | 'Programm nicht nach: Es kennt keinen Abbiegeradius, die Angabe ist vom Bearbeiter zu ' + |
| 3676 | 'verantworten. Die Spalte "tü" folgt bei Kraftfahrzeugströmen dem Rechenansatz der ' + |
| 3677 | 'Überfahrzeit (welcher gilt, steht auf dem Deckblatt und im Abschnitt ' + |
| 3678 | '"Zwischenzeitenmatrix"); für Fußgänger, Radverkehr und ÖPNV gelten stattdessen deren ' + |
| 3679 | 'eigene Regelwerte - der Ansatz erreicht sie nicht.', |
| 3680 | ); |
| 3681 | |
| 3682 | /* |
| 3683 | * Die Formel gehoert in die Unterlage, sobald eine Zeile nach ihr gerechnet |
| 3684 | * ist - sonst steht dort ein tr, das sich aus keiner gedruckten Groesse |
| 3685 | * ergibt. Ohne betroffene Zeile bleibt der Absatz weg, damit er nicht als |
| 3686 | * allgemeine Rechenregel missverstanden wird. |
| 3687 | */ |
| 3688 | if (mitEngstellenverfahren) { |
| 3689 | writeParagraph( |
| 3690 | context, |
| 3691 | 'An einer einstreifigen Verkehrsführung gilt für den Kraftfahrzeugverkehr das eigene ' + |
| 3692 | 'Verfahren der Engstellensignalisierung (RiLSA 2015, Abschnitt 5.2.2): ' + |
| 3693 | 'tz = tü + sr/Vr · 3,6, mit einer Überfahrzeit von tü = 4 s, dem Abstand der Haltlinien ' + |
| 3694 | 'als Räumweg sr – OHNE Fahrzeuglänge – und einer mittleren Räumgeschwindigkeit Vr nach ' + |
| 3695 | 'der zulässigen Höchstgeschwindigkeit (50, 40 bzw. 30 km/h bei 60, 50 bzw. 40 km/h). ' + |
| 3696 | 'Eine Einfahrzeit wird nicht abgezogen; die Spalten se, ve und te führen deshalb 0. Die ' + |
| 3697 | 'Spalte vr nennt dieselbe Räumgeschwindigkeit in m/s, damit tr = sr/vr nachzurechnen ' + |
| 3698 | 'ist. Für Rad, Fußgänger und ÖPNV gilt auch dort Abschnitt 2.5.', |
| 3699 | ); |
| 3700 | } |
| 3701 | |
| 3702 | if (untergrenzenZeilen.length > 0) { |
| 3703 | writeParagraph( |
| 3704 | context, |
| 3705 | 'Für geradeaus fahrende und abbiegende Kraftfahrzeuge verlangt die RiLSA 2015 ' + |
| 3706 | '(Abschnitt 2.5.2) eine Untergrenze: tü + tr mindestens Gelbzeit + 1 s. Wo sie greift, ' + |
| 3707 | 'ist die Spalte tz nicht tü + tr − te, sondern diese Untergrenze abzüglich te; die ' + |
| 3708 | 'Spalten tü und tr führen unverändert die gerechneten Werte. Betroffen ' + |
| 3709 | `${untergrenzenZeilen.length === 1 ? 'ist' : 'sind'} in diesem Plan: ` + |
| 3710 | `${untergrenzenZeilen.join(', ')}.`, |
| 3711 | ); |
| 3712 | } |
| 3713 | |
| 3714 | if (mitAnfahransatz) { |
| 3715 | /* |
| 3716 | * WOHER DIE VMAX KOMMT, STEHT NUR DA, WENN ES SIE IN DIESER AUSGABE GIBT |
| 3717 | * (Fassung 5.9.0; dasselbe Muster wie beim Verweis auf die Tabelle |
| 3718 | * "Signalgruppen" in zeichneMatrixAusschnitte). |
| 3719 | * |
| 3720 | * Die Spalte "V zul" entsteht nur unter `options.includeSignalGroups`, der |
| 3721 | * Rechenweg haengt an anderen Schaltern. Ohne die Tabelle verwies der |
| 3722 | * Absatz auf einen Abschnitt, den der Leser nicht hat, und das gedruckte |
| 3723 | * tr liess sich aus keiner gedruckten Groesse nachrechnen. Die |
| 3724 | * knotenpunktweite "Zulässige Höchstgeschwindigkeit" des Deckblatts ist |
| 3725 | * nicht gemeint - gerechnet wird mit der der raeumenden Signalgruppe. |
| 3726 | * Deshalb nennt der Absatz sie dann selbst - als eigener Satz und mit der |
| 3727 | * Zuordnung zur Zeile, nicht als Aneinanderreihung von Name und Zahl. |
| 3728 | */ |
| 3729 | const vmaxHerkunft = mitSignalgruppentabelle |
| 3730 | ? '(Spalte "V zul" der Signalgruppentabelle)' |
| 3731 | : '(die Tabelle „Signalgruppen" ist für diese Ausgabe abgewählt)'; |
| 3732 | const vmaxNachsatz = mitSignalgruppentabelle |
| 3733 | ? '' |
| 3734 | : ' Die räumenden Signalgruppen dieses Ansatzes und ihre Vmax: ' + |
| 3735 | `${[...anfahrGruppen.values()].join(', ')}. Maßgebend ist je Zeile die in der Spalte ` + |
| 3736 | '„Beziehung" zuerst genannte, also die räumende Signalgruppe.'; |
| 3737 | writeParagraph( |
| 3738 | context, |
| 3739 | 'Bei ÖPNV-Strömen, die vor dem Knotenpunkt halten, gilt nach RiLSA 2015 (Fall 4) der ' + |
| 3740 | 'Anfahransatz: Die Spalte vr nennt statt der Geschwindigkeit die Anfahrbeschleunigung a, ' + |
| 3741 | 'die Überfahrzeit tü beträgt 0 s, und die Räumzeit ist tr = Wurzel aus (2 · sr / a). ' + |
| 3742 | 'Erreicht das Fahrzeug dabei die zulässige Höchstgeschwindigkeit Vmax der Signalgruppe ' + |
| 3743 | `${vmaxHerkunft}, wird der Restweg mit ihr gefahren: ` + |
| 3744 | `tr = Vmax/(3,6 · a) + (sr - Vmax²/(2 · 3,6² · a)) / (Vmax/3,6).${vmaxNachsatz}`, |
| 3745 | ); |
| 3746 | } |
| 3747 | } |
| 3748 | |
| 3749 | /** |
| 3750 | * Wie viele Beziehungen ihre Wege woher haben. |
| 3751 | * |
| 3752 | * Gehoert in die Planungsgrundlage, weil der Rechenweg der Zwischenzeiten nur |
| 3753 | * auf Wunsch gedruckt wird: Ohne diese Zeile stuende in einer abgewaehlten |
| 3754 | * Fassung nirgends, dass ein Teil der Masse nicht nachgewiesen ist. Gerade in |
| 3755 | * einer Unterlage OHNE Lageplan ist "woher dann?" die entscheidende Frage. |
| 3756 | */ |
| 3757 | function wegnachweisVerteilung(project: Project): string { |
| 3758 | if (project.conflicts.length === 0) return 'Keine Konfliktbeziehungen erfasst.'; |
| 3759 | // Eigene Kurzformen statt der kleingeschriebenen Benennung: "12 herkunft |
| 3760 | // nicht nachgewiesen" ist kein deutscher Satz. |
| 3761 | const kurz = { |
| 3762 | lageplan: 'aus dem Lageplan vermessen', |
| 3763 | handeingabe: 'von Hand eingetragen', |
| 3764 | unbestimmt: 'ohne Nachweis der Herkunft', |
| 3765 | } as const; |
| 3766 | const teile: string[] = []; |
| 3767 | for (const stufe of ['lageplan', 'handeingabe', 'unbestimmt'] as const) { |
| 3768 | const anzahl = project.conflicts.filter((c) => c.wegnachweis.herkunft === stufe).length; |
| 3769 | if (anzahl > 0) teile.push(`${anzahl} ${kurz[stufe]}`); |
| 3770 | } |
| 3771 | return `${project.conflicts.length} Beziehungen: ${teile.join(', ')}.`; |
| 3772 | } |
| 3773 | |
| 3774 | /** Zustand eines Verfahrensergebnisses in einem Wort - wie in der Phasenansicht. */ |
| 3775 | function zustandstext(bounded: CycleTimeResult['bounded']): string { |
| 3776 | switch (bounded) { |
| 3777 | case 'keine': |
| 3778 | return 'Rechenwert'; |
| 3779 | case 'minimum': |
| 3780 | return 'Untergrenze maßgebend'; |
| 3781 | case 'mindestumlauf': |
| 3782 | return 'Mindestumlauf maßgebend'; |
| 3783 | case 'maximum': |
| 3784 | return 'über dem Höchstwert'; |
| 3785 | case 'uebersaettigt': |
| 3786 | return 'nicht bemessbar'; |
| 3787 | } |
| 3788 | } |
| 3789 | |
| 3790 | /** |
| 3791 | * Die vier Rechenansaetze der Umlaufzeit nebeneinander. |
| 3792 | * |
| 3793 | * WOZU DAS IN EINER ANORDNUNGSUNTERLAGE STEHT: Eine Anordnung nach Paragraf 45 |
| 3794 | * StVO ist eine Ermessensentscheidung und muss begruendet werden. Die |
| 3795 | * Umlaufzeit ist die Zahl, an der Wartezeit, Rueckstau und damit die Frage |
| 3796 | * haengen, ob die angeordnete Regelung den Verkehr bewaeltigt. Vor der |
| 3797 | * Fassung 5.29.0 stand in der Unterlage genau eine Zahl und ein Verfahrensname; |
| 3798 | * warum dieses Verfahren und nicht ein anderes, stand nirgends, und ein Pruefer |
| 3799 | * konnte es auch nicht nachrechnen, ohne das Programm selbst zu bedienen. |
| 3800 | * |
| 3801 | * ES IST KEINE EMPFEHLUNG UND WIRD ALS SOLCHE AUCH NICHT GEDRUCKT. Der |
| 3802 | * Abschnitt nennt vier Zahlen mit ihrem Zustand und den Wert, der den Bedarf |
| 3803 | * beider Zielgroessen deckt. Die Regel dahinter steht in keinem Regelwerk; der |
| 3804 | * Absatz darunter sagt das, und das Fundstellenverzeichnis fuehrt sie unter |
| 3805 | * `programm-umlaufzeitvergleich`. |
| 3806 | */ |
| 3807 | function drawCycleComparison(context: Context): void { |
| 3808 | const { doc, plan, project } = context; |
| 3809 | const vergleich = plan.cycleComparison; |
| 3810 | if (vergleich === null) { |
| 3811 | writeParagraph( |
| 3812 | context, |
| 3813 | 'Für diese Planung liegt kein Vergleich vor: Ohne Phasen gibt es keine Verlustzeit und ' + |
| 3814 | 'damit keine Umlaufzeit zu vergleichen.', |
| 3815 | ); |
| 3816 | return; |
| 3817 | } |
| 3818 | |
| 3819 | const gewaehlt = project.program.method; |
| 3820 | |
| 3821 | autoTable(doc, { |
| 3822 | startY: context.cursor, |
| 3823 | margin: TABELLENRAND, |
| 3824 | theme: 'grid', |
| 3825 | styles: { fontSize: 9, cellPadding: 3 }, |
| 3826 | headStyles: { fillColor: '#eef1f6', textColor: '#1b2230', fontStyle: 'bold' }, |
| 3827 | head: [['Verfahren', 'Umlaufzeit', 'Rechenwert', 'Zustand'].map(toWinAnsi)], |
| 3828 | body: CYCLE_METHODS.map((method) => { |
| 3829 | const ergebnis = vergleich.results[method]; |
| 3830 | return [ |
| 3831 | method === gewaehlt |
| 3832 | ? `${CYCLE_METHOD_LABELS[method]} (gewählt)` |
| 3833 | : CYCLE_METHOD_LABELS[method], |
| 3834 | fmt.seconds(ergebnis.cycleTime), |
| 3835 | // Bei Uebersaettigung ist der Rechenwert unendlich; `fmt.seconds` |
| 3836 | // schreibt dafuer einen Strich (dieselbe Stelle wie am Bildschirm). |
| 3837 | fmt.seconds(ergebnis.raw, 1), |
| 3838 | zustandstext(ergebnis.bounded), |
| 3839 | ].map(toWinAnsi); |
| 3840 | }), |
| 3841 | }); |
| 3842 | |
| 3843 | context.cursor = lastTableBottom(doc) + 12; |
| 3844 | |
| 3845 | writeParagraph(context, vergleich.reason); |
| 3846 | writeParagraph( |
| 3847 | context, |
| 3848 | 'Diese Gegenüberstellung ist keine Empfehlung. Welcher Ansatz anzuwenden ist, sagt kein ' + |
| 3849 | 'Regelwerk; die Wahl trifft der Planer und verantwortet sie fachlich. Auch die Regel, nach ' + |
| 3850 | 'der der oben genannte Wert gebildet wird – maßgebend ist der größere aus ' + |
| 3851 | 'kapazitätsorientiertem Bedarf und Wartezeitoptimum –, steht in keinem Regelwerk: Sie ist ' + |
| 3852 | 'ein Ansatz dieses Programms und im Fundstellenverzeichnis als solcher ausgewiesen. ' + |
| 3853 | 'Gerechnet wird der Signalzeitenplan ausschließlich mit dem gewählten Verfahren; die ' + |
| 3854 | 'Beanstandungen der übrigen Ansätze stehen nur in dieser Tafel und gehen nicht in den ' + |
| 3855 | 'Prüfbericht ein.', |
| 3856 | ); |
| 3857 | if (gewaehlt === 'manuell') { |
| 3858 | writeParagraph( |
| 3859 | context, |
| 3860 | 'Die Umlaufzeit dieser Planung ist von Hand vorgegeben; keines der vier Verfahren geht in ' + |
| 3861 | 'sie ein. Die Tafel steht hier als Vergleichsmaßstab: Sie zeigt, was die Ansätze aus ' + |
| 3862 | 'denselben Verkehrsstärken, Zwischenzeiten und Mindestfreigabezeiten ergeben würden.', |
| 3863 | ); |
| 3864 | } |
| 3865 | } |
| 3866 | |
| 3867 | function drawPhaseTables(context: Context): void { |
| 3868 | const { doc, project, plan } = context; |
| 3869 | |
| 3870 | // Beginn und Freigabezeit der Phasen sind auf die Umlaufzeit verteilt. |
| 3871 | ersatzwertAbsatz(context); |
| 3872 | |
| 3873 | autoTable(doc, { |
| 3874 | startY: context.cursor, |
| 3875 | margin: TABELLENRAND, |
| 3876 | theme: 'grid', |
| 3877 | styles: { fontSize: 9, cellPadding: 3 }, |
| 3878 | headStyles: { fillColor: '#eef1f6', textColor: '#1b2230', fontStyle: 'bold' }, |
| 3879 | head: [['Nr.', 'Phase', 'Signalgruppen mit Freigabe', 'Beginn', 'Freigabezeit'].map(toWinAnsi)], |
| 3880 | body: plan.phases.map((phase, index) => |
| 3881 | [ |
| 3882 | String(index + 1), |
| 3883 | phase.name, |
| 3884 | phase.signalGroupIds |
| 3885 | .map((id) => project.signalGroups.find((g) => g.id === id)?.name ?? '?') |
| 3886 | .join(', '), |
| 3887 | fmt.seconds(phase.start), |
| 3888 | fmt.seconds(phase.duration), |
| 3889 | ].map(toWinAnsi), |
| 3890 | ), |
| 3891 | }); |
| 3892 | |
| 3893 | context.cursor = lastTableBottom(doc) + 16; |
| 3894 | ensureSpace(context, 120); |
| 3895 | |
| 3896 | autoTable(doc, { |
| 3897 | startY: context.cursor, |
| 3898 | margin: TABELLENRAND, |
| 3899 | theme: 'grid', |
| 3900 | styles: { fontSize: 9, cellPadding: 3 }, |
| 3901 | headStyles: { fillColor: '#eef1f6', textColor: '#1b2230', fontStyle: 'bold' }, |
| 3902 | head: [['Übergang', 'Beginn', 'Dauer', 'Maßgebende Beziehung'].map(toWinAnsi)], |
| 3903 | body: plan.transitions.map((transition) => { |
| 3904 | const from = plan.phases.find((p) => p.id === transition.fromPhaseId); |
| 3905 | const to = plan.phases.find((p) => p.id === transition.toPhaseId); |
| 3906 | return [ |
| 3907 | `${from?.name ?? '?'} -> ${to?.name ?? '?'}`, |
| 3908 | fmt.seconds(transition.start), |
| 3909 | fmt.seconds(transition.duration), |
| 3910 | transition.critical === null |
| 3911 | ? 'keine feindliche Beziehung' |
| 3912 | : `${transition.critical.fromGroupName} räumt -> ${transition.critical.toGroupName} fährt ein`, |
| 3913 | ].map(toWinAnsi); |
| 3914 | }), |
| 3915 | }); |
| 3916 | |
| 3917 | context.cursor = lastTableBottom(doc) + 16; |
| 3918 | } |
| 3919 | |
| 3920 | /* |
| 3921 | * Kleinstmasse der gestauchten Zeichnung. |
| 3922 | * |
| 3923 | * Befund M12. Gestaucht wird nur so weit, wie die Zeichnung lesbar bleibt; |
| 3924 | * darunter wird auf mehrere Blaetter aufgeteilt. Eine Zeichnung, die so lange |
| 3925 | * verkleinert wird, bis sie passt, ist am Ende genauso unbrauchbar wie eine, |
| 3926 | * die ueber den Rand laeuft. |
| 3927 | */ |
| 3928 | const PLAN_MIN_ROW_HEIGHT = 14; |
| 3929 | const PLAN_MIN_ROW_GAP = 3; |
| 3930 | const PLAN_MIN_FONT_SIZE = 8; |
| 3931 | |
| 3932 | /** Abstand zwischen der Zeichnung und dem Absatz darunter. */ |
| 3933 | const PLAN_ABSATZ_ABSTAND = 14; |
| 3934 | |
| 3935 | /** |
| 3936 | * Verweis auf die gleichwertige Textfassung der Zeichnung. |
| 3937 | * |
| 3938 | * Befund S1 (1.1.1 Nicht-Text-Inhalt, A): Die Zeichnung hat keinen |
| 3939 | * Alternativtext, und dass die Tabelle "Signalzeiten je Signalgruppe" denselben |
| 3940 | * Inhalt in Textform enthaelt, stand NIRGENDS im Dokument - ein Suchlauf ueber |
| 3941 | * den ganzen Text fand das Wort "Grafik" genau einmal, in der |
| 3942 | * Bestandteileliste. |
| 3943 | * |
| 3944 | * Der Absatz ist die WCAG-Technik G74 (Alternative im umgebenden Text, mit |
| 3945 | * Verweis). Er behebt 1.1.1 NICHT: Ohne Strukturbaum gibt es kein Figure, dem |
| 3946 | * eine Alternative zugeordnet waere. Er macht die Unterlage im Vorlesefall |
| 3947 | * benutzbar, und er ist die Voraussetzung dafuer, dass der Abschnitt |
| 3948 | * "Barrierefreiheit dieser Datei" ueberhaupt behaupten darf, die Tabelle sei |
| 3949 | * die Textfassung der Zeichnung. |
| 3950 | */ |
| 3951 | /** |
| 3952 | * Der gleichbleibende Teil des Verweisabsatzes. |
| 3953 | * |
| 3954 | * Der Nachsatz zum Phasenband haengt daran, ob der Abschnitt "Phasen und |
| 3955 | * Phasenuebergaenge" ueberhaupt ausgegeben wird: Die Zeichnung traegt ueber der |
| 3956 | * Zeitachse Phasennamen und Uebergangszeiten, und die stehen in der |
| 3957 | * Signalzeitentabelle NICHT. Ein Verweis auf einen abgewaehlten Abschnitt waere |
| 3958 | * ein Verweis ins Leere. |
| 3959 | */ |
| 3960 | function planVerweisRumpf(mitPhasenabschnitt: boolean): string { |
| 3961 | return ( |
| 3962 | 'Über diesem Absatz steht eine Zeichnung: der Signalzeitenplan als ' + |
| 3963 | 'Balkendiagramm. Jede Zeile ist eine Signalgruppe, die waagerechte Achse ist der Umlauf in ' + |
| 3964 | 'Sekunden; die farbigen Abschnitte sind die Signalbilder und tragen ihr Kürzel (R = Rot, ' + |
| 3965 | 'RG = Rot-Gelb, F = Freigabe, GE = Gelb) und ihre Dauer. Dieselben Angaben stehen ' + |
| 3966 | 'vollständig in der Tabelle „Signalzeiten je Signalgruppe", die auf die Zeichnung folgt; ' + |
| 3967 | 'diese Tabelle ist die gleichwertige Textfassung der Zeichnung. ' + |
| 3968 | (mitPhasenabschnitt |
| 3969 | ? 'Über der Zeitachse steht das Phasenband mit Phasennamen und Übergangszeiten; es ist im ' + |
| 3970 | 'Abschnitt „Phasen und Phasenübergänge" in Textform aufgeführt.' |
| 3971 | : 'Über der Zeitachse steht das Phasenband mit Phasennamen und Übergangszeiten; der ' + |
| 3972 | 'Abschnitt „Phasen und Phasenübergänge", der sie in Textform aufführt, wurde für diese ' + |
| 3973 | 'Ausgabe abgewählt.') |
| 3974 | ); |
| 3975 | } |
| 3976 | |
| 3977 | /** Blattangabe vor dem Verweisabsatz - nur, wenn die Zeichnung geteilt ist. */ |
| 3978 | function planBlattangabe(blatt: number, blaetter: number, gruppen: string): string { |
| 3979 | return blaetter > 1 ? `Blatt ${blatt} von ${blaetter} der Zeichnung, ${gruppen}. ` : ''; |
| 3980 | } |
| 3981 | |
| 3982 | /** Bezeichnung der Signalgruppen eines Blattes - Einzahl, wenn es nur eine ist. */ |
| 3983 | function blattGruppen(gruppen: readonly PlannedSignalGroup[]): string { |
| 3984 | const erste = gruppen[0]?.name ?? '–'; |
| 3985 | const letzte = gruppen[gruppen.length - 1]?.name ?? '–'; |
| 3986 | return gruppen.length === 1 ? `Signalgruppe ${erste}` : `Signalgruppen ${erste} bis ${letzte}`; |
| 3987 | } |
| 3988 | |
| 3989 | /** |
| 3990 | * Laengste Blattangabe, die bei diesem Plan ueberhaupt entstehen kann. |
| 3991 | * |
| 3992 | * Obere Schranke fuer die Platzreserve unter der Zeichnung (Befund B1): die |
| 3993 | * hoechste denkbare Blattzahl - eine Signalgruppe je Blatt - und die beiden |
| 3994 | * laengsten Namen. Kein Blatt einer tatsaechlichen Aufteilung kann laenger |
| 3995 | * ausfallen, also genuegt EIN zweiter Anlauf. |
| 3996 | */ |
| 3997 | function laengsteBlattangabe(plan: SignalPlan): string { |
| 3998 | const anzahl = Math.max(2, plan.groups.length); |
| 3999 | const namen = [...plan.groups.map((g) => g.name)].sort((a, b) => b.length - a.length); |
| 4000 | const erste = namen[0] ?? '–'; |
| 4001 | const zweite = namen[1] ?? erste; |
| 4002 | return `Blatt ${anzahl} von ${anzahl} der Zeichnung, Signalgruppen ${erste} bis ${zweite}. `; |
| 4003 | } |
| 4004 | |
| 4005 | function planVerweisAbsatz( |
| 4006 | blatt: number, |
| 4007 | blaetter: number, |
| 4008 | gruppen: string, |
| 4009 | mitPhasenabschnitt: boolean, |
| 4010 | ): string { |
| 4011 | return `${planBlattangabe(blatt, blaetter, gruppen)}${planVerweisRumpf(mitPhasenabschnitt)}`; |
| 4012 | } |
| 4013 | |
| 4014 | /** |
| 4015 | * Fussnote zu Abschnitten, die fuer eine Beschriftung zu schmal sind. |
| 4016 | * |
| 4017 | * Befund M5 (1.4.1 Verwendung von Farbe, A): In der Zeichnung wird das Kuerzel |
| 4018 | * weggelassen, wenn der Abschnitt zu schmal ist (signalPlanDrawing.ts:246-250, |
| 4019 | * kein dritter Zweig). Im Musterplan mit 90 s Umlauf und 1 s Rot-Gelb steht |
| 4020 | * deshalb in keiner einzigen Signalgruppenzeile ein "RG" - der Abschnitt ist |
| 4021 | * dort ausschliesslich an seiner orangen Farbe zu erkennen, und im |
| 4022 | * Schwarzweissausdruck, dem Regelfall bei einer Anordnungsakte, gar nicht. |
| 4023 | * |
| 4024 | * ABGRENZUNG ZUR ZEICHNUNG: Die Zeichnung selbst benennt unter ihrer Legende, |
| 4025 | * WELCHE Signalbilder es diesmal getroffen hat (drawSchmalHinweis in |
| 4026 | * signalPlanDrawing.ts) - eine Angabe, die nur dort entstehen kann, weil nur |
| 4027 | * dort gemessen wird, was noch hineinpasst. Diese Fussnote wiederholt das nicht |
| 4028 | * und beruft sich auch nicht darauf: Sie sagt die Regel und nennt die Tabelle, |
| 4029 | * in der die Zeiten vollstaendig stehen. So bleibt sie richtig, gleich ob die |
| 4030 | * Zeichnung ihren Hinweis zeigt oder nicht - eine Fussnote, die ihren |
| 4031 | * Gegenstand ueberlebt, ist eine falsche Angabe. |
| 4032 | */ |
| 4033 | const PLAN_SCHMALE_ABSCHNITTE = |
| 4034 | 'Die Zeichnung beschriftet jeden Abschnitt mit dem Kürzel des Signalbilds und seiner Dauer, ' + |
| 4035 | 'soweit die Breite des Abschnitts dafür ausreicht. Sehr kurze Signalbilder - im Regelfall ' + |
| 4036 | 'Rot-Gelb mit 1 s - ergeben Abschnitte, die dafür zu schmal sind; in ihnen steht dann keine ' + |
| 4037 | 'Beschriftung. Für diese Abschnitte wie für jeden anderen führt die Tabelle „Signalzeiten je ' + |
| 4038 | 'Signalgruppe" das Signalbild mit seiner Dauer auf. Bei Farbfehlsichtigkeit und im ' + |
| 4039 | 'Schwarzweißausdruck ist sie deshalb die verlässliche Quelle.'; |
| 4040 | |
| 4041 | /** |
| 4042 | * Layout der Zeichnung fuer die verfuegbare Hoehe. |
| 4043 | * |
| 4044 | * KORREKTUR (Befund M12): Die Hoehe wurde mit Math.min gekappt, die Zeichnung |
| 4045 | * erfuhr davon nichts - drawSignalPlan rechnet seine Zeilen aus dem Layout, |
| 4046 | * nicht aus der Flaechenhoehe, und PdfSurface beschneidet nicht. Ab 13 |
| 4047 | * Signalgruppen lief die Zeichnung damit ueber den Seitenrand hinaus, und weil |
| 4048 | * die Legende ganz unten steht, fiel sie als Erstes weg: Danach trug in der |
| 4049 | * Zeichnung nur noch die Farbe, ohne jede Erklaerung - aus einem Layoutfehler |
| 4050 | * wurde ein Verstoss gegen 1.4.1. |
| 4051 | * |
| 4052 | * Gestaucht werden nur die Signalgruppenzeilen. Kopfbereich, Raender und der |
| 4053 | * Platz fuer Legende und Ersatzwertvermerk bleiben, wie sie sind; sie tragen |
| 4054 | * gerade den Textschluessel zu den Farben. |
| 4055 | */ |
| 4056 | function planGrafikLayout(plan: SignalPlan, zeilen: number, verfuegbar: number): PlanLayout { |
| 4057 | const jeZeile = DEFAULT_LAYOUT.rowHeight + DEFAULT_LAYOUT.rowGap; |
| 4058 | // Feste Anteile: Rand, Kopfbereich, Legende und - bei uebersaettigter Anlage - |
| 4059 | // die Vermerkzeile. planHeight rechnet mindestens eine Zeile, die hier abgeht. |
| 4060 | const fest = planHeight({ groups: [], cycleResult: plan.cycleResult }, DEFAULT_LAYOUT) - jeZeile; |
| 4061 | if (fest + zeilen * jeZeile <= verfuegbar) return DEFAULT_LAYOUT; |
| 4062 | |
| 4063 | const proZeile = Math.floor((verfuegbar - fest) / zeilen); |
| 4064 | const rowGap = Math.max( |
| 4065 | PLAN_MIN_ROW_GAP, |
| 4066 | Math.min(DEFAULT_LAYOUT.rowGap, Math.round(proZeile / 5)), |
| 4067 | ); |
| 4068 | const rowHeight = Math.max( |
| 4069 | PLAN_MIN_ROW_HEIGHT, |
| 4070 | Math.min(DEFAULT_LAYOUT.rowHeight, proZeile - rowGap), |
| 4071 | ); |
| 4072 | return { |
| 4073 | ...DEFAULT_LAYOUT, |
| 4074 | rowHeight, |
| 4075 | rowGap, |
| 4076 | // Die Schrift folgt der Zeilenhoehe, sonst steht die Beschriftung ueber dem |
| 4077 | // Balken, in dem sie liegen soll. Verhaeltnis wie im Grundlayout (22 zu 11). |
| 4078 | fontSize: Math.max( |
| 4079 | PLAN_MIN_FONT_SIZE, |
| 4080 | Math.min(DEFAULT_LAYOUT.fontSize, Math.round(rowHeight / 2)), |
| 4081 | ), |
| 4082 | }; |
| 4083 | } |
| 4084 | |
| 4085 | interface PlanAufteilung { |
| 4086 | readonly layout: PlanLayout; |
| 4087 | readonly blaetter: readonly (readonly PlannedSignalGroup[])[]; |
| 4088 | } |
| 4089 | |
| 4090 | /** |
| 4091 | * Layout und Aufteilung auf Blaetter, sodass jedes Blatt vollstaendig auf |
| 4092 | * seine Seite passt. |
| 4093 | * |
| 4094 | * Gesucht wird die kleinste Blattzahl, mit der die Zeichnung noch in den |
| 4095 | * erlaubten Grenzen darstellbar ist; die Signalgruppen werden darauf |
| 4096 | * GLEICHMAESSIG verteilt. Sonst entstuende bei 16 Signalgruppen ein Blatt mit |
| 4097 | * 14 Zeilen und eines mit 2 - fachlich unbrauchbar, weil sich die Zeilen zweier |
| 4098 | * so verschiedener Blaetter nicht mehr nebeneinanderlegen lassen. |
| 4099 | * |
| 4100 | * Jedes Blatt bekommt Zeitachse, Phasenband und Legende von drawSignalPlan |
| 4101 | * erneut. Deshalb werden hier die GRUPPEN geteilt und nicht die Zeichenflaeche |
| 4102 | * zerschnitten: Ein Blatt ohne Legende truege nur noch Farbe. |
| 4103 | */ |
| 4104 | function planAufteilung(plan: SignalPlan, verfuegbar: number): PlanAufteilung { |
| 4105 | const anzahl = Math.max(1, plan.groups.length); |
| 4106 | |
| 4107 | for (let blaetter = 1; blaetter <= anzahl; blaetter += 1) { |
| 4108 | const proBlatt = Math.ceil(anzahl / blaetter); |
| 4109 | const layout = planGrafikLayout(plan, proBlatt, verfuegbar); |
| 4110 | const hoehe = planHeight( |
| 4111 | { groups: plan.groups.slice(0, proBlatt), cycleResult: plan.cycleResult }, |
| 4112 | layout, |
| 4113 | ); |
| 4114 | if (hoehe <= verfuegbar) return { layout, blaetter: teileGruppen(plan.groups, proBlatt) }; |
| 4115 | } |
| 4116 | |
| 4117 | // Nicht erreichbar, solange eine einzelne Zeile in das kleinste Layout passt. |
| 4118 | // Der Rueckfall teilt auf je eine Signalgruppe je Blatt: lieber viele |
| 4119 | // Blaetter als eine Zeichnung, die ueber den Seitenrand hinauslaeuft. |
| 4120 | const kleinstes: PlanLayout = { |
| 4121 | ...DEFAULT_LAYOUT, |
| 4122 | rowHeight: PLAN_MIN_ROW_HEIGHT, |
| 4123 | rowGap: PLAN_MIN_ROW_GAP, |
| 4124 | fontSize: PLAN_MIN_FONT_SIZE, |
| 4125 | }; |
| 4126 | return { layout: kleinstes, blaetter: teileGruppen(plan.groups, 1) }; |
| 4127 | } |
| 4128 | |
| 4129 | function teileGruppen( |
| 4130 | gruppen: readonly PlannedSignalGroup[], |
| 4131 | proBlatt: number, |
| 4132 | ): readonly (readonly PlannedSignalGroup[])[] { |
| 4133 | if (gruppen.length <= proBlatt) return [gruppen]; |
| 4134 | const blaetter: (readonly PlannedSignalGroup[])[] = []; |
| 4135 | for (let von = 0; von < gruppen.length; von += proBlatt) { |
| 4136 | blaetter.push(gruppen.slice(von, von + proBlatt)); |
| 4137 | } |
| 4138 | return blaetter; |
| 4139 | } |
| 4140 | |
| 4141 | function drawPlanGraphic(context: Context, mitPhasenabschnitt: boolean): void { |
| 4142 | const { doc, plan } = context; |
| 4143 | |
| 4144 | /* |
| 4145 | * Der Platz fuer die beiden Absaetze unter der Zeichnung wird VORHER |
| 4146 | * abgezogen. Sie gehoeren auf dasselbe Blatt wie die Zeichnung, auf die sie |
| 4147 | * sich beziehen; ein Verweis auf die Textfassung, den man erst auf der |
| 4148 | * naechsten Seite findet, ist keiner. Der Preis ist eine leicht gestauchte |
| 4149 | * Zeichnung ab neun Signalgruppen - er ist geringer als der Verlust des |
| 4150 | * Verweises. |
| 4151 | */ |
| 4152 | /** |
| 4153 | * Hoehe der beiden Absaetze, wie sie mit dieser Blattangabe wirklich |
| 4154 | * gesetzt werden. |
| 4155 | * |
| 4156 | * KORREKTUR (Fassung 5.7.0): Gemessen wurde zuvor |
| 4157 | * `planVerweisAbsatz(1, 1, '')` - also die Fassung OHNE Blattangabe - und |
| 4158 | * dazu pauschal 18 pt fuer "eine Zeile und eine halbe zum Abrunden". Gedruckt |
| 4159 | * wird bei mehreren Blaettern aber "Blatt 1 von 2 der Zeichnung, |
| 4160 | * Signalgruppen <erste> bis <letzte>", und Signalgruppennamen sind freier |
| 4161 | * Anwendertext. Mit Namen, wie sie in einer Anordnungsunterlage vorkommen |
| 4162 | * ("K1 Zufahrt Nord, Hauptstraße stadteinwärts, Geradeaus und |
| 4163 | * Rechtsabbieger"), wuchs die Blattangabe ueber die eine reservierte Zeile |
| 4164 | * hinaus, `writeParagraph` rief `ensureSpace`, und die Fussnote landete auf |
| 4165 | * der Folgeseite - genau der Zustand, den der Kommentar oben ausschliesst. |
| 4166 | * Bei kurzen Namen (K1 bis K16) trat es nicht auf; die Pruefstuecke fuehrten |
| 4167 | * nur solche. |
| 4168 | */ |
| 4169 | const unterschriftHoehe = (blattangabe: string): number => |
| 4170 | // Abstand zwischen Zeichnung und Absatz - derselbe Wert wie unten beim |
| 4171 | // Setzen. Fehlt er hier, faellt die Fussnote auf die naechste Seite. |
| 4172 | PLAN_ABSATZ_ABSTAND + |
| 4173 | absatzHoehe(context, `${blattangabe}${planVerweisRumpf(mitPhasenabschnitt)}`) + |
| 4174 | absatzHoehe(context, PLAN_SCHMALE_ABSCHNITTE); |
| 4175 | |
| 4176 | const verfuegbarBei = (unterschrift: number): number => |
| 4177 | context.pageHeight - context.cursor - PAGE_MARGIN - FOOTER_HEIGHT - unterschrift; |
| 4178 | |
| 4179 | /* |
| 4180 | * Zwei Anlaeufe statt einer Schaetzung. |
| 4181 | * |
| 4182 | * Erster Anlauf ohne Blattangabe: Bleibt es bei EINEM Blatt, wird auch keine |
| 4183 | * gedruckt, und die Reserve stimmt auf den Punkt. Erst wenn geteilt wird, |
| 4184 | * kommt die Angabe hinzu - dann wird mit ihrer laengstmoeglichen Fassung |
| 4185 | * gerechnet und neu geteilt. Laengstmoeglich heisst: die hoechste Blattzahl, |
| 4186 | * die ueberhaupt entstehen kann (eine Signalgruppe je Blatt), und die beiden |
| 4187 | * laengsten Namen des Plans. Das ist eine obere Schranke fuer JEDE Aufteilung, |
| 4188 | * also auch fuer die, die der zweite Anlauf liefert - einen dritten Anlauf |
| 4189 | * kann es nicht brauchen. |
| 4190 | */ |
| 4191 | let aufteilung = planAufteilung(plan, verfuegbarBei(unterschriftHoehe(''))); |
| 4192 | if (aufteilung.blaetter.length > 1) { |
| 4193 | aufteilung = planAufteilung(plan, verfuegbarBei(unterschriftHoehe(laengsteBlattangabe(plan)))); |
| 4194 | } |
| 4195 | const { layout, blaetter } = aufteilung; |
| 4196 | |
| 4197 | blaetter.forEach((gruppen, index) => { |
| 4198 | if (index > 0) newSection(context, 'Signalzeitenplan (Fortsetzung)'); |
| 4199 | |
| 4200 | const blatt: SignalPlan = { ...plan, groups: gruppen }; |
| 4201 | const hoehe = planHeight(blatt, layout); |
| 4202 | const surface = new PdfSurface(doc, PAGE_MARGIN, context.cursor, context.contentWidth, hoehe); |
| 4203 | drawSignalPlan(surface, blatt, { theme: LIGHT_THEME, layout }); |
| 4204 | context.cursor += hoehe + PLAN_ABSATZ_ABSTAND; |
| 4205 | |
| 4206 | writeParagraph( |
| 4207 | context, |
| 4208 | planVerweisAbsatz(index + 1, blaetter.length, blattGruppen(gruppen), mitPhasenabschnitt), |
| 4209 | ); |
| 4210 | writeParagraph(context, PLAN_SCHMALE_ABSCHNITTE); |
| 4211 | }); |
| 4212 | } |
| 4213 | |
| 4214 | /** |
| 4215 | * Umlaufzeit als Drucktext - mit Vermerk, wenn sie kein Bemessungsergebnis ist. |
| 4216 | * |
| 4217 | * KORREKTUR (Befund C11): Bei Uebersaettigung traegt der Plan die groesste |
| 4218 | * zulaessige Umlaufzeit als Ersatzwert (bounded 'uebersaettigt'); Deckblatt und |
| 4219 | * Signalzeiten druckten "120 s" wie ein Ergebnis. Jede Stelle, die die |
| 4220 | * Umlaufzeit druckt, laeuft ueber diese Funktion; Wortlaut und Zusammenfuegen |
| 4221 | * kommen aus dem Fachkern. |
| 4222 | */ |
| 4223 | function umlaufzeitText(plan: Pick<SignalPlan, 'cycleTime' | 'cycleResult'>): string { |
| 4224 | return umlaufzeitMitVermerk(plan, fmt.seconds(plan.cycleTime)); |
| 4225 | } |
| 4226 | |
| 4227 | /** |
| 4228 | * Absatz ueber einer Tabelle, deren Zahlen auf der Umlaufzeit stehen, wenn |
| 4229 | * diese nur ein Ersatzwert ist (Befund C11). Ohne den Absatz staenden Rot- |
| 4230 | * zeiten, Freigabegrenzen, Phasenbeginne und Kapazitaeten zu einer Zahl, die |
| 4231 | * kein Ergebnis ist. Steht vor JEDER dieser Tabellen - Signalzeiten, Phasen, |
| 4232 | * Leistungsfaehigkeit, Rechenweg Wartezeit -, nicht nur vor einigen: Die |
| 4233 | * CSV-Pendants tragen den Vermerk mit derselben Begruendung. |
| 4234 | */ |
| 4235 | function ersatzwertAbsatz(context: Context): void { |
| 4236 | const { plan } = context; |
| 4237 | if (!umlaufzeitIstErsatzwert(plan)) return; |
| 4238 | writeParagraph( |
| 4239 | context, |
| 4240 | `Umlaufzeit ${umlaufzeitText(plan)}. Die Anlage ist übersättigt; eine Umlaufzeit lässt sich ` + |
| 4241 | 'nicht ermitteln. Die folgenden Zeiten beruhen auf dem Ersatzwert und sind kein ' + |
| 4242 | 'Bemessungsergebnis - siehe Prüfbericht.', |
| 4243 | ); |
| 4244 | } |
| 4245 | |
| 4246 | function drawSignalTimeTable(context: Context): void { |
| 4247 | const { doc, plan } = context; |
| 4248 | doc.addPage(); |
| 4249 | // Eigene Seite ohne newSection - das Lesezeichen braucht sie trotzdem: Sie |
| 4250 | // ist die Textfassung der Zeichnung und der Ort, auf den der Verweisabsatz |
| 4251 | // unter der Zeichnung zeigt. |
| 4252 | merkeLesezeichen(context, 'Signalzeiten je Signalgruppe'); |
| 4253 | context.cursor = PAGE_MARGIN + HEADER_HEIGHT; |
| 4254 | |
| 4255 | doc.setFont('helvetica', 'bold'); |
| 4256 | doc.setFontSize(12); |
| 4257 | doc.text(toWinAnsi('Signalzeiten je Signalgruppe'), PAGE_MARGIN, context.cursor); |
| 4258 | context.cursor += 18; |
| 4259 | |
| 4260 | ersatzwertAbsatz(context); |
| 4261 | |
| 4262 | autoTable(doc, { |
| 4263 | startY: context.cursor, |
| 4264 | margin: TABELLENRAND, |
| 4265 | theme: 'grid', |
| 4266 | styles: { fontSize: 9, cellPadding: 3 }, |
| 4267 | headStyles: { fillColor: '#eef1f6', textColor: '#1b2230', fontStyle: 'bold' }, |
| 4268 | head: [ |
| 4269 | /* |
| 4270 | * KORREKTUR (Befund C22): Hier standen zwei Spalten "Freigabe von" und |
| 4271 | * "Freigabe bis", gefuellt aus greens[0] - dem ersten Fenster -, waehrend |
| 4272 | * "Freigabezeit" die Summe aller Fenster nannte. Eine Signalgruppe in |
| 4273 | * zwei getrennten Phasen erschien damit mit einem Fenster und einer |
| 4274 | * Freigabezeit, die dazu nicht passte; das zweite Fenster kam im Ausdruck |
| 4275 | * ueberhaupt nicht vor. Jetzt eine Spalte mit allen Fenstern |
| 4276 | * (freigabefensterZelle, gleiche Zeichenkette wie in der Tabellenausgabe) |
| 4277 | * und eine Summenspalte, die Summe heisst. |
| 4278 | */ |
| 4279 | [ |
| 4280 | 'SG', |
| 4281 | 'Freigabefenster (von–bis) [s]', |
| 4282 | 'Freigabezeit (Summe)', |
| 4283 | 'Rot-Gelb', |
| 4284 | 'Gelb', |
| 4285 | 'Rot', |
| 4286 | ].map(toWinAnsi), |
| 4287 | ], |
| 4288 | body: plan.groups.map((group) => { |
| 4289 | const totals = aspectTotals(group, plan.cycleTime); |
| 4290 | return [ |
| 4291 | group.name, |
| 4292 | freigabefensterZelle(group.greens, plan.cycleTime), |
| 4293 | fmt.seconds(group.totalGreen), |
| 4294 | fmt.seconds(totals.rotgelb), |
| 4295 | fmt.seconds(totals.gelb), |
| 4296 | fmt.seconds(totals.rot), |
| 4297 | ].map(toWinAnsi); |
| 4298 | }), |
| 4299 | }); |
| 4300 | |
| 4301 | context.cursor = lastTableBottom(doc) + 10; |
| 4302 | // Ohne die Fussnote bliebe offen, warum in einer Zeile zwei Fenster stehen |
| 4303 | // und was die Summenspalte summiert (Befund C22). |
| 4304 | writeParagraph(context, FREIGABEFENSTER_FUSSNOTE); |
| 4305 | context.cursor += 6; |
| 4306 | } |
| 4307 | |
| 4308 | // --- Leistungsfaehigkeit ---------------------------------------------------- |
| 4309 | |
| 4310 | /** |
| 4311 | * Bewertungsverfahren des Plans - dieselbe Quelle wie im Fachkern |
| 4312 | * (project.settings.serviceLevelScale bezeichnet seit Fassung 5.4.0, |
| 4313 | * Befund B2, das ganze Verfahren, nicht nur die Stufentafel). |
| 4314 | */ |
| 4315 | function bewertungsverfahren(context: Context): Bewertungsverfahren { |
| 4316 | return context.project.settings.serviceLevelScale; |
| 4317 | } |
| 4318 | |
| 4319 | /** Signalgruppen, die ueber Kapazitaet und mittlere Wartezeit bewertet werden. */ |
| 4320 | function kfzOepnvGruppen(plan: SignalPlan): readonly PlannedSignalGroup[] { |
| 4321 | return plan.groups.filter((group) => hatSaettigungsverkehrsstaerke(group.mode)); |
| 4322 | } |
| 4323 | |
| 4324 | /** Signalgruppen, die ueber die maximale Wartezeit bewertet werden. */ |
| 4325 | function fussRadGruppen(plan: SignalPlan): readonly PlannedSignalGroup[] { |
| 4326 | return plan.groups.filter((group) => group.mode === 'fuss' || group.mode === 'rad'); |
| 4327 | } |
| 4328 | |
| 4329 | /** |
| 4330 | * Instationaritaetsfaktor, mit dem TATSAECHLICH gerechnet wurde. |
| 4331 | * |
| 4332 | * Aus den Ergebnissen, nicht aus der Vorgabe: Ein ungueltiger Vorgabewert |
| 4333 | * faellt in delay.ts auf den Regelwert zurueck, und der Absatz unter der |
| 4334 | * Tabelle muss die Zahl nennen, die in tW eingegangen ist - sonst rechnet der |
| 4335 | * Pruefer mit der falschen nach. Ohne bewertete Gruppe gilt der Wert der |
| 4336 | * Vorgabenverwaltung. |
| 4337 | */ |
| 4338 | function angesetzterInstationaritaetsfaktor(context: Context): number { |
| 4339 | for (const group of kfzOepnvGruppen(context.plan)) { |
| 4340 | const fin = group.delay?.rechenweg.instationaritaetsfaktor; |
| 4341 | if (fin !== null && fin !== undefined && Number.isFinite(fin)) return fin; |
| 4342 | } |
| 4343 | return context.plan.defaults.capacity.instationaritaetsfaktor; |
| 4344 | } |
| 4345 | |
| 4346 | /** |
| 4347 | * Leistungsfaehigkeit der Kfz- und OePNV-Signalgruppen. |
| 4348 | * |
| 4349 | * KORREKTUR (Fassung 5.4.0, Befunde B1 bis B4): Bis dahin behauptete |
| 4350 | * der Absatz unter dieser Tabelle "Bewertung nach HBS 2015" - waehrend fuer |
| 4351 | * beide Einstellungen dieselbe HCM-Formel gerechnet und nach der Kfz-Tafel des |
| 4352 | * HBS 2001 bewertet wurde. Und er erklaerte, Fussgaengersignalgruppen seien |
| 4353 | * "nicht aufgefuehrt, da ihre Leistungsfaehigkeit ... nicht massgebend ist" - |
| 4354 | * das HBS 2015 bewertet sie sehr wohl, nur nach einem anderen Kriterium |
| 4355 | * (maximale Wartezeit, eigene Tabelle unten). Jetzt steht hier das Verfahren, |
| 4356 | * das gerechnet wurde, mit seiner Formelkette und seiner Tafel, und die Spalte |
| 4357 | * "Abflusszeit tA" macht die Kapazitaet aus den gedruckten Groessen |
| 4358 | * nachrechenbar (C = qS · tA/tU). |
| 4359 | */ |
| 4360 | function drawPerformanceTable(context: Context, mitSignalgruppentabelle: boolean): void { |
| 4361 | const { doc, plan, project } = context; |
| 4362 | const verfahren = bewertungsverfahren(context); |
| 4363 | const gruppen = kfzOepnvGruppen(plan); |
| 4364 | |
| 4365 | ensureSpace(context, 60); |
| 4366 | doc.setFont('helvetica', 'bold'); |
| 4367 | doc.setFontSize(11); |
| 4368 | doc.setTextColor('#1b2230'); |
| 4369 | doc.text(toWinAnsi('Kraftfahrzeuge und ÖPNV'), PAGE_MARGIN, context.cursor); |
| 4370 | context.cursor += 16; |
| 4371 | |
| 4372 | if (gruppen.length === 0) { |
| 4373 | writeParagraph(context, 'Es sind keine Kfz- oder ÖPNV-Signalgruppen erfasst.'); |
| 4374 | return; |
| 4375 | } |
| 4376 | |
| 4377 | // Kapazitaet, Auslastung und Wartezeit rechnen mit der Umlaufzeit. |
| 4378 | ersatzwertAbsatz(context); |
| 4379 | |
| 4380 | const rows = gruppen.map((group) => { |
| 4381 | const demand = project.demands.find((d) => d.signalGroupId === group.groupId); |
| 4382 | return [ |
| 4383 | group.name, |
| 4384 | group.capacity === null ? '–' : fmt.vehiclesPerHour(group.capacity.saturationFlow), |
| 4385 | group.capacity === null ? '–' : fmt.seconds(group.capacity.abflusszeit, 1), |
| 4386 | group.capacity === null ? '–' : fmt.vehiclesPerHour(group.capacity.capacity), |
| 4387 | // "–" auch bei 0 (Fassung 5.10.0): Ein Datensatz ist noch keine |
| 4388 | // Zaehlung - wird allein der Schwerverkehrsanteil ausgefuellt, entsteht |
| 4389 | // ein Datensatz mit `volume: 0`. Der Fachkern liest ihn seit den Befunden |
| 4390 | // 12 und 18 als "keine Angabe" (signalPlan.ts, erfassteVerkehrsstaerke), |
| 4391 | // der Auslastungsgrad zwei Spalten weiter steht dann als "–" da und der |
| 4392 | // Bildschirm zeichnet das Feld seit Befund 58 leer. Hier stand "0 Fz/h": |
| 4393 | // eine Zaehlung, die niemand vorgenommen hat, neben zwei Strichen |
| 4394 | // derselben Zeile. Dieselbe Bedingung wie in `schwerverkehrZelle` |
| 4395 | // daneben. |
| 4396 | demand === undefined || demand.volume <= 0 ? '–' : fmt.vehiclesPerHour(demand.volume), |
| 4397 | schwerverkehrZelle(project, group.groupId), |
| 4398 | // Die Geometrie der Zufahrt: Sie mindert die Saettigungsverkehrsstaerke |
| 4399 | // der Spalte ganz links, und ohne sie liesse sich diese Zahl aus dem |
| 4400 | // Fundstellenverzeichnis nicht herleiten - derselbe Grund wie beim |
| 4401 | // Schwerverkehr daneben. |
| 4402 | geometrieZelle(project, group.groupId), |
| 4403 | // Drei Nachkommastellen wie im Rechenweg und im Pruefbericht: Mit zwei |
| 4404 | // stand bei q = 244 Fz/h und C = 243 Fz/h "1,00" neben "F (ueberlastet)" |
| 4405 | // - die gedruckte Zahl widersprach der gedruckten Stufe. |
| 4406 | group.capacity?.degreeOfSaturation === undefined |
| 4407 | ? '–' |
| 4408 | : fmt.ratio(group.capacity.degreeOfSaturation, 3), |
| 4409 | group.delay === null ? '–' : fmt.seconds(group.delay.averageDelay, 1), |
| 4410 | group.delay === null ? '–' : stufeZelle(group.delay.serviceLevel), |
| 4411 | ].map(toWinAnsi); |
| 4412 | }); |
| 4413 | |
| 4414 | autoTable(doc, { |
| 4415 | startY: context.cursor, |
| 4416 | margin: TABELLENRAND, |
| 4417 | theme: 'grid', |
| 4418 | styles: { fontSize: 9, cellPadding: 3 }, |
| 4419 | headStyles: { fillColor: '#eef1f6', textColor: '#1b2230', fontStyle: 'bold' }, |
| 4420 | head: [ |
| 4421 | [ |
| 4422 | 'SG', |
| 4423 | 'Sättigungsverkehrsstärke', |
| 4424 | // Nach HBS 2015 tF + 1 s je Freigabezeitfenster, nach HCM die |
| 4425 | // Freigabezeit selbst - der Absatz unter der Tabelle sagt, was gilt. |
| 4426 | 'Abflusszeit tA', |
| 4427 | 'Kapazität', |
| 4428 | 'Verkehrsstärke', |
| 4429 | // Der Anteil mindert die Saettigungsverkehrsstaerke, stand aber in |
| 4430 | // keiner Ausgabe. Der Pruefer las eine Kapazitaet, die er aus dem |
| 4431 | // Fundstellenverzeichnis nicht herleiten konnte, und hatte keine |
| 4432 | // Moeglichkeit, die Differenz aufzuloesen. Seit Schema 13 stehen in |
| 4433 | // der Zelle drei Zeilen: Anteil, Aufteilung und fSV mit der Gleichung |
| 4434 | // (siehe schwerverkehrZelle). |
| 4435 | 'Schwerverkehr (Anteil, Aufteilung, fSV)', |
| 4436 | 'Geometrie (b, R, s; Faktor)', |
| 4437 | 'Auslastungsgrad', |
| 4438 | 'Mittlere Wartezeit', |
| 4439 | 'Qualitätsstufe', |
| 4440 | ].map(toWinAnsi), |
| 4441 | ], |
| 4442 | body: rows, |
| 4443 | }); |
| 4444 | |
| 4445 | context.cursor = lastTableBottom(doc) + 10; |
| 4446 | writeParagraph(context, verfahrensAbsatz(context, verfahren, gruppen, mitSignalgruppentabelle)); |
| 4447 | } |
| 4448 | |
| 4449 | /** |
| 4450 | * Wo n und die Fahrbeziehung stehen, aus denen die gedruckte |
| 4451 | * Saettigungsverkehrsstaerke entsteht (Fassung 5.10.0; dasselbe Muster wie beim |
| 4452 | * Anfahransatz, Fassung 5.9.0). |
| 4453 | * |
| 4454 | * Der Erlaeuterungsabsatz nennt die Kette qS0 · n · fA / fSV. Die |
| 4455 | * Leistungstabelle fuehrt keine Fahrstreifenspalte, weil n und die |
| 4456 | * Fahrbeziehung in der Signalgruppentabelle stehen - eine halbe Wiederholung |
| 4457 | * dort waere ohne die Fahrbeziehung derselben Zeile kein geschlossenes Glied. |
| 4458 | * Diese Begruendung faellt jedoch, sobald der Anwender die Tabelle |
| 4459 | * "Signalgruppen" abwaehlt: Dann stand die Kette in einer Unterlage, in der |
| 4460 | * weder "Fahrstr." noch "Fahrbeziehung" vorkommt und aus keiner gedruckten |
| 4461 | * Groesse zu schliessen ist. In diesem Fall traegt der Absatz beides selbst |
| 4462 | * nach - Name, Fahrstreifenzahl und Fahrbeziehung je Strom der Tabelle. |
| 4463 | * |
| 4464 | * Die Fahrstreifenzahl ueber `fmt` und nicht ueber `String` (Befund 55): Eine |
| 4465 | * Projektdatei mit "lanes": 2.5 wird uebernommen, und die englische |
| 4466 | * Schreibweise haette sich hier eine zweite Stelle gesucht. |
| 4467 | */ |
| 4468 | function fahrstreifenHerkunftSatz( |
| 4469 | project: Project, |
| 4470 | gruppen: readonly PlannedSignalGroup[], |
| 4471 | mitSignalgruppentabelle: boolean, |
| 4472 | ): string { |
| 4473 | if (mitSignalgruppentabelle) { |
| 4474 | return ' n und die Fahrbeziehung stehen je Strom in der Tabelle "Signalgruppen" (Spalten "Fahrstr." und "Fahrbeziehung").'; |
| 4475 | } |
| 4476 | // Der Name in Anfuehrungszeichen, damit Name und Zahl in der Aufzaehlung |
| 4477 | // nicht unverbunden hintereinander stehen - wie beim Anfahransatz. |
| 4478 | const angaben = gruppen.map((gruppe) => { |
| 4479 | const group = project.signalGroups.find((g) => g.id === gruppe.groupId); |
| 4480 | return group === undefined |
| 4481 | ? `"${gruppe.name}" ohne Angabe` |
| 4482 | : `"${group.name}" ${fmt.numShort(group.lanes)} Fahrstreifen, ${MOVEMENT_LABELS[group.movement]}`; |
| 4483 | }); |
| 4484 | return ( |
| 4485 | ' Die Tabelle "Signalgruppen", in der n und die Fahrbeziehung stehen, ist für diese Ausgabe ' + |
| 4486 | `abgewählt; für die Ströme dieser Tabelle gilt: ${angaben.join('; ')}.` |
| 4487 | ); |
| 4488 | } |
| 4489 | |
| 4490 | /** |
| 4491 | * Der Absatz, der sagt, WAS gerechnet wurde. Zahlen und Tafeln kommen aus den |
| 4492 | * Konstanten des Fachkerns (bewertung.ts), nicht aus dem Satz. |
| 4493 | */ |
| 4494 | function verfahrensAbsatz( |
| 4495 | context: Context, |
| 4496 | verfahren: Bewertungsverfahren, |
| 4497 | gruppen: readonly PlannedSignalGroup[], |
| 4498 | mitSignalgruppentabelle: boolean, |
| 4499 | ): string { |
| 4500 | const tafeln = stufentafeln(verfahren); |
| 4501 | const mitOepnv = gruppen.some((g) => g.mode === 'oepnv'); |
| 4502 | /* |
| 4503 | * KORREKTUR (Fassung 5.5.0, Frage E6): Hier stand "Der Schwerverkehrsanteil |
| 4504 | * mindert die Saettigungsverkehrsstaerke (Pkw-Aequivalent 2,0)" - unter der |
| 4505 | * Ueberschrift "Bewertung nach HBS 2015". Die 2,0 sind der Pkw-Gleichwert des |
| 4506 | * HCM 2010; das HBS rechnet mit dem Anpassungsfaktor fSV. Der Satz kommt |
| 4507 | * jetzt aus bewertung.ts und nennt die ganze Kette, mit den Vorgaben dieses |
| 4508 | * Plans. |
| 4509 | * |
| 4510 | * MIT DER DATENLAGE DIESES PLANS (Schema 13): Beide Gleichungen des HBS sind |
| 4511 | * im Einsatz, und welche gilt, entscheidet der einzelne Strom. Der Absatz |
| 4512 | * bekommt deshalb die Zaehlung mit - sonst stuende unter einer Tabelle, in |
| 4513 | * der jeder Strom eine erfasste Aufteilung hat, weiter der Vorbehalt zur |
| 4514 | * fehlenden Aufteilung. |
| 4515 | */ |
| 4516 | const schwerverkehr = |
| 4517 | schwerverkehrSatz(context.plan.defaults, aufteilungslage(context.project)) + |
| 4518 | // Wo n und fA in DIESER Ausgabe stehen - der Satz aus bewertung.ts nennt |
| 4519 | // die Kette, nicht ihre Fundstelle. |
| 4520 | fahrstreifenHerkunftSatz(context.project, gruppen, mitSignalgruppentabelle); |
| 4521 | |
| 4522 | if (verfahren === 'HBS') { |
| 4523 | const fin = fmt.numShort(angesetzterInstationaritaetsfaktor(context), 2); |
| 4524 | return ( |
| 4525 | `Bewertung nach ${verfahrenLabel(verfahren)}: Kapazität C = qS · tA/tU mit der Abflusszeit tA = tF + 1 s je ` + |
| 4526 | 'Freigabezeitfenster; mittlere Wartezeit tW = tW,G + tW,R (Grundwartezeit und Wartezeit im ' + |
| 4527 | `Reststau, Betrachtungszeitraum T = 1 h, Instationaritätsfaktor fin = ${fin}); ` + |
| 4528 | `Qualitätsstufe nach der mittleren Wartezeit (Kfz: ${tafeln.kfz}` + |
| 4529 | (mitOepnv ? `; ÖPNV: ${tafeln.oepnv}` : '') + |
| 4530 | '); Stufe F bei Überlastung (q > C), unabhängig von der Wartezeit. ' + |
| 4531 | schwerverkehr |
| 4532 | ); |
| 4533 | } |
| 4534 | return ( |
| 4535 | `Bewertung nach ${verfahrenLabel(verfahren)} (Highway Capacity Manual): Kapazität C = qS · g/tU mit der Freigabezeit ` + |
| 4536 | 'g = tF (Spalte "Abflusszeit tA"); mittlere Wartezeit d = d1 + d2 (gleichmäßiger Anteil und ' + |
| 4537 | 'Überlastungsanteil, Bemessungszeitraum T = 0,25 h, k = 0,5, I = 1,0); Qualitätsstufe nach ' + |
| 4538 | `der mittleren Wartezeit (${tafeln.kfz}), Stufe F außerdem bei Überlastung (q > C). ` + |
| 4539 | schwerverkehr |
| 4540 | ); |
| 4541 | } |
| 4542 | |
| 4543 | /** |
| 4544 | * Fussgaenger und Radverkehr. |
| 4545 | * |
| 4546 | * KORREKTUR (Fassung 5.4.0, Befund B4): Das HBS 2015 bewertet Fussgaenger und |
| 4547 | * Radverkehr an Lichtsignalanlagen nach der MAXIMALEN Wartezeit - der laengsten |
| 4548 | * Sperrzeit im Umlauf - mit eigener Tafel; eine Kapazitaet und ein |
| 4549 | * Auslastungsgrad werden dafuer nicht gebildet. Bis dahin fehlten die |
| 4550 | * Fussgaenger im Leistungsabschnitt ganz, und Radgruppen standen mit einer |
| 4551 | * Kfz-Kapazitaet in der Kfz-Tabelle. Nach HCM gibt es fuer beide keine Stufe; |
| 4552 | * das steht dann ausdruecklich hier, statt eine Zelle leer zu lassen. |
| 4553 | */ |
| 4554 | function drawFussRadTable(context: Context): void { |
| 4555 | const { doc, plan } = context; |
| 4556 | const gruppen = fussRadGruppen(plan); |
| 4557 | if (gruppen.length === 0) return; |
| 4558 | const verfahren = bewertungsverfahren(context); |
| 4559 | const tafeln = stufentafeln(verfahren); |
| 4560 | |
| 4561 | ensureSpace(context, 90); |
| 4562 | doc.setFont('helvetica', 'bold'); |
| 4563 | doc.setFontSize(11); |
| 4564 | doc.setTextColor('#1b2230'); |
| 4565 | doc.text(toWinAnsi('Fußgänger und Radverkehr'), PAGE_MARGIN, context.cursor); |
| 4566 | context.cursor += 16; |
| 4567 | |
| 4568 | autoTable(doc, { |
| 4569 | startY: context.cursor, |
| 4570 | margin: TABELLENRAND, |
| 4571 | theme: 'grid', |
| 4572 | styles: { fontSize: 9, cellPadding: 3 }, |
| 4573 | headStyles: { fillColor: '#eef1f6', textColor: '#1b2230', fontStyle: 'bold' }, |
| 4574 | head: [ |
| 4575 | [ |
| 4576 | 'SG', |
| 4577 | 'Verkehrsart', |
| 4578 | 'Freigabezeit', |
| 4579 | 'Längste Sperrzeit = maximale Wartezeit', |
| 4580 | 'Mittlere Wartezeit', |
| 4581 | 'Qualitätsstufe', |
| 4582 | ].map(toWinAnsi), |
| 4583 | ], |
| 4584 | body: gruppen.map((group) => |
| 4585 | [ |
| 4586 | group.name, |
| 4587 | TRAFFIC_MODE_LABELS[group.mode], |
| 4588 | fmt.seconds(group.totalGreen), |
| 4589 | group.delay === null ? '–' : fmt.seconds(group.delay.maximumDelay, 1), |
| 4590 | group.delay === null ? '–' : fmt.seconds(group.delay.averageDelay, 1), |
| 4591 | group.delay === null ? '–' : stufeZelle(group.delay.serviceLevel), |
| 4592 | ].map(toWinAnsi), |
| 4593 | ), |
| 4594 | }); |
| 4595 | |
| 4596 | context.cursor = lastTableBottom(doc) + 10; |
| 4597 | |
| 4598 | const mittlere = |
| 4599 | 'Die mittlere Wartezeit gilt für über den Umlauf gleichverteilt eintreffende Fußgänger bzw. ' + |
| 4600 | 'Radfahrer (Summe der Sperrzeitquadrate / (2 · tU)); sie ist eine Kenngröße für den ' + |
| 4601 | 'Variantenvergleich, keine Bewertungsgröße. Kapazität und Auslastungsgrad werden für ' + |
| 4602 | 'Fußgänger und Radverkehr nicht gebildet.'; |
| 4603 | |
| 4604 | writeParagraph( |
| 4605 | context, |
| 4606 | tafeln.fussRad !== null |
| 4607 | ? 'Nach HBS 2015 werden Fußgänger und Radverkehr nach der maximalen Wartezeit bewertet - ' + |
| 4608 | `der längsten Sperrzeit im Umlauf: ${tafeln.fussRad}. ` + |
| 4609 | mittlere |
| 4610 | : 'Nach HCM wird für Fußgänger und Radverkehr keine Stufe vergeben; die HCM-Fußgängerbewertung ' + |
| 4611 | 'ist in diesem Programm nicht hinterlegt. Ausgewiesen wird die maximale Wartezeit (längste ' + |
| 4612 | 'Sperrzeit im Umlauf), nach der das HBS 2015 bewerten würde. ' + |
| 4613 | mittlere, |
| 4614 | ); |
| 4615 | } |
| 4616 | |
| 4617 | /** |
| 4618 | * Rechenweg der mittleren Wartezeit je Kfz- und OePNV-Signalgruppe. |
| 4619 | * |
| 4620 | * Grundsatz: Was gedruckt wird, muss sich aus den gedruckten Groessen |
| 4621 | * nachrechnen lassen. Die Leistungstabelle nennt tW; hier stehen die |
| 4622 | * Zwischenwerte, aus denen es entsteht (HBS: tA, fA, x, tW,G, NGE, tW,R; HCM: |
| 4623 | * g, x, d1, d2 mit T, k, I), und darunter die Formeln. |
| 4624 | */ |
| 4625 | function drawDelayDetails(context: Context): void { |
| 4626 | const { doc, plan } = context; |
| 4627 | const verfahren = bewertungsverfahren(context); |
| 4628 | const gruppen = kfzOepnvGruppen(plan).filter((group) => group.delay !== null); |
| 4629 | |
| 4630 | if (gruppen.length === 0) { |
| 4631 | writeParagraph( |
| 4632 | context, |
| 4633 | 'Für keine Kfz- oder ÖPNV-Signalgruppe ist eine Verkehrsstärke erfasst; eine Wartezeit ' + |
| 4634 | 'wurde nicht gerechnet.', |
| 4635 | ); |
| 4636 | return; |
| 4637 | } |
| 4638 | |
| 4639 | // tU ist bei Uebersaettigung nur der Ersatzwert; das gehoert VOR die |
| 4640 | // Formelkette, die mit ihm rechnet (Befund C11). Die Spalte selbst bleibt |
| 4641 | // die Zahl - der Vermerk in vierzehn Spalten je Zeile waere unlesbar. |
| 4642 | ersatzwertAbsatz(context); |
| 4643 | |
| 4644 | const head = |
| 4645 | verfahren === 'HBS' |
| 4646 | ? [ |
| 4647 | 'SG', |
| 4648 | 'tU', |
| 4649 | 'tF', |
| 4650 | 'tA', |
| 4651 | 'fA = tA/tU', |
| 4652 | 'C', |
| 4653 | 'q', |
| 4654 | 'x = q/C', |
| 4655 | 'fin', |
| 4656 | 'tW,G', |
| 4657 | 'NGE', |
| 4658 | 'tW,R', |
| 4659 | 'tW', |
| 4660 | 'Stufe', |
| 4661 | ] |
| 4662 | : [ |
| 4663 | 'SG', |
| 4664 | 'tU', |
| 4665 | 'g = tF', |
| 4666 | 'g/tU', |
| 4667 | 'C', |
| 4668 | 'q', |
| 4669 | 'x = q/C', |
| 4670 | 'T', |
| 4671 | 'k', |
| 4672 | 'I', |
| 4673 | 'd1', |
| 4674 | 'd2', |
| 4675 | 'd', |
| 4676 | 'Stufe', |
| 4677 | ]; |
| 4678 | |
| 4679 | const body = gruppen.map((group) => { |
| 4680 | const delay = group.delay!; |
| 4681 | const capacity = group.capacity; |
| 4682 | const rw = delay.rechenweg; |
| 4683 | const demand = context.project.demands.find((d) => d.signalGroupId === group.groupId); |
| 4684 | const gemeinsam = [group.name, fmt.seconds(plan.cycleTime)]; |
| 4685 | const kapazitaet = [ |
| 4686 | capacity === null ? '–' : fmt.vehiclesPerHour(capacity.capacity), |
| 4687 | // Dieselbe Bedingung wie in der Leistungstabelle darueber, damit beide |
| 4688 | // Tabellen dieselbe Aussage machen. Erreichbar ist der Strich hier |
| 4689 | // nicht: Diese Tabelle fuehrt nur Gruppen mit `delay !== null`, und ein |
| 4690 | // Wartezeitergebnis entsteht nur aus einer ERFASSTEN Verkehrsstaerke |
| 4691 | // (signalPlan.ts) - `volume <= 0` gilt dort als keine. |
| 4692 | demand === undefined || demand.volume <= 0 ? '–' : fmt.vehiclesPerHour(demand.volume), |
| 4693 | fmt.ratio(delay.degreeOfSaturation, 3), |
| 4694 | ]; |
| 4695 | // Stufe mit Kriterium: "F (Ueberlastung (q > C))" oder "B (mittlere |
| 4696 | // Wartezeit)" - der Zusatz "ueberlastet" aus der Leistungstabelle waere |
| 4697 | // hier doppelt. |
| 4698 | const stufe = |
| 4699 | delay.serviceLevel === null |
| 4700 | ? '–' |
| 4701 | : `${delay.serviceLevel.grade} (${KRITERIUM_LABELS[delay.serviceLevel.kriterium]})`; |
| 4702 | if (verfahren === 'HBS') { |
| 4703 | return [ |
| 4704 | ...gemeinsam, |
| 4705 | fmt.seconds(group.totalGreen, 1), |
| 4706 | fmt.seconds(rw.abflusszeit, 1), |
| 4707 | fmt.ratio(rw.abflussanteil, 3), |
| 4708 | ...kapazitaet, |
| 4709 | rw.instationaritaetsfaktor === null ? '–' : fmt.numShort(rw.instationaritaetsfaktor, 2), |
| 4710 | fmt.seconds(delay.uniformDelay, 1), |
| 4711 | rw.reststau === null ? '–' : fmt.numShort(rw.reststau, 2), |
| 4712 | fmt.seconds(delay.incrementalDelay, 1), |
| 4713 | fmt.seconds(delay.averageDelay, 1), |
| 4714 | stufe, |
| 4715 | ].map(toWinAnsi); |
| 4716 | } |
| 4717 | return [ |
| 4718 | ...gemeinsam, |
| 4719 | fmt.seconds(rw.abflusszeit, 1), |
| 4720 | fmt.ratio(rw.abflussanteil, 3), |
| 4721 | ...kapazitaet, |
| 4722 | `${fmt.numShort(rw.betrachtungszeitraum, 2)} h`, |
| 4723 | rw.hcmK === null ? '–' : fmt.num(rw.hcmK, 1), |
| 4724 | rw.hcmI === null ? '–' : fmt.num(rw.hcmI, 1), |
| 4725 | fmt.seconds(delay.uniformDelay, 1), |
| 4726 | fmt.seconds(delay.incrementalDelay, 1), |
| 4727 | fmt.seconds(delay.averageDelay, 1), |
| 4728 | stufe, |
| 4729 | ].map(toWinAnsi); |
| 4730 | }); |
| 4731 | |
| 4732 | autoTable(doc, { |
| 4733 | startY: context.cursor, |
| 4734 | margin: TABELLENRAND, |
| 4735 | theme: 'grid', |
| 4736 | styles: { fontSize: 8, cellPadding: 2.5, valign: 'top' }, |
| 4737 | headStyles: { fillColor: '#eef1f6', textColor: '#1b2230', fontStyle: 'bold' }, |
| 4738 | head: [head.map(toWinAnsi)], |
| 4739 | body, |
| 4740 | }); |
| 4741 | |
| 4742 | context.cursor = lastTableBottom(doc) + 10; |
| 4743 | |
| 4744 | /* |
| 4745 | * Formeln in ASCII-nahen Zeichen: Wurzelzeichen und das typografische Minus |
| 4746 | * liegen ausserhalb des WinAnsi-Vorrats und wuerden von toWinAnsi entfernt - |
| 4747 | * aus "(x - 1)" wuerde "(x 1)". |
| 4748 | */ |
| 4749 | writeParagraph( |
| 4750 | context, |
| 4751 | verfahren === 'HBS' |
| 4752 | ? 'Formeln nach HBS 2015 (Leyn 2016, Gl. 2-7 bis 2-10 und 3-2): tW = tW,G + tW,R; ' + |
| 4753 | 'tW,G = tU · (1 - fA)² / (2 · (1 - min(1; x) · fA)) mit fA = tA/tU und tA = tF + 1 s je ' + |
| 4754 | 'Freigabezeitfenster; tW,R = NGE · 3600 / C; NGE = max { 0,58 · T · C/4 · [(fin · x - 1) + ' + |
| 4755 | 'Wurzel aus ((fin · x - 1)² + 4 · fin · x / (0,58 · T · C))] ; T · C/4 · [(x - 1) + ' + |
| 4756 | 'Wurzel aus ((x - 1)² + 4 · x / (T · C))] } mit T = 1 h; fin = 1 + ((q15/q) - 1) / 1,5 ' + |
| 4757 | '(Vorgabewert 1,1 ohne 15-Minuten-Zähldaten). Angaben mit Zwischenrundung auf die ' + |
| 4758 | 'gedruckten Stellen; die Kapazität C ist ganzzahlig, x = q/C daraus gebildet. ' + |
| 4759 | /* |
| 4760 | * Bis hierher endete der Absatz vor der Fassung 5.11.0 - und |
| 4761 | * ein Pruefer, der die Zeile nach der genannten Gleichung nachrechnet, |
| 4762 | * kam an rund der Haelfte der Zeilen auf eine andere Zahl als die |
| 4763 | * gedruckte (im Regelbeispiel K1: 5,7 gegen 5,6 und K4: 3,8 gegen 3,7). |
| 4764 | * Der Grund steht in delay.ts und ist begruendet: Die Anteile muessen |
| 4765 | * auf den gedruckten Ziffern aufgehen, sonst widerlegt sich die Zeile |
| 4766 | * selbst. Ungesagt blieb er trotzdem, waehrend der Zusatz |
| 4767 | * "Zwischenrundung auf die gedruckten Stellen" ausdruecklich dazu |
| 4768 | * auffordert, mit ihnen nachzurechnen. Behoben wird deshalb hier und |
| 4769 | * nicht in delay.ts. |
| 4770 | */ |
| 4771 | 'Die Spalte tW,R ist nicht aus NGE und C gerundet, sondern als Differenz tW - tW,G auf ' + |
| 4772 | 'den ausgegebenen Ziffern gebildet, damit tW,G + tW,R die gedruckte Gesamtwartezeit ' + |
| 4773 | 'ergibt; gegenüber NGE · 3600 / C kann sie deshalb um bis zu 0,1 s abweichen.' |
| 4774 | : 'Formeln nach HCM: d = d1 + d2; d1 = 0,5 · tU · (1 - g/tU)² / (1 - min(1; x) · g/tU); ' + |
| 4775 | 'd2 = 900 · T · [(x - 1) + Wurzel aus ((x - 1)² + 8 · k · I · x / (C · T))] mit ' + |
| 4776 | 'T = 0,25 h, k = 0,5 (Festzeitsteuerung), I = 1,0 (isolierter Knotenpunkt). Angaben mit ' + |
| 4777 | 'Zwischenrundung auf die gedruckten Stellen; die Kapazität C ist ganzzahlig, x = q/C ' + |
| 4778 | 'daraus gebildet.', |
| 4779 | ); |
| 4780 | |
| 4781 | drawRueckstau(context); |
| 4782 | } |
| 4783 | |
| 4784 | /** |
| 4785 | * Der Rueckstau als Laenge, gehalten gegen den erfassten Stauraum. |
| 4786 | * |
| 4787 | * WARUM ER HIER STEHT UND NICHT ALS SPALTE: Die Tabelle darueber fuehrt im |
| 4788 | * HBS-Verfahren vierzehn Spalten. Eine fuenfzehnte haette sie unlesbar |
| 4789 | * gemacht, und die Umrechnung gehoert ohnehin nicht in die Formelkette - sie |
| 4790 | * ist kein Glied der Wartezeitrechnung, sondern eine Auskunft daneben. |
| 4791 | * |
| 4792 | * WARUM ER UEBERHAUPT GEDRUCKT WIRD: Die Spalte NGE nennt eine Fahrzeugzahl. |
| 4793 | * Ob ein Aufstellstreifen ueberlaeuft, entscheidet eine Laenge, und daran |
| 4794 | * haengt eine Abwaegung nach Paragraf 45 StVO. |
| 4795 | */ |
| 4796 | function drawRueckstau(context: Context): void { |
| 4797 | const { doc, plan } = context; |
| 4798 | const zeilen = plan.groups.filter((g) => g.rueckstau !== null); |
| 4799 | if (zeilen.length === 0) return; |
| 4800 | |
| 4801 | ensureSpace(context, 90); |
| 4802 | doc.setFont('helvetica', 'bold'); |
| 4803 | doc.setFontSize(10); |
| 4804 | doc.setTextColor('#1b2230'); |
| 4805 | doc.text(toWinAnsi('Rückstau und Stauraum'), PAGE_MARGIN, context.cursor); |
| 4806 | context.cursor += 14; |
| 4807 | |
| 4808 | autoTable(doc, { |
| 4809 | startY: context.cursor, |
| 4810 | margin: TABELLENRAND, |
| 4811 | theme: 'grid', |
| 4812 | styles: { fontSize: 9, cellPadding: 3 }, |
| 4813 | headStyles: { fillColor: '#eef1f6', textColor: '#1b2230', fontStyle: 'bold' }, |
| 4814 | head: [ |
| 4815 | ['SG', 'NGE', 'Fahrstreifen', 'Rückstau je Fahrstreifen', 'Stauraum', 'Auslastung'].map( |
| 4816 | toWinAnsi, |
| 4817 | ), |
| 4818 | ], |
| 4819 | body: zeilen.map((group) => { |
| 4820 | const r = group.rueckstau!; |
| 4821 | const fahrstreifen = |
| 4822 | context.project.signalGroups.find((g) => g.id === group.groupId)?.lanes ?? 1; |
| 4823 | return [ |
| 4824 | group.name, |
| 4825 | `${fmt.numShort(r.fahrzeuge, 2)} Kfz`, |
| 4826 | // NICHT `String(...)`: Eine Fahrstreifenzahl darf gebrochen sein, und |
| 4827 | // `String(2.5)` schreibt den englischen Punkt. Genau das war ein |
| 4828 | // Befund, behoben mit Fassung 5.10.0, und sein Waechter hat diese Zeile |
| 4829 | // beim ersten Lauf gefangen. |
| 4830 | fmt.numShort(fahrstreifen, 2), |
| 4831 | fmt.meters(r.laenge, 1), |
| 4832 | r.stauraum === null ? 'nicht erfasst' : fmt.meters(r.stauraum, 1), |
| 4833 | r.auslastung === null |
| 4834 | ? '–' |
| 4835 | : `${fmt.percent(r.auslastung)}${r.ueberlauf ? ' – erreicht' : ''}`, |
| 4836 | ].map(toWinAnsi); |
| 4837 | }), |
| 4838 | }); |
| 4839 | |
| 4840 | context.cursor = lastTableBottom(doc) + 10; |
| 4841 | |
| 4842 | writeParagraph( |
| 4843 | context, |
| 4844 | `Länge = NGE · ${fmt.meters(STAURAUMBEDARF_JE_FAHRZEUG, 0)} je wartendem Fahrzeug, geteilt ` + |
| 4845 | 'durch die Fahrstreifen der Signalgruppe. Die Länge je Fahrzeug ist ein Praxiswert; weder ' + |
| 4846 | 'die RiLSA 2015 noch das HBS 2015 nennen einen, und bei hohem Schwerverkehrsanteil ist ' + |
| 4847 | 'der wirkliche Rückstau länger. ' + |
| 4848 | 'VERGLICHEN WIRD DER MITTLERE RÜCKSTAU bei Freigabezeitende über die Betrachtungsstunde: ' + |
| 4849 | 'In etwa der Hälfte der Umläufe steht mehr, und ein Stauraum, der gerade eben reicht, ' + |
| 4850 | 'reicht dann nicht. Ein 95-Prozent-Wert wäre die richtige Größe für die Frage, ob ein ' + |
| 4851 | 'Aufstellstreifen überläuft; dieses Programm führt ihn nicht und bildet ihn auch nicht ' + |
| 4852 | 'aus einem geschätzten Faktor. Der Stauraum geht in keine Zahl der Bemessung ein.', |
| 4853 | ); |
| 4854 | } |
| 4855 | |
| 4856 | /** |
| 4857 | * Schwerverkehr einer Signalgruppe in drei Zeilen: Anteil, Aufteilung, |
| 4858 | * Anpassungsfaktor fSV mit der Gleichung, die ihn gebildet hat. |
| 4859 | * |
| 4860 | * ERGAENZT (Fassung 5.5.0, Frage E6): Frueher stand hier nur |
| 4861 | * der Anteil. Zwischen ihm und der gedruckten Saettigungsverkehrsstaerke lag |
| 4862 | * eine Umrechnung, die nirgends stand - und die gedruckte Erlaeuterung nannte |
| 4863 | * dafuer ein "Pkw-Aequivalent 2,0", das nachweislich nicht der Wert des HBS |
| 4864 | * ist. Der Faktor daneben ist das Glied, das die Spalte ohne eigene Angabe |
| 4865 | * nicht hergaebe. |
| 4866 | * |
| 4867 | * BERICHTIGT (Fassung 5.10.0): Hier stand die Zusage, mit dem Faktor daneben |
| 4868 | * lasse sich die Spalte "Saettigungsverkehrsstaerke" aus dem Regelwert je |
| 4869 | * Fahrstreifen nachrechnen (qS = 3600 / (fSV · tB)). Mit fSV allein geht das |
| 4870 | * nicht, und das Zeichen qS stand in einem Satz fuer zwei Groessen. Die Spalte |
| 4871 | * fuehrt qS0 · n · fA / fSV: qS0 ist der Grundwert je Fahrstreifen aus den |
| 4872 | * Vorgaben, n die Fahrstreifenzahl und fA die Abminderung fuer links und rechts |
| 4873 | * abbiegende Stroeme. Der Erlaeuterungsabsatz unter der Tabelle nennt diese |
| 4874 | * Kette vollstaendig (bewertung.ts, schwerverkehrSatz); die gleichlautende |
| 4875 | * Zusage in der Tabellenausgabe ist nachgezogen (csv.ts). |
| 4876 | * |
| 4877 | * WO N UND FA STEHEN: in der Signalgruppentabelle - n in ihrer Spalte |
| 4878 | * "Fahrstr.", die Fahrbeziehung, aus der fA folgt, in ihrer Spalte |
| 4879 | * "Fahrbeziehung". Eine eigene Fahrstreifenspalte in der Leistungstabelle |
| 4880 | * bliebe ohne die Fahrbeziehung derselben Zeile ein halbes Glied und stuende |
| 4881 | * zugleich ein zweites Mal in der Unterlage. |
| 4882 | * |
| 4883 | * SOLANGE ES DIESE TABELLE GIBT: Der Anwender kann sie abwaehlen. Der |
| 4884 | * Erlaeuterungsabsatz unter der Leistungstabelle nennt deshalb beide Faelle - |
| 4885 | * mit Tabelle die Spalten, ohne sie die Abwahl und die Werte selbst |
| 4886 | * (`fahrstreifenHerkunftSatz`). |
| 4887 | * |
| 4888 | * AUFTEILUNG UND GLEICHUNG (Schema 13): Seit die Aufteilung des Schwerverkehrs |
| 4889 | * erfassbar ist, reicht der Anteil zum Nachrechnen nicht mehr aus - zwei |
| 4890 | * Stroeme mit denselben 12 % tragen verschiedene Faktoren, je nachdem, ob ihre |
| 4891 | * Lastzuege gezaehlt sind. Die Zelle nennt deshalb auch, WAS erfasst ist und |
| 4892 | * nach welcher Gleichung gerechnet wurde. Bis Schema 13 bildete diese Funktion den |
| 4893 | * Faktor mit `schwerverkehrsfaktor(anteil)`, also OHNE die Aufteilung: Der |
| 4894 | * Ausdruck druckte dann 1,180 neben einer Saettigungsverkehrsstaerke, die zu |
| 4895 | * 1,300 gehoert. |
| 4896 | * |
| 4897 | * Faktor und Worte kommen aus `schwerverkehrsangabe` (bewertung.ts) - eine |
| 4898 | * zweite Fassung der Gleichung im Ausdruck waere genau die Stelle, an der Zahl |
| 4899 | * und Nachweis auseinanderlaufen, und andere Worte als in der |
| 4900 | * Tabellenausgabe waeren die Stelle, an der beide Ausgaben auseinanderlaufen. |
| 4901 | */ |
| 4902 | function schwerverkehrZelle(project: Project, groupId: string): string { |
| 4903 | const demand = project.demands.find((d) => d.signalGroupId === groupId); |
| 4904 | if (demand === undefined || demand.volume <= 0) return '–'; |
| 4905 | const angabe = schwerverkehrsangabe(demand); |
| 4906 | // Der Anteil kommt aus derselben Angabe wie fSV daneben und nicht noch |
| 4907 | // einmal aus `demand`: Sonst stuende hier der eingetragene und dort der |
| 4908 | // gerechnete Wert (bewertung.ts, anteilProzent). |
| 4909 | return ( |
| 4910 | `${angabe.anteilProzent} %\n${angabe.datenlage}\n` + |
| 4911 | `fSV = ${fmt.numShort(angabe.fsv, 3)} (${angabe.gleichungKurz})` |
| 4912 | ); |
| 4913 | } |
| 4914 | |
| 4915 | /** |
| 4916 | * Die Geometrie einer Zufahrt in einer Zelle - erfasste Werte, angesetzter |
| 4917 | * Faktor und die beiden Faelle, in denen eine Eingabe nicht wirkt. |
| 4918 | * |
| 4919 | * "Standard (angenommen)" und nicht "Standard": Ein leeres Feld heisst nicht, |
| 4920 | * dass jemand 3,25 m gemessen hat, sondern dass niemand gemessen hat. Der |
| 4921 | * Unterschied ist derselbe wie bei der Wegherkunft 'unbestimmt' - behauptet |
| 4922 | * werden darf nur, was bekannt ist. |
| 4923 | */ |
| 4924 | function geometrieZelle(project: Project, groupId: string): string { |
| 4925 | const group = project.signalGroups.find((g) => g.id === groupId); |
| 4926 | if (group === undefined) return '–'; |
| 4927 | const faktor = geometriefaktor(fahrstreifengeometrie(group), group.movement); |
| 4928 | const teile: string[] = []; |
| 4929 | if (group.fahrstreifenbreite !== undefined) { |
| 4930 | teile.push(`b = ${fmt.meters(group.fahrstreifenbreite, 2)}`); |
| 4931 | } |
| 4932 | if (group.kurvenradius !== undefined) teile.push(`R = ${fmt.meters(group.kurvenradius, 1)}`); |
| 4933 | if (group.laengsneigung !== undefined) { |
| 4934 | teile.push(`s = ${fmt.numShort(group.laengsneigung, 1)} %`); |
| 4935 | } |
| 4936 | if (teile.length === 0) return 'Standard (angenommen)\nFaktor 1,000'; |
| 4937 | |
| 4938 | const zeilen = [teile.join('; '), `Faktor ${fmt.numShort(faktor.gesamt, 3)}`]; |
| 4939 | if (faktor.gedeckelt) zeilen.push('Gefälle hebt nicht an (gedeckelt)'); |
| 4940 | if (faktor.radiusVomAbbiegerfaktorGedeckt) zeilen.push('R ohne Wirkung (Abbiegerfaktor)'); |
| 4941 | return zeilen.join('\n'); |
| 4942 | } |
| 4943 | |
| 4944 | function drawReport(context: Context): void { |
| 4945 | const { doc, report } = context; |
| 4946 | |
| 4947 | if (report.findings.length === 0) { |
| 4948 | writeParagraph(context, 'Die Prüfung ergab keine Beanstandungen.'); |
| 4949 | return; |
| 4950 | } |
| 4951 | |
| 4952 | autoTable(doc, { |
| 4953 | startY: context.cursor, |
| 4954 | margin: TABELLENRAND, |
| 4955 | theme: 'grid', |
| 4956 | styles: { fontSize: 8.5, cellPadding: 3, valign: 'top' }, |
| 4957 | headStyles: { fillColor: '#eef1f6', textColor: '#1b2230', fontStyle: 'bold' }, |
| 4958 | columnStyles: { |
| 4959 | 0: { cellWidth: 55 }, |
| 4960 | 1: { cellWidth: 90 }, |
| 4961 | 2: { cellWidth: 150 }, |
| 4962 | 4: { cellWidth: 130 }, |
| 4963 | }, |
| 4964 | head: [['Art', 'Bereich', 'Beanstandung', 'Beschreibung', 'Fundstelle'].map(toWinAnsi)], |
| 4965 | body: report.findings.map((finding) => |
| 4966 | [ |
| 4967 | SEVERITY_LABELS[finding.severity], |
| 4968 | CATEGORY_LABELS[finding.category], |
| 4969 | finding.title, |
| 4970 | `${finding.message}\n\nZu tun: ${finding.suggestion}`, |
| 4971 | finding.reference, |
| 4972 | ].map(toWinAnsi), |
| 4973 | ), |
| 4974 | }); |
| 4975 | |
| 4976 | context.cursor = lastTableBottom(doc) + 16; |
| 4977 | } |
| 4978 | |
| 4979 | /** |
| 4980 | * Abweichungen vom Regelansatz, die KEIN Zahlenwert sind |
| 4981 | * (Fassung 5.5.0, Fragen E1 und E2). |
| 4982 | * |
| 4983 | * `resolveRilsaSettings` kennt nur Kennwerte - Zahlen mit Regelwert, Einheit |
| 4984 | * und Schranke. Die beiden neuen Wahlmoeglichkeiten sind keine: Der |
| 4985 | * Ueberfahrzeit-Ansatz ist die Wahl zwischen zwei Rechenwegen, der enge |
| 4986 | * Innenradius ein Merkmal einzelner Konfliktbeziehungen. Beide veraendern die |
| 4987 | * Zwischenzeiten, und beide sind Praxisansaetze ohne Regelwerksbeleg - eine |
| 4988 | * Unterlage, die sie nicht unter "Abweichungen" fuehrt, verschweigt genau das, |
| 4989 | * was zu begruenden ist. |
| 4990 | * |
| 4991 | * Die Zeilen stehen in derselben Tabelle wie die Kennwerte, weil ein Pruefer |
| 4992 | * EINE Liste der Abweichungen erwartet und nicht zwei nebeneinander. |
| 4993 | * |
| 4994 | * UND KEINE ABWEICHUNG IST, WAS IN KEINE ZAHL EINGEHT (seit 5.43.0): Die Zeile |
| 4995 | * zum festen Ansatz stand allein nach der Einstellung da - also auch an einer |
| 4996 | * einstreifigen Verkehrsfuehrung, wo nach RiLSA 2015, Abschnitt 5.2.2 gerechnet |
| 4997 | * wird, und dort verlangte sie samt Richtungssatz eine Begruendung fuer eine |
| 4998 | * Wahl ohne Wirkung. Denselben Grundsatz haelt der Aufrufer fuer die Kennwerte |
| 4999 | * seit Befund C20 ausdruecklich fest; er gilt fuer einen Rechenansatz genauso. |
| 5000 | * Die Zeile bleibt mit dem Wirkungsvermerk - eine Einstellung, die nicht wirkt, |
| 5001 | * gehoert genannt -, aber ohne Richtungssatz: Was nicht rechnet, verkuerzt und |
| 5002 | * verlaengert nichts. |
| 5003 | */ |
| 5004 | function rechenansatzAbweichungen( |
| 5005 | project: Project, |
| 5006 | plan: SignalPlan, |
| 5007 | ): readonly (readonly string[])[] { |
| 5008 | const zeilen: string[][] = []; |
| 5009 | const defaults = plan.defaults; |
| 5010 | |
| 5011 | if (project.settings.ueberfahrzeitAnsatz === 'fest') { |
| 5012 | const ohneWirkung = ueberfahrzeitAnsatzOhneWirkung(project, plan); |
| 5013 | zeilen.push([ |
| 5014 | 'Überfahrzeit des Kraftfahrzeugverkehrs (Rechenansatz, kein Kennwert)', |
| 5015 | UEBERFAHRZEIT_ANSATZ_LABELS.gelbzeit, |
| 5016 | ohneWirkung !== null |
| 5017 | ? `${UEBERFAHRZEIT_ANSATZ_LABELS.fest} – ${ohneWirkung.vermerk}. ` + |
| 5018 | `${ohneWirkung.begruendung} ${ohneWirkung.zutun}` |
| 5019 | : `${UEBERFAHRZEIT_ANSATZ_LABELS.fest} – angesetzt sind ` + |
| 5020 | `${fmt.seconds(defaults.crossingTime.kfzGeradeaus)} geradeaus und ` + |
| 5021 | // Richtung aus den wirksamen Vorgaben (Fassung 5.5.0, Befund B5): |
| 5022 | // "VERKUERZT" gilt nur, solange die eingetragenen festen Werte unter |
| 5023 | // der Gelbzeit liegen; einstellbar sind sie von 1 bis 10 s. |
| 5024 | `${fmt.seconds(defaults.crossingTime.kfzAbbiegend)} abbiegend. ` + |
| 5025 | ueberfahrzeitAnsatzRichtungSatz(defaults), |
| 5026 | '–', |
| 5027 | ]); |
| 5028 | } |
| 5029 | |
| 5030 | // Nur die gesetzten Merkmale zaehlen, nicht die Beziehungen insgesamt: Die |
| 5031 | // Zeile soll sagen, WIE OFT vom Regelfall abgewichen wird. |
| 5032 | // |
| 5033 | // WIRKSAM heisst: abbiegender Kfz-Strom UND keine Raeumgeschwindigkeit von |
| 5034 | // Hand (Fassung 5.5.0, Befund B2). Gezaehlt werden beide - die Zeile nennt, was erfasst |
| 5035 | // ist, und daneben, an wie vielen Beziehungen es tatsaechlich gerechnet hat. |
| 5036 | // |
| 5037 | // ABBIEGEND IST DER MASSGEBENDE STROM, nicht die Signalgruppe: |
| 5038 | // `massgebendeRaeumbeziehung` (signalPlan.ts) sagt an einer Stelle, mit |
| 5039 | // welcher Fahrbeziehung eine Beziehung raeumt. Mit `from.movement` zaehlte |
| 5040 | // die Zeile an einer Sammelsignalgruppe "geradeaus UND rechts" null wirksame |
| 5041 | // Beziehungen, waehrend derselbe Ausdruck in der Spalte "vr" derselben Zeile |
| 5042 | // die gesenkte Raeumgeschwindigkeit auswies. |
| 5043 | const mitMerkmal = project.conflicts.filter((c) => c.engerRadius === true).length; |
| 5044 | if (mitMerkmal > 0) { |
| 5045 | const wirksam = project.conflicts.filter((c) => { |
| 5046 | const from = project.signalGroups.find((g) => g.id === c.fromId); |
| 5047 | return ( |
| 5048 | from !== undefined && |
| 5049 | engerRadiusWirktAn({ |
| 5050 | mode: from.mode, |
| 5051 | movement: massgebendeRaeumbeziehung(from, c), |
| 5052 | engerRadius: c.engerRadius === true, |
| 5053 | ...(c.clearingSpeedOverride !== null |
| 5054 | ? { clearingSpeedOverride: c.clearingSpeedOverride } |
| 5055 | : {}), |
| 5056 | }) |
| 5057 | ); |
| 5058 | }).length; |
| 5059 | zeilen.push([ |
| 5060 | `Räumgeschwindigkeit Kfz abbiegend, Merkmal „enger Innenradius" an ${mitMerkmal} von ` + |
| 5061 | `${project.conflicts.length} Konfliktbeziehungen (${ENGER_RADIUS_LABEL})`, |
| 5062 | fmt.metersPerSecond(RILSA_DEFAULTS.clearingSpeed.kfzTurning, 1), |
| 5063 | `${fmt.metersPerSecond(defaults.clearingSpeed.kfzTurningEngerRadius, 1)} an den ` + |
| 5064 | (wirksam === mitMerkmal |
| 5065 | ? 'gekennzeichneten Beziehungen. ' |
| 5066 | : `${wirksam} Beziehungen, an denen es wirkt (an den übrigen ist der räumende Strom ` + |
| 5067 | 'kein abbiegender Kraftfahrzeugstrom oder es steht eine Räumgeschwindigkeit von ' + |
| 5068 | 'Hand daneben, die Vorrang hat). ') + |
| 5069 | (defaults.clearingSpeed.kfzTurningEngerRadius < defaults.clearingSpeed.kfzTurning |
| 5070 | ? 'Die Wahl VERLÄNGERT die Zwischenzeiten.' |
| 5071 | : 'Die Wahl verlängert die Zwischenzeiten nicht: Der Wert entspricht der ' + |
| 5072 | 'Räumgeschwindigkeit des Abbiegers ohne dieses Merkmal.'), |
| 5073 | 'm/s', |
| 5074 | ]); |
| 5075 | } |
| 5076 | |
| 5077 | return zeilen; |
| 5078 | } |
| 5079 | |
| 5080 | function drawSettingsDeviations(context: Context): void { |
| 5081 | const { doc, project, plan } = context; |
| 5082 | // Mit der Anlagenart des Projekts: Die Umlaufzeit-Vorgaben werden gegen |
| 5083 | // DEREN Schranken aufgeloest (Befund C10). Ohne sie gaelte 'knotenpunkt', |
| 5084 | // und der Ausdruck einer einstreifigen Fuehrung fuehrte eine Abweichung, die |
| 5085 | // der Plan selbst nicht kennt - oder liesse eine aus, die er kennt. |
| 5086 | const resolved = resolveRilsaSettings(project.settings.rilsa, undefined, project.anlagenart); |
| 5087 | // Mit dem PLAN und nicht nur mit seinen Kennwerten: Die Zeilen nennen die |
| 5088 | // Kennwerte DIESES Plans, nicht die Regelwerte - wer die feste Ueberfahrzeit |
| 5089 | // oder die Raeumgeschwindigkeit bei engem Innenradius angepasst hat, bekaeme |
| 5090 | // sonst eine Abweichung gedruckt, die andere Zahlen nennt als die Rechnung |
| 5091 | // (Befund C21) -, und sie fragen am Plan nach, ob der Ansatz ueberhaupt |
| 5092 | // gerechnet hat (seit 5.43.0). |
| 5093 | const ansaetze = rechenansatzAbweichungen(project, plan); |
| 5094 | if (resolved.deviations.length === 0 && ansaetze.length === 0) return; |
| 5095 | |
| 5096 | newSection(context, 'Abweichungen von den Regelwerten'); |
| 5097 | const verfahren = bewertungsverfahren(context); |
| 5098 | // Mit der Anlagenart wie engine.ts checkSettings - fuer den Quellenschluessel |
| 5099 | // ohne Wirkung, aber der eine Aufrufer ohne sie waere der, der beim naechsten |
| 5100 | // Feld mit Schranke je Anlagenart falsch liegt. |
| 5101 | const quelleJeFeld = new Map( |
| 5102 | settingFields(undefined, project.anlagenart).map((f) => [f.key, f.quellenSchluessel]), |
| 5103 | ); |
| 5104 | // Ein Kennwert, der in keine Zahl dieser Planung eingeht, ist keine zu |
| 5105 | // begruendende Abweichung. Das steht an der Zeile, statt eine Begruendung |
| 5106 | // fuer eine wirkungslose Zahl zu verlangen. |
| 5107 | // |
| 5108 | // Den GRUND liefert der Fachkern ueber wirkungsvermerk (settings.ts, |
| 5109 | // `wirkungslosigkeit`); vor Fassung 5.4.0 (Befund C20) stand |
| 5110 | // hier ein fest eingetragenes " (ohne Wirkung: Bewertungsverfahren HCM)", |
| 5111 | // waehrend die Bedingung bereits jeden wirkungslosen Kennwert traf. Die |
| 5112 | // Koordinierungs-Kennwerte haetten damit im Ausdruck ein Bewertungsverfahren |
| 5113 | // als Grund genannt bekommen, das mit ihnen nichts zu tun hat. |
| 5114 | writeParagraph( |
| 5115 | context, |
| 5116 | 'Die folgende Aufstellung nennt, worin dieser Plan von den Regelwerten und vom Regelansatz ' + |
| 5117 | 'dieses Programms abweicht: angepasste Kennwerte (RiLSA 2015, HBS 2015 oder Planungspraxis, ' + |
| 5118 | 'siehe Fundstellenverzeichnis) und gewählte Rechenansätze, die kein Kennwert sind. Jede ' + |
| 5119 | 'wirksame Abweichung ist fachlich zu begründen.', |
| 5120 | ); |
| 5121 | |
| 5122 | autoTable(doc, { |
| 5123 | startY: context.cursor, |
| 5124 | margin: TABELLENRAND, |
| 5125 | theme: 'grid', |
| 5126 | styles: { fontSize: 9, cellPadding: 3, valign: 'top' }, |
| 5127 | headStyles: { fillColor: '#fff4e0', textColor: '#1b2230', fontStyle: 'bold' }, |
| 5128 | head: [['Kennwert', 'Regelwert', 'Angesetzt', 'Einheit'].map(toWinAnsi)], |
| 5129 | body: [ |
| 5130 | /* |
| 5131 | * Die Rechenansaetze zuerst: Sie wirken auf jede Zwischenzeit des Plans, |
| 5132 | * waehrend ein angepasster Kennwert eine einzelne Groesse betrifft. Wer |
| 5133 | * die Aufstellung von oben liest, soll die weitreichendere Abweichung |
| 5134 | * zuerst sehen (Fassung 5.5.0, Fragen E1 und E2). |
| 5135 | */ |
| 5136 | ...ansaetze.map((zeile) => zeile.map(toWinAnsi)), |
| 5137 | ...resolved.deviations.map((d) => |
| 5138 | [ |
| 5139 | // Mit dem Ueberfahrzeit-Ansatz dieses Projekts (Fassung 5.5.0, Frage E2): Die |
| 5140 | // beiden festen Ueberfahrzeiten wirken nur beim Ansatz "fest". Ohne |
| 5141 | // ihn verlangte der Ausdruck eine Begruendung fuer einen Wert, der |
| 5142 | // in keine Zahl eingeht (Befund C20). |
| 5143 | `${d.label}${wirkungsvermerk(quelleJeFeld.get(d.key) ?? 'praxis', verfahren, { |
| 5144 | ueberfahrzeitAnsatz: project.settings.ueberfahrzeitAnsatz, |
| 5145 | // Ebenso (Fassung 5.5.0, Frage E1): Ohne eine Beziehung mit dem |
| 5146 | // Merkmal geht die Raeumgeschwindigkeit bei engem Innenradius in |
| 5147 | // keine Zahl ein. |
| 5148 | engerRadiusGesetzt: project.conflicts.some((c) => c.engerRadius === true), |
| 5149 | koordinierungErfasst: project.koordinierung !== null, |
| 5150 | })}`, |
| 5151 | fmt.numShort(d.standardValue, 3), |
| 5152 | fmt.numShort(d.appliedValue, 3), |
| 5153 | d.unit, |
| 5154 | ].map(toWinAnsi), |
| 5155 | ), |
| 5156 | ], |
| 5157 | }); |
| 5158 | |
| 5159 | context.cursor = lastTableBottom(doc) + 16; |
| 5160 | } |
| 5161 | |
| 5162 | // --- Nachweis der Kennwerte ------------------------------------------------- |
| 5163 | |
| 5164 | /** |
| 5165 | * Rechtliche Einordnung. |
| 5166 | * |
| 5167 | * Fuer die Pruefung der Unterlagen ist entscheidend, dass die StVO selbst keine |
| 5168 | * Zahlenwerte fuer Signalzeiten enthaelt: Sie regelt die Bedeutung der Signale |
| 5169 | * und die Zustaendigkeit, die Kennwerte stehen in den Regelwerken der FGSV. |
| 5170 | * |
| 5171 | * ANLAGENARTABHAENGIG seit Fassung 5.4.0 (Befund C13): Die Tabelle |
| 5172 | * nannte unabhaengig von der Anlagenart nur Par. 37, Par. 45, VwV-StVO, RiLSA |
| 5173 | * und HBS. Fuer eine Baustellenampel fehlten damit genau die Regelwerke, nach |
| 5174 | * denen sie angeordnet, ausgeschrieben und abgenommen wird (RSA 21, TL und ZTV |
| 5175 | * transportable Lichtsignalanlagen 2023) - waehrend derselbe Ausdruck im |
| 5176 | * Pruefbericht laengst RSA- und ZTV-Anforderungen zitierte. Welche Grundlagen |
| 5177 | * hinzukommen, entscheidet der Fachkern (quellen.ts, rechtsgrundlagenFuer), |
| 5178 | * damit Ausdruck und Vorgabenansicht dieselbe Liste fuehren. |
| 5179 | */ |
| 5180 | function drawLegalBasis(context: Context, mitLeistungsfaehigkeit: boolean): void { |
| 5181 | const { doc, project } = context; |
| 5182 | |
| 5183 | writeParagraph( |
| 5184 | context, |
| 5185 | 'Die nachstehenden Angaben weisen aus, worauf die verwendeten Kennwerte beruhen. Die StVO ' + |
| 5186 | 'enthält selbst keine Zahlenwerte für Signalzeiten; sie regelt die Bedeutung der Signale und ' + |
| 5187 | 'die Zuständigkeit der Straßenverkehrsbehörde.', |
| 5188 | ); |
| 5189 | |
| 5190 | autoTable(doc, { |
| 5191 | startY: context.cursor, |
| 5192 | margin: TABELLENRAND, |
| 5193 | theme: 'grid', |
| 5194 | styles: { fontSize: 8.5, cellPadding: 3, valign: 'top' }, |
| 5195 | headStyles: { fillColor: '#eef1f6', textColor: '#1b2230', fontStyle: 'bold' }, |
| 5196 | columnStyles: { 0: { cellWidth: 190 }, 2: { cellWidth: 80 } }, |
| 5197 | head: [['Grundlage', 'Inhalt', 'Prüfstand'].map(toWinAnsi)], |
| 5198 | body: rechtsgrundlagenFuer(project.anlagenart).map((grundlage) => |
| 5199 | [ |
| 5200 | grundlage.bezeichnung, |
| 5201 | grundlage.hinweis === undefined |
| 5202 | ? grundlage.inhalt |
| 5203 | : `${grundlage.inhalt}\n\n${grundlage.hinweis}`, |
| 5204 | PRUEFSTAND_LABELS[grundlage.pruefstand], |
| 5205 | ].map(toWinAnsi), |
| 5206 | ), |
| 5207 | }); |
| 5208 | |
| 5209 | context.cursor = lastTableBottom(doc) + 10; |
| 5210 | |
| 5211 | // Die Tabelle nennt das HBS 2015 als Grundlage von Kapazitaet, Wartezeit |
| 5212 | // und Qualitaetsstufen - allgemein, fuer das Programm. Ob DIESES Projekt |
| 5213 | // danach bewertet, steht erst hier: Bei HCM stammen aus dem HBS nur die |
| 5214 | // Eingangsgroessen der Kapazitaet, und wer die Grundlagen liest, ohne den |
| 5215 | // Leistungsabschnitt daneben zu haben, hielte sonst die ganze Bewertung fuer |
| 5216 | // eine nach HBS 2015. |
| 5217 | writeParagraph( |
| 5218 | context, |
| 5219 | verfahrensGrundlage(bewertungsverfahren(context), mitLeistungsfaehigkeit), |
| 5220 | ); |
| 5221 | context.cursor += 6; |
| 5222 | } |
| 5223 | |
| 5224 | /** |
| 5225 | * Ein Satz dazu, nach welchem Verfahren dieses Projekt bewertet - fuer die |
| 5226 | * Grundlagen. |
| 5227 | * |
| 5228 | * Der HBS-Zweig schickte den Leser vor der Fassung 5.11.0 unbedingt in |
| 5229 | * den Abschnitt "Leistungsfaehigkeit" - auch dann, wenn dieser Abschnitt |
| 5230 | * abgewaehlt war und die Bestandteileuebersicht ihn auf Blatt 2 derselben Datei |
| 5231 | * bereits als "abgewählt - in dieser Ausgabe nicht enthalten" auswies. Zwei |
| 5232 | * gegenlaeufige Aussagen in einer Anordnungsunterlage. |
| 5233 | * |
| 5234 | * DER SATZ SELBST BLEIBT UNBEDINGT: Er unterscheidet HBS von HCM. Nur sein |
| 5235 | * Klammerzusatz haengt daran, ob es den genannten Abschnitt in DIESER Ausgabe |
| 5236 | * gibt - wie an den uebrigen Verweisen dieser Datei. Der HCM-Zweig beschreibt |
| 5237 | * das Verfahren selbst und verweist auf nichts; er bleibt unveraendert. |
| 5238 | */ |
| 5239 | function verfahrensGrundlage( |
| 5240 | verfahren: Bewertungsverfahren, |
| 5241 | mitLeistungsfaehigkeit: boolean, |
| 5242 | ): string { |
| 5243 | const name = verfahrenLabel(verfahren); |
| 5244 | if (verfahren === 'HCM') { |
| 5245 | return ( |
| 5246 | `Dieses Projekt bewertet nach dem ${name} (Highway Capacity Manual): Kapazität mit ` + |
| 5247 | 'g = tF, Wartezeit d = d1 + d2, Level of Service. Aus dem HBS 2015 stammen hier nur ' + |
| 5248 | 'Sättigungsverkehrsstärke und Abbiegerfaktor.' |
| 5249 | ); |
| 5250 | } |
| 5251 | return mitLeistungsfaehigkeit |
| 5252 | ? `Dieses Projekt rechnet und bewertet nach dem ${name} (Verfahren siehe Abschnitt ` + |
| 5253 | 'Leistungsfähigkeit).' |
| 5254 | : `Dieses Projekt rechnet und bewertet nach dem ${name}; der Abschnitt ` + |
| 5255 | '"Leistungsfähigkeit", der das Verfahren zeigt, ist für diese Ausgabe abgewählt.'; |
| 5256 | } |
| 5257 | |
| 5258 | /** |
| 5259 | * Schranken und Pruefregeln, die an der Anlagenart haengen. |
| 5260 | * |
| 5261 | * Ohne diesen Abschnitt stuende im Ausdruck eine Umlaufzeit, deren zulaessiger |
| 5262 | * Bereich nirgends benannt ist - und die Angabe, wonach sie zu beurteilen ist, |
| 5263 | * fehlte genau dort, wo der Ausdruck sonst jede Zahl auf ein Regelwerk |
| 5264 | * zurueckfuehrt. |
| 5265 | * |
| 5266 | * KORREKTUR (Fassung 5.4.0, Befund B8): Bis dahin schrieb diese Tabelle in JEDE |
| 5267 | * Zeile dieselbe Quelle der Anlagenart - "RiLSA 2015, gesichert" auch neben die |
| 5268 | * Wartezeitschwellen 90/120 s, den vollen Namen der RSA 21 auch neben die |
| 5269 | * Umlaufzeit von 30 bis 300 s. Weder die eine noch die andere Zahl steht im |
| 5270 | * jeweils genannten Regelwerk (Fassung 5.4.0, Fragen E4 und E7). Jede Zeile |
| 5271 | * bezieht ihre Grundlage und ihren Pruefstand jetzt aus `grenzen.herkunft` - |
| 5272 | * derselben Angabe, aus der auch der Pruefbericht seine Fundstelle nimmt |
| 5273 | * (Befund B9). Das Programm hatte den Sachverhalt intern laengst richtig |
| 5274 | * kommentiert; nur die gedruckte Spalte sagte etwas anderes. |
| 5275 | * |
| 5276 | * DIE SPALTE "ANGESETZT": Sie las danach trotzdem weiter aus |
| 5277 | * ANLAGENART_GRENZEN - also die Werte der Anlagenart und nicht die dieses |
| 5278 | * Plans. Wirksam sind aber die aus den Vorgaben abgeleiteten Grenzen |
| 5279 | * (`anlagenartGrenzen` in signalPlan.ts): Der Pruefbericht misst gegen |
| 5280 | * `plan.grenzen.cycleTime`, die Umlaufzeitermittlung rechnet damit, und die |
| 5281 | * Aufstellung "Abweichungen von den Regelwerten" fuehrt dieselbe Vorgabe als |
| 5282 | * Abweichung. Bei eingetragenen 45 bis 100 s stand hier "30 s bis 120 s" mit |
| 5283 | * dem Pruefstand "gesichert" - zwei Zahlenwerke in einer Anordnungsunterlage, |
| 5284 | * und quellen.ts verweist im selben Dokument auf genau diesen Abschnitt als die |
| 5285 | * Stelle, die die WIRKSAME Schranke nennt. |
| 5286 | * |
| 5287 | * DIE TABELLE UND DER SATZ DARUNTER FUEHREN DESHALB VERSCHIEDENE WERTE, und das |
| 5288 | * ist kein Versehen: In der Spalte "Angesetzt" steht, wonach dieser Plan |
| 5289 | * beurteilt wird; im Satz bleibt die Obergrenze der RiLSA stehen, weil ihre |
| 5290 | * Begruendung ("Wer so lange vor einem Signal steht, faehrt bei Rot") am |
| 5291 | * Regelwerkswert haengt. Eine selbst gesetzte Schranke ist eine |
| 5292 | * Betriebsentscheidung und traegt diese Begruendung nicht - sie wird daneben |
| 5293 | * benannt, nicht an ihre Stelle gesetzt. |
| 5294 | * |
| 5295 | * GRUNDLAGE UND PRUEFSTAND: Sie kommen weiter aus `grenzen.herkunft`, und |
| 5296 | * `anlagenartGrenzen` reicht die Herkunft der Anlagenart unveraendert durch. |
| 5297 | * Neben einem eingeengten Wert stand damit "RiLSA 2015 - Umlaufzeit am |
| 5298 | * Knotenpunkt (30 bis 120 s)" und der Pruefstand "gesichert" - die Verbindung |
| 5299 | * aus selbst gesetzter Schwelle und "gesichert", die dieselbe Datei fuer das |
| 5300 | * Fundstellenverzeichnis schon einmal aufgeloest hat (Fassung 5.9.0). |
| 5301 | * Aufgeloest wird sie hier auf demselben Weg: Der Vermerk haengt an der |
| 5302 | * Kennwertzelle, Grundlage und Pruefstand bleiben beim Regelwert und sind so |
| 5303 | * die Gegenprobe zum angesetzten Wert; der Einleitungsabsatz sagt es fuer die |
| 5304 | * ganze Tabelle. |
| 5305 | */ |
| 5306 | function drawAnlagenartGrenzen(context: Context): void { |
| 5307 | const { doc, project } = context; |
| 5308 | const grenzen = context.plan.grenzen; |
| 5309 | const regelwerk = ANLAGENART_GRENZEN[project.anlagenart]; |
| 5310 | |
| 5311 | ensureSpace(context, 140); |
| 5312 | |
| 5313 | doc.setFont('helvetica', 'bold'); |
| 5314 | doc.setFontSize(11); |
| 5315 | doc.setTextColor('#1b2230'); |
| 5316 | doc.text(toWinAnsi('Schranken der Anlagenart'), PAGE_MARGIN, context.cursor); |
| 5317 | context.cursor += 16; |
| 5318 | |
| 5319 | writeParagraph( |
| 5320 | context, |
| 5321 | `Dieser Plan ist als "${ANLAGENART_LABELS[project.anlagenart]}" angelegt. Daran hängen die ` + |
| 5322 | 'Schranken der Umlaufzeit, der größte eintragbare Räumweg und die Wartezeiten, ab denen die ' + |
| 5323 | 'Prüfung warnt oder beanstandet. Die Angaben gelten für den gesamten Plan. Die Spalte ' + |
| 5324 | '"Grundlage" sagt je Zeile, woher der Zahlenwert stammt - nicht, welches Regelwerk die ' + |
| 5325 | 'Anlagenart im Allgemeinen behandelt: Mehrere dieser Schwellen setzt dieses Programm selbst, ' + |
| 5326 | 'und sie sind mit der Straßenverkehrsbehörde abzustimmen statt in einem Regelwerk ' + |
| 5327 | 'nachzuschlagen. Steht unter "Angesetzt" eine eigene Vorgabe aus der Vorgabenverwaltung, ist ' + |
| 5328 | 'das an der Zeile vermerkt: Grundlage und Prüfstand nennen dann weiterhin den Regelwert, von ' + |
| 5329 | 'dem die Vorgabe abweicht, und nicht die Herkunft des angesetzten Wertes.', |
| 5330 | ); |
| 5331 | |
| 5332 | /** Grundlage und Pruefstand einer Zeile - beide aus derselben Herkunft. */ |
| 5333 | const herkunft = (h: SchrankenHerkunft): readonly [string, string] => [ |
| 5334 | h.text, |
| 5335 | PRUEFSTAND_LABELS[h.pruefstand], |
| 5336 | ]; |
| 5337 | |
| 5338 | /* |
| 5339 | * Der Vermerk an der Kennwertzelle - dieselbe Stelle, an der das |
| 5340 | * Fundstellenverzeichnis "(angesetzt: X, abweichend vom Regelwert)" fuehrt. |
| 5341 | * Ohne ihn stuende die eigene Vorgabe neben der Grundlage und dem Pruefstand |
| 5342 | * des Regelwerks, als waere sie dort belegt. |
| 5343 | * |
| 5344 | * "abweichend" und nicht "enger": Der zulaessige Bereich kann nur eingeengt |
| 5345 | * werden (Math.max/Math.min in anlagenartGrenzen), der empfohlene Bereich |
| 5346 | * aber auch aufgeweitet - und er verschiebt sich schon dann, wenn allein |
| 5347 | * eine eigene Obergrenze eingetragen ist und clamp() ihn in den zulaessigen |
| 5348 | * Bereich legt. Ausgeloest ist beides von einer eigenen Vorgabe; ohne eine |
| 5349 | * solche sind angesetzter Wert und Regelwert gleich. |
| 5350 | */ |
| 5351 | const abweichungsvermerk = (angesetzt: string, regelwert: string): string => |
| 5352 | angesetzt === regelwert ? '' : ` (eigene Vorgabe, abweichend vom Regelwert ${regelwert})`; |
| 5353 | |
| 5354 | const spanne = (g: AnlagenartGrenzen, welche: 'zulaessig' | 'regelbereich'): string => |
| 5355 | welche === 'zulaessig' |
| 5356 | ? `${fmt.seconds(g.cycleTime.min)} bis ${fmt.seconds(g.cycleTime.max)}` |
| 5357 | : `${fmt.seconds(g.cycleTime.recommendedMin)} bis ${fmt.seconds(g.cycleTime.recommendedMax)}`; |
| 5358 | |
| 5359 | const zulaessig = spanne(grenzen, 'zulaessig'); |
| 5360 | const regelbereich = spanne(grenzen, 'regelbereich'); |
| 5361 | |
| 5362 | autoTable(doc, { |
| 5363 | startY: context.cursor, |
| 5364 | margin: TABELLENRAND, |
| 5365 | theme: 'grid', |
| 5366 | styles: { fontSize: 8.5, cellPadding: 3, valign: 'top' }, |
| 5367 | headStyles: { fillColor: '#eef1f6', textColor: '#1b2230', fontStyle: 'bold' }, |
| 5368 | columnStyles: { |
| 5369 | 0: { cellWidth: 190, fontStyle: 'bold' }, |
| 5370 | 2: { cellWidth: 260 }, |
| 5371 | 3: { cellWidth: 80 }, |
| 5372 | }, |
| 5373 | head: [['Schranke', 'Angesetzt', 'Grundlage', 'Prüfstand'].map(toWinAnsi)], |
| 5374 | body: [ |
| 5375 | [ |
| 5376 | `Umlaufzeit, zulässig${abweichungsvermerk(zulaessig, spanne(regelwerk, 'zulaessig'))}`, |
| 5377 | zulaessig, |
| 5378 | ...herkunft(grenzen.herkunft.umlaufzeit), |
| 5379 | ], |
| 5380 | [ |
| 5381 | 'Umlaufzeit, empfohlener Bereich' + |
| 5382 | abweichungsvermerk(regelbereich, spanne(regelwerk, 'regelbereich')), |
| 5383 | regelbereich, |
| 5384 | ...herkunft(grenzen.herkunft.regelbereich), |
| 5385 | ], |
| 5386 | // Kein Regelwerkswert, sondern die Eingabegrenze dieses Programms - und |
| 5387 | // deshalb ausdruecklich so benannt. Sie gehoert trotzdem in die |
| 5388 | // Unterlage: Sie sagt, dass ein laengerer Raeumweg gar nicht erfassbar |
| 5389 | // gewesen waere, und erklaert damit, warum der Plan so aussieht, wie er |
| 5390 | // aussieht. Diese Zeile war die einzige, die ihre Herkunft schon vor |
| 5391 | // Befund B8 richtig fuehrte; jetzt fuehren sie alle so. |
| 5392 | [ |
| 5393 | 'Größter eintragbarer Räumweg', |
| 5394 | fmt.meters(grenzen.maxClearingDistance, 0), |
| 5395 | ...herkunft(grenzen.herkunft.raeumweg), |
| 5396 | ], |
| 5397 | [ |
| 5398 | 'Wartezeit: Warnung / Beanstandung', |
| 5399 | `ab ${fmt.seconds(grenzen.warteZeitWarnung)} / ab ${fmt.seconds(grenzen.warteZeitFehler)}`, |
| 5400 | ...herkunft(grenzen.herkunft.wartezeit), |
| 5401 | ], |
| 5402 | // Nur bei einstreifiger Fuehrung: die Laengen, ab denen der Pruefbericht |
| 5403 | // auf Abstimmungsbedarf hinweist (Befund C5). BEHOERDENPRAXIS, KEIN |
| 5404 | // REGELWERKSWERT - so benannt, damit ein Pruefer die 400 m nicht in der |
| 5405 | // RSA sucht. |
| 5406 | ...(project.anlagenart === 'einstreifig' |
| 5407 | ? [ |
| 5408 | [ |
| 5409 | 'Engstellenlänge: Hinweis / Warnung', |
| 5410 | `ab ${fmt.meters(ENGSTELLE_PRAXIS.hinweisAb, 0)} / ab ${fmt.meters(ENGSTELLE_PRAXIS.warnungAb, 0)}`, |
| 5411 | 'Behördenpraxis (z. B. Hessen Mobil), kein Regelwerkswert', |
| 5412 | PRUEFSTAND_LABELS.abzugleichen, |
| 5413 | ], |
| 5414 | ] |
| 5415 | : []), |
| 5416 | ].map((row) => row.map((cell) => toWinAnsi(String(cell)))), |
| 5417 | }); |
| 5418 | |
| 5419 | context.cursor = lastTableBottom(doc) + 12; |
| 5420 | |
| 5421 | /* |
| 5422 | * Die Zahlen im Fliesstext kommen aus dem REGELWERK der Anlagenart, nicht aus |
| 5423 | * der Tabelle darueber. Ausgeschriebene Werte in einem Absatz sind genau die |
| 5424 | * Stelle, an der ein Ausdruck unbemerkt veraltet - deshalb sind sie |
| 5425 | * gerechnet und nicht getippt; welche der beiden Groessen sie nennen, ist |
| 5426 | * eine fachliche Entscheidung (siehe Kopf dieser Funktion, Befund 7). |
| 5427 | */ |
| 5428 | const obergrenze = fmt.seconds(regelwerk.cycleTime.max); |
| 5429 | const knotenpunktGrenze = fmt.seconds(ANLAGENART_GRENZEN.knotenpunkt.cycleTime.max); |
| 5430 | // Hat der Anwender enger vorgegeben, steht das daneben - sonst behauptete der |
| 5431 | // Satz eine Obergrenze, nach der dieser Plan gar nicht beurteilt wird. |
| 5432 | const eigeneObergrenze = |
| 5433 | grenzen.cycleTime.max < regelwerk.cycleTime.max |
| 5434 | ? ` Für diesen Plan ist eine engere Obergrenze von ${fmt.seconds(grenzen.cycleTime.max)} ` + |
| 5435 | 'vorgegeben; sie ist eine Betriebsentscheidung dieses Projekts und trägt die vorstehende ' + |
| 5436 | 'Begründung nicht. Beurteilt wird der Plan nach ihr.' |
| 5437 | : ''; |
| 5438 | |
| 5439 | writeParagraph( |
| 5440 | context, |
| 5441 | (grenzen.quelle === 'rilsa-umlaufzeit' |
| 5442 | ? `Die Obergrenze der Umlaufzeit von ${obergrenze} ist eine Sicherheitsgrenze und keine ` + |
| 5443 | 'Bequemlichkeitsschwelle: Wer so lange vor einem Signal steht, hält die Anlage für ' + |
| 5444 | 'gestört und fährt bei Rot. Der empfohlene Bereich liegt deutlich darunter; ihn zu ' + |
| 5445 | 'überschreiten ist in den Planunterlagen zu begründen.' |
| 5446 | : /* |
| 5447 | * KORREKTUR (Fassung 5.4.0, Hinweis D1): Die Herleitung rechnete |
| 5448 | * mit RILSA_DEFAULTS statt mit den Vorgaben DIESES Plans. Bei einer |
| 5449 | * abweichenden Raeumgeschwindigkeit druckte der Ausdruck damit eine |
| 5450 | * Zwischenzeit- und Umlaufzahl, die im selben Dokument nirgends vorkommt - |
| 5451 | * und genau diese Zahl ist hier die BEGRUENDUNG der Umlaufzeitobergrenze. |
| 5452 | */ |
| 5453 | herleitungEinstreifig(regelwerk, obergrenze, knotenpunktGrenze, context.plan.defaults)) + |
| 5454 | eigeneObergrenze, |
| 5455 | ); |
| 5456 | } |
| 5457 | |
| 5458 | /** |
| 5459 | * Herleitung der Umlaufzeitobergrenze fuer eine einstreifige Verkehrsfuehrung. |
| 5460 | * |
| 5461 | * Die Zahlen werden GERECHNET und stehen nicht im Satz. Zuvor trug der Text |
| 5462 | * feste 125 s und 290 s neben einer gerechneten Engstellenlaenge - und beide |
| 5463 | * waren falsch: Aus 1000 m folgen mit den Kennwerten dieses Programms 104 s |
| 5464 | * Zwischenzeit je Richtung. Eine Zahl, die in eine Planunterlage gedruckt wird |
| 5465 | * und sich aus keiner Rechnung des Programms ergibt, ist als Begruendung |
| 5466 | * wertlos - ein Pruefer, der sie nachrechnet, findet etwas anderes. |
| 5467 | */ |
| 5468 | function herleitungEinstreifig( |
| 5469 | grenzen: AnlagenartGrenzen, |
| 5470 | obergrenze: string, |
| 5471 | knotenpunktGrenze: string, |
| 5472 | defaults: RilsaDefaults, |
| 5473 | ): string { |
| 5474 | const laenge = grenzen.maxClearingDistance; |
| 5475 | // 50 km/h und 20 s Freigabe je Richtung: der Fall, den man an einer |
| 5476 | // innerstaedtischen Arbeitsstelle tatsaechlich anordnen wuerde. Die |
| 5477 | // Untergrenze aus engstellenFolgen unterstellt 5 s und taugt als Begruendung |
| 5478 | // nicht - sie beschriebe eine Schaltung, die niemand vorsieht. |
| 5479 | const freigabeJeRichtung = 20; |
| 5480 | // `defaults` sind die Kennwerte DIESES Plans (Hinweis D1) - nicht die |
| 5481 | // Regelwerte: Die beiden Zahlen unten muessen sich mit dem Rest des |
| 5482 | // Ausdrucks nachrechnen lassen. |
| 5483 | const { zwischenzeit } = engstellenFolgen(laenge, 50, defaults); |
| 5484 | const umlauf = umlaufBeiFreigabe(laenge, 50, freigabeJeRichtung, defaults); |
| 5485 | |
| 5486 | return ( |
| 5487 | 'Die Umlaufzeitgrenzen dieses Plans stammen NICHT aus der RiLSA. Die RiLSA regelt den ' + |
| 5488 | `Knotenpunkt; dort ist die Obergrenze von ${knotenpunktGrenze} eine Sicherheitsgrenze, ` + |
| 5489 | 'weil längere Umläufe zu Rotlichtverstößen führen. An einer Arbeitsstelle gilt sie ' + |
| 5490 | 'nicht: Der Räumweg ist dort die gesamte Engstelle, die Umlaufzeit folgt zwangsläufig ' + |
| 5491 | 'aus ihrer Länge, und maßgebend ist stattdessen die zumutbare Wartezeit. Die hier ' + |
| 5492 | `angesetzte Obergrenze von ${obergrenze} ist aus den übrigen Schranken hergeleitet und ` + |
| 5493 | 'keine Zahl aus einem Regelwerk - bei der größten eintragbaren Engstelle von ' + |
| 5494 | `${fmt.meters(laenge, 0)} ergeben sich ${fmt.seconds(zwischenzeit)} Zwischenzeit je ` + |
| 5495 | `Richtung und mit zwei Übergängen sowie ${fmt.seconds(freigabeJeRichtung)} Freigabe je ` + |
| 5496 | `Richtung ${fmt.seconds(umlauf)} Umlaufzeit. Wer länger absperrt, braucht ein anderes ` + |
| 5497 | `Verfahren; die RSA 21 sieht dann vor: ${RSA_ALTERNATIVEN_ZUR_SIGNALREGELUNG}. Die Werte ` + |
| 5498 | 'sind vor dem Einsatz gegen die eigene Ausgabe der RSA abzugleichen und in den ' + |
| 5499 | 'Planunterlagen zu begründen. Unabhängig davon stellen Straßenbaubehörden Baustellenampeln ' + |
| 5500 | `üblicherweise nur bis ${fmt.meters(ENGSTELLE_PRAXIS.hinweisAb, 0)} auf, ausnahmsweise und mit ` + |
| 5501 | `Zustimmung bis ${fmt.meters(ENGSTELLE_PRAXIS.warnungAb, 0)} – Behördenpraxis (z. B. Hessen ` + |
| 5502 | 'Mobil), kein Regelwerkswert; der Prüfbericht weist ab diesen Längen auf den Abstimmungsbedarf hin.' |
| 5503 | ); |
| 5504 | } |
| 5505 | |
| 5506 | /** Fundstellenverzeichnis: jeder Kennwert mit Regelwerk, Sachgebiet und Pruefstand. */ |
| 5507 | function drawSourceRegister(context: Context, options: PdfOptions): void { |
| 5508 | const { doc, plan, project } = context; |
| 5509 | ensureSpace(context, 120); |
| 5510 | |
| 5511 | doc.setFont('helvetica', 'bold'); |
| 5512 | doc.setFontSize(11); |
| 5513 | doc.setTextColor('#1b2230'); |
| 5514 | doc.text(toWinAnsi('Fundstellenverzeichnis'), PAGE_MARGIN, context.cursor); |
| 5515 | context.cursor += 16; |
| 5516 | |
| 5517 | writeParagraph(context, FUNDSTELLEN_HINWEIS); |
| 5518 | |
| 5519 | /* |
| 5520 | * Die Fundstelle der EIGENEN Ausgabe geht der hinterlegten vor (5.27.0). |
| 5521 | * Bis dahin gab es nur die eigene, weil das Programm keine Nummer kannte - |
| 5522 | * jetzt kennt es die der RiLSA 2015, an der gekauften Ausgabe |
| 5523 | * nachgeschlagen. Wer eine andere Ausgabe hat, hat eine andere Gliederung |
| 5524 | * und traegt sie ein; deshalb die Reihenfolge und nicht umgekehrt. |
| 5525 | */ |
| 5526 | const fundstelle = (schluessel: QuellenSchluessel): string => { |
| 5527 | const eigene = options.fundstellen[schluessel] ?? ''; |
| 5528 | if (eigene !== '') return eigene; |
| 5529 | return QUELLEN[schluessel].fundstelle ?? 'nicht eingetragen'; |
| 5530 | }; |
| 5531 | |
| 5532 | const verfahren = bewertungsverfahren(context); |
| 5533 | const rows: RowInput[] = []; |
| 5534 | const verbunden = (text: string): CellDef => ({ content: toWinAnsi(text), colSpan: 2 }); |
| 5535 | /* |
| 5536 | * Die Zeilen entstanden vor der Fassung 5.9.0 aus |
| 5537 | * `settingFieldsByQuelle(plan.defaults, ...)` - das sind die AUFGELOESTEN |
| 5538 | * Kennwerte einschliesslich aller Vorgaben des Anwenders. In der Spalte |
| 5539 | * "Regelwert" stand damit bei gesetzter Vorgabe der selbst gesetzte Wert, |
| 5540 | * daneben das Regelwerk und der Pruefstand "gesichert". Derselbe Ausdruck |
| 5541 | * widersprach sich zweifach: Der Abschnitt "Abweichungen von den |
| 5542 | * Regelwerten" nennt fuer denselben Kennwert den echten Regelwert aus |
| 5543 | * RILSA_DEFAULTS, und der VwV-StVO-Eintrag DIESES Verzeichnisses haengt bei |
| 5544 | * abweichender Vorgabe ausdruecklich "abweichend vom Regelwert ..." an. Der |
| 5545 | * mitgedruckte Quellenhinweis "Die hier genannten Regelwerte sind die Werte |
| 5546 | * dieser Zeile" wurde von der Zelle daneben widerlegt. |
| 5547 | * |
| 5548 | * Die Spalte fuehrt deshalb den Regelwert. Verschwiegen wird der angesetzte |
| 5549 | * Wert nicht: Er steht mit dem Vermerk der Abweichung an der Kennwertzelle, |
| 5550 | * dort, wo auch der Wirkungsvermerk steht. |
| 5551 | * |
| 5552 | * NACHGEZOGEN (Fassung 5.12.0): WELCHER Wert der Regelwert ist und WANN eine |
| 5553 | * Vorgabe davon abweicht, entschied diese Stelle bis dahin selbst - aus |
| 5554 | * `settingFields` und einem Wertvergleich daneben. Bei den beiden |
| 5555 | * Regelbereichsfeldern der Umlaufzeit misst die Abweichungsliste aber gegen |
| 5556 | * den Wert, der ohne Eintrag fuer die ANLAGENART gilt, und an einer |
| 5557 | * Fussgaengerschutzanlage nannten die beiden Aufstellungen desselben |
| 5558 | * Ausdrucks damit verschiedene Regelwerte (40 gegen 60 s). Beides kommt jetzt |
| 5559 | * aus `fundstellenkennwerte` und damit aus derselben Quelle wie die |
| 5560 | * Aufstellung "Abweichungen von den Regelwerten": aus `resolveRilsaSettings`. |
| 5561 | * Der Rueckfall daneben nimmt den `regelwert` der Vorgabenzeile - seit der |
| 5562 | * Fassung 5.12.0 dieselbe Zahl, gebildet an derselben Stelle (settings.ts, |
| 5563 | * `regelwertVon`); mit `standard` stand dort wieder der Knotenpunktwert. |
| 5564 | */ |
| 5565 | const kennwerte = fundstellenkennwerte(project.settings.rilsa, plan.anlagenart); |
| 5566 | for (const gruppe of settingFieldsByQuelle(RILSA_DEFAULTS, plan.anlagenart)) { |
| 5567 | for (const [index, feld] of gruppe.felder.entries()) { |
| 5568 | const kennwert = kennwerte.get(feld.key); |
| 5569 | const regelwert = kennwert?.regelwert ?? feld.regelwert; |
| 5570 | const angesetzt = kennwert?.angesetzt ?? null; |
| 5571 | const abweichung = |
| 5572 | angesetzt === null |
| 5573 | ? '' |
| 5574 | : ` (angesetzt: ${fmt.numShort(angesetzt, 3)} ${feld.unit}, abweichend vom Regelwert)`; |
| 5575 | rows.push( |
| 5576 | [ |
| 5577 | index === 0 ? gruppe.quelle.kurz : '', |
| 5578 | // Mit dem Wirkungskontext dieses Projekts: Beim Regelansatz |
| 5579 | // "Ueberfahrzeit = Gelbzeit" gehen die beiden festen Ueberfahrzeiten |
| 5580 | // in keine Zahl ein, und ohne eine Beziehung mit dem Merkmal "enger |
| 5581 | // Innenradius" auch die zugehoerige Raeumgeschwindigkeit nicht. Das |
| 5582 | // Verzeichnis sagt es an der Zeile (Fassung 5.5.0, Fragen E1 und E2, Muster von |
| 5583 | // Befund C20). |
| 5584 | `${feld.label}${wirkungsvermerk(gruppe.schluessel, verfahren, { |
| 5585 | ueberfahrzeitAnsatz: project.settings.ueberfahrzeitAnsatz, |
| 5586 | engerRadiusGesetzt: project.conflicts.some((c) => c.engerRadius === true), |
| 5587 | koordinierungErfasst: project.koordinierung !== null, |
| 5588 | })}${abweichung}`, |
| 5589 | `${fmt.numShort(regelwert, 3)} ${feld.unit}`, |
| 5590 | index === 0 ? gruppe.quelle.sachgebiet : '', |
| 5591 | index === 0 ? fundstelle(gruppe.schluessel) : '', |
| 5592 | index === 0 ? PRUEFSTAND_LABELS[gruppe.quelle.pruefstand] : '', |
| 5593 | ].map(toWinAnsi), |
| 5594 | ); |
| 5595 | } |
| 5596 | /* |
| 5597 | * "Zu beachten" auch fuer Feldgruppen, nicht nur fuer feldlose Quellen: Der |
| 5598 | * Hinweis der Quelle traegt den Vorbehalt des Pruefstands - bei |
| 5599 | * 'rilsa-signalzeiten' etwa, dass die Furtregel am Wortlaut und am Bezug |
| 5600 | * auf DIN 32981 abzugleichen ist. Vor der Fassung 5.4.0 stand er nur am |
| 5601 | * Bildschirm; der Ausdruck nannte "gesichert" ohne den Vorbehalt. |
| 5602 | */ |
| 5603 | if (gruppe.quelle.hinweis !== undefined) { |
| 5604 | rows.push(['', verbunden(`Zu beachten: ${gruppe.quelle.hinweis}`), '', '', '']); |
| 5605 | } |
| 5606 | } |
| 5607 | |
| 5608 | /* |
| 5609 | * Quellen ohne Vorgabenfeld - die Stufentafeln der Qualitaetsstufen, die |
| 5610 | * VwV-StVO und (bei einstreifiger Fuehrung) die RSA 21. Sie fehlten hier |
| 5611 | * ganz, weil das Verzeichnis nur aus den Vorgabenfeldern entsteht: Die |
| 5612 | * gedruckten Tafeln des Leistungsabschnitts hatten im Nachweis weder Herkunft |
| 5613 | * noch Pruefstand, und die Grundlagen der Pruefregeln C2/C3/C6 ebenso wenig. |
| 5614 | * Kennwert und Regelwert stehen in einer verbundenen Zelle: Eine Tafel ist |
| 5615 | * kein einzelner Zahlenwert und passte nicht in die schmale Regelwertspalte. |
| 5616 | */ |
| 5617 | // Mit den Kennwerten DIESES Plans (Befund C21): Der VwV-StVO-Eintrag nennt |
| 5618 | // Rot-Gelb-Zeit und Gelbzeitstaffel, und beide sind einstellbar. |
| 5619 | for (const eintrag of quellenOhneFeld( |
| 5620 | verfahren, |
| 5621 | plan.anlagenart, |
| 5622 | plan.defaults, |
| 5623 | options.includeCycleComparison, |
| 5624 | )) { |
| 5625 | const { quelle } = eintrag; |
| 5626 | rows.push([ |
| 5627 | toWinAnsi(quelle.kurz), |
| 5628 | verbunden(`${eintrag.bezeichnung}\n\n${quelle.verwendung}`), |
| 5629 | toWinAnsi(quelle.sachgebiet), |
| 5630 | toWinAnsi(fundstelle(eintrag.schluessel)), |
| 5631 | toWinAnsi(PRUEFSTAND_LABELS[quelle.pruefstand]), |
| 5632 | ]); |
| 5633 | for (const kennwert of eintrag.kennwerte) { |
| 5634 | rows.push(['', verbunden(`${kennwert.label}: ${kennwert.wert}`), '', '', '']); |
| 5635 | } |
| 5636 | if (quelle.hinweis !== undefined) { |
| 5637 | rows.push(['', verbunden(`Zu beachten: ${quelle.hinweis}`), '', '', '']); |
| 5638 | } |
| 5639 | } |
| 5640 | |
| 5641 | autoTable(doc, { |
| 5642 | startY: context.cursor, |
| 5643 | margin: TABELLENRAND, |
| 5644 | theme: 'grid', |
| 5645 | /* |
| 5646 | * Eine Zeile dieses Verzeichnisses ist EINE Aussage - "Stufentafel |
| 5647 | * Fussgaenger und Radverkehr (maximale Wartezeit): A bis 30 s, ...". |
| 5648 | * Bricht sie mitten im Satz auf die naechste Seite um, stehen zwischen |
| 5649 | * ihren Haelften Fusszeile, Kopfzeile und Tabellenkopf, und ein Pruefer |
| 5650 | * muss die Aussage aus zwei Seiten zusammensetzen. Sie wandert deshalb |
| 5651 | * lieber ganz auf die naechste Seite. Aufgefallen, als der neue Abschnitt |
| 5652 | * "Bestandteile der Anordnungsunterlage" (Befund C14) den Satzspiegel |
| 5653 | * verschob - der Umbruch war vorher nur zufaellig guenstig. |
| 5654 | */ |
| 5655 | rowPageBreak: 'avoid', |
| 5656 | styles: { fontSize: 8, cellPadding: 2.5, valign: 'top' }, |
| 5657 | headStyles: { fillColor: '#eef1f6', textColor: '#1b2230', fontStyle: 'bold' }, |
| 5658 | /* |
| 5659 | * Die Spalte "Fundstelle" bekommt eine feste Breite (Fassung 5.5.0, Fragen E1/E2). |
| 5660 | * Sie war die einzige neben "Kennwert" ohne, und autoTable teilt den Rest |
| 5661 | * nach Inhaltsbreite auf: Mit den neuen Kennwertbezeichnungen samt |
| 5662 | * Wirkungsvermerk ("Überfahrzeit Kfz geradeaus (Ansatz „feste |
| 5663 | * Überfahrzeit") (ohne Wirkung: Überfahrzeit = Gelbzeit)") zog die |
| 5664 | * Kennwertspalte so viel Platz an sich, dass die selbst eingetragene |
| 5665 | * Fundstelle "Abschnitt 3.4, Tabelle 6" als "Abschnitt 3.4, Tabelle" / |
| 5666 | * "6" umbrach - eine Fundstelle, die ueber zwei Zeilen zerrissen ist, |
| 5667 | * laesst sich nicht mehr abschreiben. |
| 5668 | */ |
| 5669 | columnStyles: { |
| 5670 | 0: { cellWidth: 70, fontStyle: 'bold' }, |
| 5671 | 2: { cellWidth: 62, halign: 'right' }, |
| 5672 | 3: { cellWidth: 175 }, |
| 5673 | 4: { cellWidth: 110 }, |
| 5674 | 5: { cellWidth: 62 }, |
| 5675 | }, |
| 5676 | head: [ |
| 5677 | ['Regelwerk', 'Kennwert', 'Regelwert', 'Sachgebiet', 'Fundstelle', 'Prüfstand'].map( |
| 5678 | toWinAnsi, |
| 5679 | ), |
| 5680 | ], |
| 5681 | body: rows, |
| 5682 | }); |
| 5683 | |
| 5684 | context.cursor = lastTableBottom(doc) + 12; |
| 5685 | |
| 5686 | /* |
| 5687 | * Die Legende kommt aus PRUEFSTAND_ERLAEUTERUNG und nicht aus einem eigenen |
| 5688 | * Satz: Vor der Fassung 5.4.0 stand hier eine zweite Fassung, die |
| 5689 | * "abzugleichen" nur als "am Original zu pruefen" erklaerte - waehrend die |
| 5690 | * Schrankentabelle desselben Ausdrucks dasselbe Wort fuer selbst gesetzte |
| 5691 | * Schwellen verwendet, fuer die es kein Original gibt. Zwei Fassungen einer |
| 5692 | * Erklaerung laufen genau so auseinander. |
| 5693 | */ |
| 5694 | writeParagraph( |
| 5695 | context, |
| 5696 | PRUEFSTAND_REIHENFOLGE.map( |
| 5697 | (stand) => `Prüfstand "${PRUEFSTAND_LABELS[stand]}": ${PRUEFSTAND_ERLAEUTERUNG[stand]}`, |
| 5698 | ).join(' '), |
| 5699 | ); |
| 5700 | } |
| 5701 | |
| 5702 | // --- Hilfsfunktionen -------------------------------------------------------- |
| 5703 | |
| 5704 | /** Zeilenabstand eines Absatzes (9 pt in Grau). */ |
| 5705 | const ABSATZ_ZEILENHOEHE = 12; |
| 5706 | |
| 5707 | /** Abstand zum naechsten Block, den ein Absatz hinter sich laesst. */ |
| 5708 | const ABSATZ_NACHLAUF = 8; |
| 5709 | |
| 5710 | /** |
| 5711 | * Ein Absatz in 9 pt Grau - gemessen gesetzt, mit so vielen Folgeblaettern wie |
| 5712 | * noetig. |
| 5713 | * |
| 5714 | * Hier stand vor der Fassung 5.11.0 `ensureSpace` und danach |
| 5715 | * ein einzelnes `doc.text(lines, ...)`. `ensureSpace` legt genau EIN Folgeblatt |
| 5716 | * an; die Zeilen liefen danach fortlaufend nach unten - erst in den Fusssteg, |
| 5717 | * dann ueber die Fusszeile, die paintHeadersAndFooters spaeter darueber setzt, |
| 5718 | * und schliesslich aus dem Blatt. Ohne Hinweis. |
| 5719 | * |
| 5720 | * Drei Aufrufer fuehren Anwendertext ohne Laengengrenze: der Furtregelabsatz |
| 5721 | * (Rechenweg der Mindestfreigabezeit je Fussgaengergruppe, mit dem |
| 5722 | * Mittelinselvermerk am Ende), der Fahrstreifensatz der Leistungstabelle und |
| 5723 | * der Vmax-Nachsatz des Anfahransatzes. Bei den beiden letzten ist der Absatz |
| 5724 | * der EINZIGE Traeger seiner Nachweisgroesse, sobald die Tabelle |
| 5725 | * "Signalgruppen" abgewaehlt ist. |
| 5726 | * |
| 5727 | * Gesetzt wird deshalb ueber `setzeZeilenMitUmbruch` - denselben Weg, den |
| 5728 | * Deckblatttitel (Befund 43) und Bemerkung (Befund 18) schon gehen. Das |
| 5729 | * beseitigt zugleich, dass hier mit 12 pt je Zeile gerechnet, aber mit den |
| 5730 | * 10,35 pt des Feldsatzes gezeichnet wurde. |
| 5731 | * |
| 5732 | * DIE RESERVE BLEIBT: Passt der ganze Absatz noch auf dieses Blatt, beginnt er |
| 5733 | * dort; sonst beginnt er auf einem frischen. Mehr als ein volles Blatt wird |
| 5734 | * nicht verlangt - sonst legte ein ueberlanger Absatz auch dann ein Blatt an, |
| 5735 | * wenn er ohnehin oben beginnt, und liesse ein leeres zurueck. |
| 5736 | */ |
| 5737 | function writeParagraph(context: Context, text: string): void { |
| 5738 | context.doc.setFont('helvetica', 'normal'); |
| 5739 | context.doc.setFontSize(9); |
| 5740 | const lines = context.doc.splitTextToSize(toWinAnsi(text), context.contentWidth) as string[]; |
| 5741 | // Mit der tatsaechlichen Hoehe statt pauschal 40 pt: Die Absaetze der |
| 5742 | // Barrierefreiheitserklaerung sind laenger als drei Zeilen und liefen sonst |
| 5743 | // unter die Fusszeile. |
| 5744 | ensureSpace(context, Math.min(absatzHoehe(context, text), nutzhoeheJeBlatt(context))); |
| 5745 | context.doc.setTextColor('#5b6577'); |
| 5746 | setzeZeilenMitUmbruch(context, lines, ABSATZ_ZEILENHOEHE); |
| 5747 | context.cursor += ABSATZ_NACHLAUF; |
| 5748 | context.doc.setTextColor('#1b2230'); |
| 5749 | } |
| 5750 | |
| 5751 | /** |
| 5752 | * Kuerzt einen Text auf die gemessene Breite, mit Auslassungszeichen. |
| 5753 | * |
| 5754 | * Der Text muss bereits durch `toWinAnsi` gegangen sein, und Schrift und |
| 5755 | * Schriftgroesse muessen am Dokument gesetzt sein - gemessen wird mit dem, was |
| 5756 | * danach gezeichnet wird. |
| 5757 | */ |
| 5758 | function gekuerztAufBreite(doc: jsPDF, text: string, breite: number): string { |
| 5759 | if (doc.getTextWidth(text) <= breite) return text; |
| 5760 | // toWinAnsi macht aus dem Auslassungszeichen drei Punkte; Latin-1 kennt es |
| 5761 | // nicht, und ein unsichtbar verschlucktes Zeichen waere hier das Gegenteil |
| 5762 | // eines Hinweises auf die Kuerzung. |
| 5763 | const auslassung = '...'; |
| 5764 | // Halbierend statt zeichenweise: Die Kopfzeile wird fuer JEDE Seite neu |
| 5765 | // gesetzt, und ein Projektname hat keine Laengengrenze. |
| 5766 | let unten = 0; |
| 5767 | let oben = text.length; |
| 5768 | while (unten < oben) { |
| 5769 | const mitte = Math.ceil((unten + oben) / 2); |
| 5770 | if (doc.getTextWidth(`${text.slice(0, mitte)}${auslassung}`) <= breite) unten = mitte; |
| 5771 | else oben = mitte - 1; |
| 5772 | } |
| 5773 | return `${text.slice(0, unten).trimEnd()}${auslassung}`; |
| 5774 | } |
| 5775 | |
| 5776 | /** |
| 5777 | * Setzt bereits umbrochene Zeilen mit fester Schrittweite und legt dabei so |
| 5778 | * viele Folgeblaetter an, wie noetig sind. |
| 5779 | * |
| 5780 | * Schrift, Groesse und Farbe setzt der Aufrufer - gemessen und gezeichnet wird |
| 5781 | * mit dem, was am Dokument steht. Danach steht der Cursor eine Schrittweite |
| 5782 | * unter der letzten Grundlinie. |
| 5783 | * |
| 5784 | * Deckblatttitel (Befund 43) und Bemerkung (Befund 18) liefen ohne diese |
| 5785 | * Rechnung in den Fusssteg, ueber die Fusszeile und schliesslich aus dem |
| 5786 | * Blatt - ohne Hinweis. Dasselbe galt fuer jeden Absatz aus `writeParagraph`, |
| 5787 | * sobald Signalgruppennamen hineinliefen. Deshalb ein Weg fuer alle drei: |
| 5788 | * Laeuft die Rechnung auseinander, laeuft auch der gezeichnete Abstand |
| 5789 | * auseinander. |
| 5790 | */ |
| 5791 | function setzeZeilenMitUmbruch( |
| 5792 | context: Context, |
| 5793 | zeilen: readonly string[], |
| 5794 | zeilenhoehe: number, |
| 5795 | ): void { |
| 5796 | const { doc } = context; |
| 5797 | const unterkante = context.pageHeight - PAGE_MARGIN - FOOTER_HEIGHT; |
| 5798 | let offen = [...zeilen]; |
| 5799 | |
| 5800 | while (offen.length > 0) { |
| 5801 | // Sichert, dass wenigstens eine Zeile auf dieses Blatt passt; danach |
| 5802 | // ergibt die Rechnung darunter mindestens zwei. |
| 5803 | ensureSpace(context, zeilenhoehe); |
| 5804 | const platz = Math.floor((unterkante - context.cursor) / zeilenhoehe) + 1; |
| 5805 | const jetzt = offen.slice(0, Math.max(1, platz)); |
| 5806 | // Jede Zeile mit eigener Grundlinie statt als Feld: Nur so ist der |
| 5807 | // gezeichnete Abstand derselbe, mit dem hier gerechnet wird. |
| 5808 | for (const [i, zeile] of jetzt.entries()) { |
| 5809 | doc.text(zeile, PAGE_MARGIN, context.cursor + i * zeilenhoehe); |
| 5810 | } |
| 5811 | context.cursor += jetzt.length * zeilenhoehe; |
| 5812 | offen = offen.slice(jetzt.length); |
| 5813 | } |
| 5814 | } |
| 5815 | |
| 5816 | /** Zeilenabstand der Bemerkung - dieselbe Schrittweite wie in writeParagraph. */ |
| 5817 | const BEMERKUNG_ZEILENHOEHE = 12; |
| 5818 | |
| 5819 | /** |
| 5820 | * Die Bemerkung des Anwenders - gemessen gesetzt, mit so vielen Folgeblaettern |
| 5821 | * wie noetig. |
| 5822 | * |
| 5823 | * FASSUNG 5.9.0, BEFUND 18: Hier stand ein einzelnes |
| 5824 | * `doc.text(beschreibung, x, y, { maxWidth })` - eine von zwei Textstellen |
| 5825 | * dieser Datei, die ihre Hoehe nicht gemessen haben; die andere ist der |
| 5826 | * Deckblatttitel (Befund 43). `maxWidth` bricht in jsPDF nur um; ein Folgeblatt |
| 5827 | * legt es nicht an und beschnitten wird auch nichts. Die Zeilen liefen |
| 5828 | * fortlaufend nach unten: ab der 43. ueber die Fusszeile, die |
| 5829 | * paintHeadersAndFooters danach zusaetzlich darueber zeichnet, ab der 47. |
| 5830 | * ausserhalb des Blattes. Der Anwender bekam keinen Hinweis, dass seine |
| 5831 | * Bemerkung im Ausdruck fehlt - und das Feld hat keine Laengengrenze: Beim |
| 5832 | * Einlesen einer 4.x-Datei haengt die Migration "Anordnungsgrund" und |
| 5833 | * "Anordnungsnummer" an das Ende genau dieses Textes. |
| 5834 | * |
| 5835 | * WARUM NICHT writeParagraph: Das setzt 9 pt in Grau, die Bemerkung behaelt |
| 5836 | * ihre Auszeichnung (9,5 pt, #1b2230). Den Umbruch ueber mehrere Blaetter |
| 5837 | * teilen sich beide inzwischen (setzeZeilenMitUmbruch); vor der Fassung 5.11.0 |
| 5838 | * war er der zweite Grund - writeParagraph legte damals hoechstens EIN |
| 5839 | * Folgeblatt an. |
| 5840 | */ |
| 5841 | function writeBemerkung(context: Context, text: string): void { |
| 5842 | const { doc } = context; |
| 5843 | doc.setFont('helvetica', 'normal'); |
| 5844 | doc.setFontSize(9.5); |
| 5845 | doc.setTextColor('#1b2230'); |
| 5846 | |
| 5847 | setzeZeilenMitUmbruch( |
| 5848 | context, |
| 5849 | doc.splitTextToSize(toWinAnsi(text), context.contentWidth) as string[], |
| 5850 | BEMERKUNG_ZEILENHOEHE, |
| 5851 | ); |
| 5852 | } |
| 5853 | |
| 5854 | /** |
| 5855 | * Hoehe, die writeParagraph fuer diesen Text braucht - ohne ihn zu drucken. |
| 5856 | * |
| 5857 | * Damit laesst sich Platz reservieren, BEVOR eine Zeichnung gesetzt wird, die |
| 5858 | * den Rest der Seite fuellt (Befund M12). Die Rechnung ist dieselbe wie in |
| 5859 | * writeParagraph; steht sie auseinander, stimmt die Reserve nicht mehr. |
| 5860 | */ |
| 5861 | function absatzHoehe(context: Context, text: string): number { |
| 5862 | context.doc.setFont('helvetica', 'normal'); |
| 5863 | context.doc.setFontSize(9); |
| 5864 | const lines = context.doc.splitTextToSize(toWinAnsi(text), context.contentWidth) as string[]; |
| 5865 | return lines.length * ABSATZ_ZEILENHOEHE + ABSATZ_NACHLAUF; |
| 5866 | } |
| 5867 | |
| 5868 | function lastTableBottom(doc: jsPDF): number { |
| 5869 | const result = (doc as unknown as { lastAutoTable?: { finalY?: number } }).lastAutoTable; |
| 5870 | return result?.finalY ?? PAGE_MARGIN + HEADER_HEIGHT; |
| 5871 | } |
| 5872 | |
| 5873 | function methodLabel(method: Project['program']['method']): string { |
| 5874 | switch (method) { |
| 5875 | case 'webster': |
| 5876 | return 'Webster (wartezeitminimal)'; |
| 5877 | case 'akcelik': |
| 5878 | return 'Akcelik (wartezeitminimal)'; |
| 5879 | case 'hbs': |
| 5880 | return 'HBS (kapazitätsorientiert)'; |
| 5881 | case 'hcm': |
| 5882 | return 'HCM (kapazitätsorientiert)'; |
| 5883 | case 'manuell': |
| 5884 | return 'feste Vorgabe'; |
| 5885 | } |
| 5886 | } |