lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
| 1 | import type { Meters, Ratio, Seconds, VehiclesPerHour } from '../units'; |
| 2 | import { isFiniteNumber, roundTo } from '../units'; |
| 3 | import { |
| 4 | RILSA_DEFAULTS, |
| 5 | SERVICE_LEVEL_F_UEBERLASTET, |
| 6 | SERVICE_LEVEL_SCALES, |
| 7 | STAURAUMBEDARF_JE_FAHRZEUG, |
| 8 | } from './constants'; |
| 9 | import type { Bewertungsverfahren, CalculationNote, ServiceLevel, TrafficMode } from './types'; |
| 10 | |
| 11 | /** |
| 12 | * Wartezeit und Qualitaetsstufe einer Signalgruppe. |
| 13 | * |
| 14 | * KORREKTUR (Fassung 5.4.0, Befund B2): Bis dahin rechnete diese |
| 15 | * Datei fuer beide Einstellungen dieselbe HCM-Formel (d1 + d2 mit T = 0,25 h, |
| 16 | * k = 0,5, I = 1,0), und nur die Stufentafel wechselte - waehrend der Ausdruck |
| 17 | * "Bewertung nach HBS 2015" behauptete. Bei hoher Auslastung (x ab etwa 0,9) |
| 18 | * liefert die HCM-Formel deutlich kleinere Wartezeiten als das HBS-Verfahren: |
| 19 | * zu gute Stufen genau dort, wo die Bewertung kippt. |
| 20 | * |
| 21 | * Jetzt gibt es zwei Verfahren, und jedes rechnet, was es behauptet: |
| 22 | * |
| 23 | * HBS 2015 (Leyn 2016, KIT-Dissertation, Kap. 2.3.3, Gl. 2-7 bis 2-10, und |
| 24 | * Kap. 3.3.2, Gl. 3-2 - die Dissertation gibt das HBS-Verfahren woertlich |
| 25 | * wieder; die Formelkette ist am LISA+-Ausdruck "HBS-Bewertung 2015" |
| 26 | * Kornwestheim 2017 nachgerechnet, siehe tests/domain/hbsWartezeit.test.ts): |
| 27 | * |
| 28 | * tW = tW,G + tW,R (2-7) |
| 29 | * tW,G = tU * (1 - fA)^2 / (2 * (1 - min(1; x) * fA)) (2-8) |
| 30 | * mit fA = tA / tU und tA = tF + 1 s (Abflusszeit) |
| 31 | * Diese geschlossene Form gilt fuer EIN Freigabezeitfenster je |
| 32 | * Umlauf. Allgemein steht im Zaehler Summe(r_j^2) / tU ueber die |
| 33 | * Sperrzeiten r_j des Umlaufs; siehe summeSperrzeitenQuadrate. |
| 34 | * tW,R = NGE * 3600 / C (2-9) |
| 35 | * NGE = max{ 0,58*T*C/4 * [ (fin*x - 1) + sqrt((fin*x - 1)^2 + 4*fin*x / (0,58*T*C)) ] ; |
| 36 | * T*C/4 * [ (x - 1) + sqrt((x - 1)^2 + 4*x / (T*C)) ] } (2-10) |
| 37 | * mit T = 1 h (Betrachtungszeitraum) und |
| 38 | * fin = 1 + ((q15/q) - 1) / 1,5 (Instationaritaetsfaktor) (3-2) |
| 39 | * |
| 40 | * HCM (Highway Capacity Manual, Kapitel signalisierte Knotenpunkte): |
| 41 | * |
| 42 | * d = d1 + d2 |
| 43 | * d1 = 0,5 * tU * (1 - g/tU)^2 / (1 - min(1; x) * g/tU) |
| 44 | * d2 = 900 * T * [ (x - 1) + sqrt( (x - 1)^2 + 8*k*I*x / (c*T) ) ] |
| 45 | * mit T = 0,25 h, k = 0,5 (Festzeit), I = 1,0 (isoliert) |
| 46 | * |
| 47 | * Aeltere Korrekturen gegenueber dem Altbestand bleiben gueltig: Dort wurde |
| 48 | * nur d1 gerechnet, mit fest eingetragenem x = 0,85 statt dem tatsaechlichen, |
| 49 | * und die Funktion lief wegen eines nie geschriebenen Feldes faktisch nie. |
| 50 | */ |
| 51 | |
| 52 | /** Rechenweg der Wartezeit - fuer Ausdruck und Ansicht, damit sich tW nachrechnen laesst. */ |
| 53 | export interface DelayRechenweg { |
| 54 | /** Abflusszeit tA (HBS: tF + 1 s je Fenster) bzw. Freigabezeit g (HCM). */ |
| 55 | readonly abflusszeit: Seconds; |
| 56 | /** Abflusszeitanteil fA = tA/tU bzw. g/tU. */ |
| 57 | readonly abflussanteil: Ratio; |
| 58 | /** Betrachtungszeitraum T in Stunden (HBS 1 h, HCM 0,25 h). */ |
| 59 | readonly betrachtungszeitraum: number; |
| 60 | /** Instationaritaetsfaktor fin (nur HBS, sonst null). */ |
| 61 | readonly instationaritaetsfaktor: Ratio | null; |
| 62 | /** Mittlerer Rueckstau bei Freigabezeitende NGE in Kfz (nur HBS, sonst null). */ |
| 63 | readonly reststau: number | null; |
| 64 | /** HCM-Beiwerte k und I (nur HCM, sonst null). */ |
| 65 | readonly hcmK: number | null; |
| 66 | readonly hcmI: number | null; |
| 67 | } |
| 68 | |
| 69 | /** Aufteilung der mittleren Wartezeit in ihre Anteile. */ |
| 70 | export interface DelayResult { |
| 71 | /** Verfahren, nach dem gerechnet und bewertet wurde. */ |
| 72 | readonly verfahren: Bewertungsverfahren; |
| 73 | /** Mittlere Wartezeit je Fahrzeug in Sekunden (HBS: tW; HCM: d). */ |
| 74 | readonly averageDelay: Seconds; |
| 75 | /** Grundwartezeit tW,G (HBS) bzw. gleichmaessiger Anteil d1 (HCM). */ |
| 76 | readonly uniformDelay: Seconds; |
| 77 | /** Wartezeit im Reststau tW,R (HBS) bzw. Ueberlastungsanteil d2 (HCM). */ |
| 78 | readonly incrementalDelay: Seconds; |
| 79 | /** Auslastungsgrad x = q / C. */ |
| 80 | readonly degreeOfSaturation: Ratio; |
| 81 | /** |
| 82 | * Maximale Wartezeit: die laengste Sperrzeit im Umlauf. Fuer Fussgaenger |
| 83 | * und Radverkehr ist sie nach HBS 2015 das Bewertungskriterium. |
| 84 | */ |
| 85 | readonly maximumDelay: Seconds; |
| 86 | /** |
| 87 | * Qualitaetsstufe; null, wenn das Verfahren fuer diese Verkehrsart keine |
| 88 | * Tafel hat (HCM fuer Fussgaenger und Radverkehr). |
| 89 | */ |
| 90 | readonly serviceLevel: ServiceLevel | null; |
| 91 | readonly rechenweg: DelayRechenweg; |
| 92 | readonly notes: readonly CalculationNote[]; |
| 93 | } |
| 94 | |
| 95 | /** Bemessungszeitraum des HCM-Verfahrens in Stunden (Spitzenviertelstunde). */ |
| 96 | const HCM_ANALYSIS_PERIOD_HOURS = 0.25; |
| 97 | /** HCM-Beiwert fuer Festzeitsteuerung. */ |
| 98 | const HCM_PRETIMED_K = 0.5; |
| 99 | /** HCM-Beiwert fuer isolierte Knotenpunkte ohne Koordinierung. */ |
| 100 | const HCM_ISOLATED_I = 1.0; |
| 101 | /** Betrachtungszeitraum des HBS-Verfahrens in Stunden (Bemessungsstunde). */ |
| 102 | const HBS_BETRACHTUNGSZEITRAUM_HOURS = 1; |
| 103 | /** Koeffizient des ersten Terms der Rueckstauformel (HBS 2015, Gl. 2-10 bei Leyn). */ |
| 104 | const HBS_RESTSTAU_KOEFFIZIENT = 0.58; |
| 105 | |
| 106 | export interface DelayInput { |
| 107 | readonly cycleTime: Seconds; |
| 108 | /** Freigabezeit tF - Summe aller Freigabezeitfenster im Umlauf. */ |
| 109 | readonly greenTime: Seconds; |
| 110 | /** Kapazitaet C in Fz/h, wie von computeCapacity ausgewiesen (ganzzahlig). */ |
| 111 | readonly capacity: VehiclesPerHour; |
| 112 | /** Verkehrsstaerke q in Fz/h. */ |
| 113 | readonly demand: VehiclesPerHour; |
| 114 | readonly verfahren: Bewertungsverfahren; |
| 115 | /** |
| 116 | * Verkehrsart - bestimmt die Bewertungstafel. Nur Kfz und OePNV: Fuer |
| 117 | * Fussgaenger und Radverkehr gibt es keine Kapazitaet und keine mittlere |
| 118 | * Wartezeit als Kriterium; sie laufen ueber computeFussRadDelay. Der Typ |
| 119 | * schliesst aus, dass jemand eine mittlere Wartezeit an der Fuss/Rad-Tafel |
| 120 | * (maximale Wartezeit) misst. |
| 121 | */ |
| 122 | readonly mode: 'kfz' | 'oepnv'; |
| 123 | /** |
| 124 | * Abflusszeit tA (HBS) bzw. Freigabezeit g (HCM), wie sie in die |
| 125 | * Kapazitaet eingegangen ist. Fehlt sie, gilt bei HBS tF + 1 s (ein |
| 126 | * Fenster), bei HCM tF. |
| 127 | */ |
| 128 | readonly abflusszeit?: Seconds; |
| 129 | /** Laengste Sperrzeit im Umlauf; fehlt sie, gilt tU - tF. */ |
| 130 | readonly longestRed?: Seconds; |
| 131 | /** |
| 132 | * Alle Sperrzeiten des Umlaufs (Rot-Abschnitte zwischen den |
| 133 | * Freigabezeitfenstern), wie sie sperrzeitenAusFenstern liefert. Nur mit |
| 134 | * ihnen laesst sich die Grundwartezeit bei mehr als einem Fenster je Umlauf |
| 135 | * richtig bilden; fehlen sie, wird ein einziges Fenster unterstellt. |
| 136 | */ |
| 137 | readonly sperrzeiten?: readonly Seconds[]; |
| 138 | /** Instationaritaetsfaktor fin (nur HBS); fehlt er, gilt der Vorgabewert. */ |
| 139 | readonly instationaritaetsfaktor?: Ratio; |
| 140 | } |
| 141 | |
| 142 | /** |
| 143 | * Mittlere Wartezeit und Qualitaetsstufe eines Kfz- oder OePNV-Stroms. |
| 144 | * Fuer Fussgaenger und Radverkehr siehe computeFussRadDelay. |
| 145 | */ |
| 146 | export function computeDelay(input: DelayInput): DelayResult { |
| 147 | const notes: CalculationNote[] = []; |
| 148 | const { cycleTime, capacity, verfahren, mode } = input; |
| 149 | |
| 150 | if (!isFiniteNumber(cycleTime) || cycleTime <= 0) { |
| 151 | notes.push({ |
| 152 | severity: 'fehler', |
| 153 | code: 'umlaufzeit-ungueltig', |
| 154 | message: 'Die Umlaufzeit ist kein gültiger Wert; die Wartezeit ist nicht bestimmbar.', |
| 155 | }); |
| 156 | return emptyResult(input, notes); |
| 157 | } |
| 158 | |
| 159 | const green = clampGreen(input.greenTime, cycleTime, notes); |
| 160 | const tA = resolveAbflusszeit(input, green, cycleTime); |
| 161 | const fA = tA / cycleTime; |
| 162 | const maximumDelay = resolveLongestRed(input, green, cycleTime); |
| 163 | |
| 164 | if (!isFiniteNumber(capacity) || capacity <= 0) { |
| 165 | notes.push({ |
| 166 | severity: 'fehler', |
| 167 | code: 'kapazitaet-null', |
| 168 | message: |
| 169 | 'Die Kapazität beträgt 0 Fz/h. Ohne Freigabezeit entsteht ein unbegrenzter Rückstau; ' + |
| 170 | 'eine mittlere Wartezeit lässt sich nicht angeben.', |
| 171 | }); |
| 172 | return emptyResult(input, notes, maximumDelay); |
| 173 | } |
| 174 | |
| 175 | const q = isFiniteNumber(input.demand) && input.demand >= 0 ? input.demand : 0; |
| 176 | const x = q / capacity; |
| 177 | // Zaehler der Grundwartezeit; null, wenn keine brauchbaren Sperrzeiten |
| 178 | // vorliegen - dann gilt die geschlossene Einfensterform tU * (1 - fA)^2. |
| 179 | const sperrzeitenanteil = summeSperrzeitenQuadrate(input.sperrzeiten, tA, cycleTime); |
| 180 | const grundwartezeitZaehler = sperrzeitenanteil ?? cycleTime * (1 - fA) ** 2; |
| 181 | |
| 182 | let uniform: number; |
| 183 | let incremental: number; |
| 184 | let rechenweg: DelayRechenweg; |
| 185 | |
| 186 | if (verfahren === 'HBS') { |
| 187 | const fin = resolveInstationaritaet(input, notes); |
| 188 | const T = HBS_BETRACHTUNGSZEITRAUM_HOURS; |
| 189 | |
| 190 | // Gl. 2-8: Grundwartezeit. Im Zaehler steht Summe(r_j^2) / tU ueber die |
| 191 | // Sperrzeiten des Umlaufs; bei einem Fenster je Umlauf ist das die |
| 192 | // geschlossene Form tU * (1 - fA)^2. Der Nenner wird bei x -> 1 und |
| 193 | // fA -> 1 klein; die Begrenzung von x auf 1 haelt ihn positiv. |
| 194 | const denominator = 2 * (1 - Math.min(1, x) * fA); |
| 195 | uniform = denominator > 1e-9 ? grundwartezeitZaehler / denominator : 0.5 * cycleTime * (1 - fA); |
| 196 | |
| 197 | // Gl. 2-10: mittlerer Rueckstau bei Freigabezeitende, groesserer der |
| 198 | // beiden Terme (Instationaritaet bzw. reine Akcelik-Form). |
| 199 | const reststau = hbsReststau(x, capacity, T, fin); |
| 200 | // Gl. 2-9: Wartezeit im Reststau. |
| 201 | incremental = (reststau * 3600) / capacity; |
| 202 | |
| 203 | rechenweg = { |
| 204 | abflusszeit: roundTo(tA, 3), |
| 205 | abflussanteil: roundTo(fA, 4), |
| 206 | betrachtungszeitraum: T, |
| 207 | instationaritaetsfaktor: fin, |
| 208 | reststau: roundTo(reststau, 3), |
| 209 | hcmK: null, |
| 210 | hcmI: null, |
| 211 | }; |
| 212 | } else { |
| 213 | const T = HCM_ANALYSIS_PERIOD_HOURS; |
| 214 | // d1 ist dieselbe Webster-Form wie Gl. 2-8 und wird ebenso ueber die |
| 215 | // Sperrzeiten des Umlaufs gebildet. |
| 216 | const denominator = 1 - Math.min(1, x) * fA; |
| 217 | uniform = |
| 218 | denominator > 1e-9 ? (0.5 * grundwartezeitZaehler) / denominator : 0.5 * cycleTime * (1 - fA); |
| 219 | const radicand = (x - 1) ** 2 + (8 * HCM_PRETIMED_K * HCM_ISOLATED_I * x) / (capacity * T); |
| 220 | incremental = 900 * T * (x - 1 + Math.sqrt(Math.max(0, radicand))); |
| 221 | |
| 222 | rechenweg = { |
| 223 | abflusszeit: roundTo(tA, 3), |
| 224 | abflussanteil: roundTo(fA, 4), |
| 225 | betrachtungszeitraum: T, |
| 226 | instationaritaetsfaktor: null, |
| 227 | reststau: null, |
| 228 | hcmK: HCM_PRETIMED_K, |
| 229 | hcmI: HCM_ISOLATED_I, |
| 230 | }; |
| 231 | } |
| 232 | |
| 233 | const averageDelay = uniform + incremental; |
| 234 | |
| 235 | if (x > 1) { |
| 236 | notes.push({ |
| 237 | severity: 'fehler', |
| 238 | code: 'ueberlastet', |
| 239 | message: |
| 240 | `Der Auslastungsgrad beträgt ${formatRatio(x)}. Die Wartezeit wächst während des ` + |
| 241 | 'Bemessungszeitraums stetig an; der angegebene Wert gilt nur für diesen Zeitraum.', |
| 242 | }); |
| 243 | } |
| 244 | |
| 245 | /* |
| 246 | * Bewertet wird der Wert, der auch AUSGEGEBEN wird. |
| 247 | * |
| 248 | * Zuvor stand hier die ungerundete Wartezeit, waehrend darueber die |
| 249 | * gerundete zurueckgegeben wurde. Bei 35,02 s las der Pruefer dann "35,0 s" |
| 250 | * neben der Stufe C - waehrend die Stufentafel im selben Dokument bis |
| 251 | * einschliesslich 35 s die Stufe B ausweist. Die Zeile widerlegt sich damit |
| 252 | * selbst, und zwar zu Ungunsten der Anlage: Die Bewertung faellt eine Stufe |
| 253 | * zu STRENG aus. |
| 254 | * |
| 255 | * Das Fenster ist mit 0,05 s je Stufengrenze schmal. Eine Planunterlage, in |
| 256 | * der Zahl und Bewertung nicht zusammenpassen, ist an dieser Zeile aber |
| 257 | * nicht mehr zu gebrauchen - und niemand kann den Widerspruch aufloesen, |
| 258 | * weil der ungerundete Wert nirgends steht. |
| 259 | * |
| 260 | * Die Rundung auf eine Zehntelsekunde vor der Bewertung ist fachlich |
| 261 | * unbedenklich: Sie liegt weit unterhalb der Genauigkeit, mit der eine |
| 262 | * mittlere Wartezeit ueberhaupt ermittelt wird. |
| 263 | * |
| 264 | * Dasselbe gilt fuer die Stufe F bei Ueberlastung: Sie haengt an q > C, und |
| 265 | * beide Zahlen stehen ganzzahlig im Ausdruck. |
| 266 | */ |
| 267 | const ausgegebeneWartezeit = roundTo(averageDelay, 1); |
| 268 | /* |
| 269 | * Die Anteile muessen auf den gedruckten Ziffern aufgehen: tW = tW,G + tW,R |
| 270 | * (Gl. 2-7) steht so im Ausdruck nebeneinander. Werden alle drei unabhaengig |
| 271 | * gerundet, ergibt 37,85 + 21,17 = 59,02 gedruckt "37,9 + 21,2 = 59,0" - eine |
| 272 | * Zeile, die sich selbst widerlegt. Deshalb wird nur die Grundwartezeit |
| 273 | * eigenstaendig gerundet und der Reststauanteil als Differenz gebildet. |
| 274 | */ |
| 275 | const ausgegebeneGrundwartezeit = roundTo(uniform, 1); |
| 276 | const ausgegebenerReststauanteil = roundTo(ausgegebeneWartezeit - ausgegebeneGrundwartezeit, 1); |
| 277 | |
| 278 | return { |
| 279 | verfahren, |
| 280 | averageDelay: ausgegebeneWartezeit, |
| 281 | uniformDelay: ausgegebeneGrundwartezeit, |
| 282 | incrementalDelay: ausgegebenerReststauanteil, |
| 283 | degreeOfSaturation: roundTo(x, 4), |
| 284 | maximumDelay: roundTo(maximumDelay, 1), |
| 285 | serviceLevel: serviceLevelFor(ausgegebeneWartezeit, verfahren, { mode, degreeOfSaturation: x }), |
| 286 | rechenweg, |
| 287 | notes, |
| 288 | }; |
| 289 | } |
| 290 | |
| 291 | /** |
| 292 | * Zaehler der Grundwartezeit: Summe(r_j^2) / tU ueber die Sperrzeiten r_j |
| 293 | * eines Umlaufs. Null, wenn keine brauchbaren Sperrzeiten vorliegen - dann |
| 294 | * gilt die geschlossene Einfensterform. |
| 295 | * |
| 296 | * KORREKTUR (Fassung 5.10.0): Gl. 2-8 ist die Webster-Form fuer EIN |
| 297 | * Freigabezeitfenster je Umlauf; dort ist r = tU - tA, und der Zaehler wird zu |
| 298 | * (tU - tA)^2 / tU = tU * (1 - fA)^2. Erhaelt eine Signalgruppe mehrere |
| 299 | * Fenster, wiederholt sich das Signalbild oefter als einmal je Umlauf, und die |
| 300 | * Grundwartezeit richtet sich nach der Summe der QUADRIERTEN Sperrzeiten, nicht |
| 301 | * nach dem Quadrat ihrer Summe. Weil Summe(r_j^2) <= (Summe r_j)^2 gilt - mit |
| 302 | * Gleichheit nur bei einem Fenster -, fiel die Einfensterform bei jeder |
| 303 | * Aufteilung zu gross aus, bei zwei gleich langen Sperrzeiten genau um das |
| 304 | * Doppelte. computeFussRadDelay rechnet dieselbe Verallgemeinerung fuer |
| 305 | * Fussgaenger und Radverkehr bereits (Grenzfall x * fA -> 0). |
| 306 | * |
| 307 | * Die uebergebenen Sperrzeiten sind aus den Freigabezeitfenstern gebildet und |
| 308 | * enthalten den Zeitzuschlag der Abflusszeit (HBS: 1 s je Fenster) noch. Er |
| 309 | * faellt am Ende jedes Fensters an und wird deshalb von jeder Sperrzeit |
| 310 | * einzeln abgezogen; damit bleibt Summe(r_j) = tU - tA, und der Einfensterfall |
| 311 | * geht unveraendert in die geschlossene Form ueber. |
| 312 | */ |
| 313 | function summeSperrzeitenQuadrate( |
| 314 | sperrzeiten: readonly Seconds[] | undefined, |
| 315 | tA: Seconds, |
| 316 | cycleTime: Seconds, |
| 317 | ): number | null { |
| 318 | if (sperrzeiten === undefined) return null; |
| 319 | const gueltig = sperrzeiten.filter((s) => isFiniteNumber(s) && s > 0); |
| 320 | if (gueltig.length === 0) return null; |
| 321 | const summe = gueltig.reduce((wert, s) => wert + s, 0); |
| 322 | const wirksam = cycleTime - tA; |
| 323 | // Passen Sperrzeiten und Abflusszeit nicht zusammen, waere der Zuschlag |
| 324 | // negativ und wuerde die Sperrzeiten verlaengern. Dann lieber die |
| 325 | // geschlossene Form, die nur an tA haengt. |
| 326 | if (wirksam < 0 || summe < wirksam) return null; |
| 327 | const zuschlagJeFenster = (summe - wirksam) / gueltig.length; |
| 328 | let quadrate = 0; |
| 329 | for (const s of gueltig) { |
| 330 | const r = Math.max(0, s - zuschlagJeFenster); |
| 331 | quadrate += r * r; |
| 332 | } |
| 333 | return quadrate / cycleTime; |
| 334 | } |
| 335 | |
| 336 | /** |
| 337 | * Mittlerer Rueckstau bei Freigabezeitende NGE nach HBS 2015 (Gl. 2-10 bei |
| 338 | * Leyn 2016): der groessere von zwei Termen. Der erste enthaelt den |
| 339 | * Instationaritaetsfaktor und den Koeffizienten 0,58 (nach Leyn im HBS selbst |
| 340 | * nicht hergeleitet, geht auf Wu 1990 zurueck), der zweite ist die Formel von |
| 341 | * Akcelik (1980) fuer den Betrachtungszeitraum T. |
| 342 | */ |
| 343 | export function hbsReststau(x: Ratio, capacity: VehiclesPerHour, T: number, fin: Ratio): number { |
| 344 | const akcelik = (faktor: number, xEff: number): number => { |
| 345 | const a = (faktor * T * capacity) / 4; |
| 346 | const radicand = (xEff - 1) ** 2 + (4 * xEff) / (faktor * T * capacity); |
| 347 | return a * (xEff - 1 + Math.sqrt(Math.max(0, radicand))); |
| 348 | }; |
| 349 | const instationaer = akcelik(HBS_RESTSTAU_KOEFFIZIENT, fin * x); |
| 350 | const stationaer = akcelik(1, x); |
| 351 | return Math.max(0, instationaer, stationaer); |
| 352 | } |
| 353 | |
| 354 | export interface FussRadDelayInput { |
| 355 | readonly cycleTime: Seconds; |
| 356 | /** Sperrzeiten im Umlauf (Dauer jedes Rot-Abschnitts), siehe sperrzeitenAusFenstern. */ |
| 357 | readonly sperrzeiten: readonly Seconds[]; |
| 358 | readonly verfahren: Bewertungsverfahren; |
| 359 | readonly mode: 'fuss' | 'rad'; |
| 360 | } |
| 361 | |
| 362 | /** |
| 363 | * Wartezeit und Qualitaetsstufe fuer Fussgaenger und Radverkehr. |
| 364 | * |
| 365 | * KORREKTUR (Fassung 5.4.0, Befund B4): Das HBS 2015 bewertet |
| 366 | * Fussgaenger und Radverkehr an Lichtsignalanlagen nach der MAXIMALEN |
| 367 | * Wartezeit - der laengsten Sperrzeit im Umlauf - mit eigener Tafel. Eine |
| 368 | * Kapazitaet und ein Auslastungsgrad werden dafuer nicht gebildet. |
| 369 | * |
| 370 | * Als Kenngroesse wird zusaetzlich die mittlere Wartezeit eines ueber den |
| 371 | * Umlauf gleichverteilt eintreffenden Fussgaengers ausgewiesen: |
| 372 | * Summe(tS_i^2) / (2 * tU). Sie ist keine Bewertungsgroesse des HBS, aber |
| 373 | * eine nachrechenbare Zahl fuer den Vergleich von Varianten. |
| 374 | */ |
| 375 | export function computeFussRadDelay(input: FussRadDelayInput): DelayResult { |
| 376 | const notes: CalculationNote[] = []; |
| 377 | const { cycleTime, verfahren, mode } = input; |
| 378 | const rechenweg: DelayRechenweg = { |
| 379 | abflusszeit: 0, |
| 380 | abflussanteil: 0, |
| 381 | betrachtungszeitraum: 0, |
| 382 | instationaritaetsfaktor: null, |
| 383 | reststau: null, |
| 384 | hcmK: null, |
| 385 | hcmI: null, |
| 386 | }; |
| 387 | |
| 388 | if (!isFiniteNumber(cycleTime) || cycleTime <= 0) { |
| 389 | notes.push({ |
| 390 | severity: 'fehler', |
| 391 | code: 'umlaufzeit-ungueltig', |
| 392 | message: 'Die Umlaufzeit ist kein gültiger Wert; die Wartezeit ist nicht bestimmbar.', |
| 393 | }); |
| 394 | return { |
| 395 | verfahren, |
| 396 | averageDelay: Number.POSITIVE_INFINITY, |
| 397 | uniformDelay: Number.POSITIVE_INFINITY, |
| 398 | incrementalDelay: 0, |
| 399 | degreeOfSaturation: 0, |
| 400 | maximumDelay: Number.POSITIVE_INFINITY, |
| 401 | serviceLevel: serviceLevelFor(Number.POSITIVE_INFINITY, verfahren, { mode }), |
| 402 | rechenweg, |
| 403 | notes, |
| 404 | }; |
| 405 | } |
| 406 | |
| 407 | const sperrzeiten = input.sperrzeiten.filter((s) => isFiniteNumber(s) && s > 0); |
| 408 | const laengste = sperrzeiten.length > 0 ? Math.max(...sperrzeiten) : 0; |
| 409 | const mittel = sperrzeiten.reduce((sum, s) => sum + s * s, 0) / (2 * cycleTime); |
| 410 | |
| 411 | if (verfahren === 'HCM') { |
| 412 | notes.push({ |
| 413 | severity: 'hinweis', |
| 414 | code: 'keine-fuss-rad-tafel', |
| 415 | message: |
| 416 | 'Für Fußgänger und Radverkehr ist im HCM-Verfahren keine Bewertungstafel hinterlegt; ' + |
| 417 | 'eine Qualitätsstufe wird nicht vergeben. Nach HBS 2015 würde die maximale Wartezeit bewertet.', |
| 418 | }); |
| 419 | } |
| 420 | |
| 421 | const ausgegebeneMaximale = roundTo(laengste, 1); |
| 422 | return { |
| 423 | verfahren, |
| 424 | averageDelay: roundTo(mittel, 1), |
| 425 | uniformDelay: roundTo(mittel, 1), |
| 426 | incrementalDelay: 0, |
| 427 | degreeOfSaturation: 0, |
| 428 | maximumDelay: ausgegebeneMaximale, |
| 429 | serviceLevel: serviceLevelFor(ausgegebeneMaximale, verfahren, { mode }), |
| 430 | rechenweg, |
| 431 | notes, |
| 432 | }; |
| 433 | } |
| 434 | |
| 435 | /** |
| 436 | * Sperrzeiten (Rot-Abschnitte) eines Umlaufs aus den Freigabezeitfenstern. |
| 437 | * Fenster duerfen ueber den Umlaufbeginn hinweg reichen; sie werden in den |
| 438 | * Umlauf gefaltet. Ohne Fenster ist der ganze Umlauf Sperrzeit; deckt ein |
| 439 | * Fenster den ganzen Umlauf, gibt es keine. |
| 440 | */ |
| 441 | export function sperrzeitenAusFenstern( |
| 442 | fenster: readonly { readonly start: Seconds; readonly duration: Seconds }[], |
| 443 | cycleTime: Seconds, |
| 444 | ): Seconds[] { |
| 445 | if (!isFiniteNumber(cycleTime) || cycleTime <= 0) return []; |
| 446 | const gueltig = fenster.filter( |
| 447 | (f) => isFiniteNumber(f.start) && isFiniteNumber(f.duration) && f.duration > 0, |
| 448 | ); |
| 449 | if (gueltig.length === 0) return [cycleTime]; |
| 450 | if (gueltig.some((f) => f.duration >= cycleTime)) return []; |
| 451 | |
| 452 | const modulo = (v: number): number => ((v % cycleTime) + cycleTime) % cycleTime; |
| 453 | // Belegte Abschnitte im Umlauf, ueberlaufende Fenster aufgeteilt. |
| 454 | const belegt: { von: number; bis: number }[] = []; |
| 455 | for (const f of gueltig) { |
| 456 | const von = modulo(f.start); |
| 457 | const bis = von + f.duration; |
| 458 | if (bis <= cycleTime) belegt.push({ von, bis }); |
| 459 | else { |
| 460 | belegt.push({ von, bis: cycleTime }); |
| 461 | belegt.push({ von: 0, bis: bis - cycleTime }); |
| 462 | } |
| 463 | } |
| 464 | belegt.sort((a, b) => a.von - b.von); |
| 465 | // Ueberlappungen verschmelzen. |
| 466 | const verschmolzen: { von: number; bis: number }[] = []; |
| 467 | for (const b of belegt) { |
| 468 | const letzter = verschmolzen[verschmolzen.length - 1]; |
| 469 | if (letzter && b.von <= letzter.bis + 1e-9) letzter.bis = Math.max(letzter.bis, b.bis); |
| 470 | else verschmolzen.push({ ...b }); |
| 471 | } |
| 472 | const luecken: number[] = []; |
| 473 | for (let i = 0; i < verschmolzen.length; i += 1) { |
| 474 | const aktuell = verschmolzen[i]!; |
| 475 | const naechster = verschmolzen[(i + 1) % verschmolzen.length]!; |
| 476 | const luecke = |
| 477 | i + 1 < verschmolzen.length |
| 478 | ? naechster.von - aktuell.bis |
| 479 | : cycleTime - aktuell.bis + naechster.von; |
| 480 | if (luecke > 1e-9) luecken.push(roundTo(luecke, 3)); |
| 481 | } |
| 482 | return luecken; |
| 483 | } |
| 484 | |
| 485 | export interface ServiceLevelKontext { |
| 486 | /** Verkehrsart; Vorgabe 'kfz'. Bei 'fuss'/'rad' ist `wert` die maximale Wartezeit. */ |
| 487 | readonly mode?: TrafficMode; |
| 488 | /** Auslastungsgrad x = q/C; ueber 1 ergibt fuer Kfz und OePNV die Stufe F (Ueberlastung). */ |
| 489 | readonly degreeOfSaturation?: Ratio; |
| 490 | } |
| 491 | |
| 492 | /** |
| 493 | * Qualitaetsstufe zu einem Wartezeitwert. |
| 494 | * |
| 495 | * Kfz und OePNV: `wert` ist die mittlere Wartezeit. Nach HBS 2015 ist die |
| 496 | * Stufe F fuer Kfz keine Wartezeitstufe, sondern gilt bei Ueberlastung |
| 497 | * (q > C); die Tafel selbst endet mit E ohne Obergrenze. Das HCM 2010 vergibt |
| 498 | * F ebenfalls, sobald v/c > 1, unabhaengig von der Wartezeit. |
| 499 | * Fussgaenger und Radverkehr: `wert` ist die maximale Wartezeit (laengste |
| 500 | * Sperrzeit); nach HBS 2015 mit eigener Tafel, nach HCM ohne Tafel (null). |
| 501 | * |
| 502 | * Das verwendete Verfahren und das Kriterium werden mit ausgegeben, damit im |
| 503 | * Bericht nicht offen bleibt, wonach bewertet wurde. |
| 504 | */ |
| 505 | export function serviceLevelFor( |
| 506 | wert: Seconds, |
| 507 | verfahren: Bewertungsverfahren, |
| 508 | kontext: ServiceLevelKontext = {}, |
| 509 | ): ServiceLevel | null { |
| 510 | const mode = kontext.mode ?? 'kfz'; |
| 511 | const value = isFiniteNumber(wert) ? wert : Number.POSITIVE_INFINITY; |
| 512 | |
| 513 | if (mode === 'fuss' || mode === 'rad') { |
| 514 | if (verfahren !== 'HBS') return null; |
| 515 | return stufeAusTafel(SERVICE_LEVEL_SCALES.HBS.fussRad, value, verfahren, 'maximale-wartezeit'); |
| 516 | } |
| 517 | |
| 518 | const x = kontext.degreeOfSaturation; |
| 519 | /* |
| 520 | * Der Linter haelt den Vergleich mit Infinity fuer unerreichbar ("types |
| 521 | * have no overlap"), weil `isFiniteNumber` das Praedikat `wert is number` |
| 522 | * traegt und der Uebersetzer im Nein-Zweig auf `undefined` verengt. Das |
| 523 | * Praedikat ist zu grosszuegig: Infinity und NaN sind Zahlen und landen |
| 524 | * ebenfalls hier. |
| 525 | * |
| 526 | * Erreicht wird der Zweig aus dem Programm selbst - `emptyResult` weiter |
| 527 | * unten setzt bei Nachfrage ohne Kapazitaet ausdruecklich |
| 528 | * `degreeOfSaturation: Number.POSITIVE_INFINITY` und verlaesst sich fuer |
| 529 | * die Stufe F auf diese Zeile. Gemessen: Ohne den Vergleich faellt die |
| 530 | * Bewertung auf die Tafel zurueck und druckt E statt F - die gefaehrliche |
| 531 | * Richtung. Bewacht ist das in hbsWartezeit, performance und |
| 532 | * dokumentationZahlen (tests/domain). |
| 533 | */ |
| 534 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- das Praedikat verengt den Nein-Zweig zu Unrecht; siehe darueber |
| 535 | if (isFiniteNumber(x) ? x > 1 : x === Number.POSITIVE_INFINITY) { |
| 536 | return { |
| 537 | grade: SERVICE_LEVEL_F_UEBERLASTET.grade, |
| 538 | label: SERVICE_LEVEL_F_UEBERLASTET.label, |
| 539 | scale: verfahren, |
| 540 | kriterium: 'ueberlastung', |
| 541 | }; |
| 542 | } |
| 543 | |
| 544 | const tafel = |
| 545 | verfahren === 'HBS' |
| 546 | ? mode === 'oepnv' |
| 547 | ? SERVICE_LEVEL_SCALES.HBS.oepnv |
| 548 | : SERVICE_LEVEL_SCALES.HBS.kfz |
| 549 | : SERVICE_LEVEL_SCALES.HCM.kfz; |
| 550 | return stufeAusTafel(tafel, value, verfahren, 'mittlere-wartezeit'); |
| 551 | } |
| 552 | |
| 553 | /** |
| 554 | * Qualitaetsstufe eines Kfz- oder OePNV-Stroms - nie null. Fuer Aufrufer, |
| 555 | * die keine Fussgaenger- oder Radgruppen bewerten (Simulation). |
| 556 | */ |
| 557 | export function kfzServiceLevel( |
| 558 | averageDelay: Seconds, |
| 559 | verfahren: Bewertungsverfahren, |
| 560 | degreeOfSaturation?: Ratio, |
| 561 | mode: 'kfz' | 'oepnv' = 'kfz', |
| 562 | ): ServiceLevel { |
| 563 | const level = serviceLevelFor( |
| 564 | averageDelay, |
| 565 | verfahren, |
| 566 | degreeOfSaturation === undefined ? { mode } : { mode, degreeOfSaturation }, |
| 567 | ); |
| 568 | if (level === null) { |
| 569 | // Fuer kfz/oepnv liefert serviceLevelFor immer eine Stufe. Ein Rueckfall |
| 570 | // auf eine fremde Tafel waere schlimmer als ein Abbruch: Er truege das |
| 571 | // Etikett des einen Verfahrens ueber der Tafel des anderen. |
| 572 | throw new Error(`Keine Bewertungstafel fuer ${mode} nach ${verfahren}.`); |
| 573 | } |
| 574 | return level; |
| 575 | } |
| 576 | |
| 577 | function stufeAusTafel( |
| 578 | tafel: readonly { grade: ServiceLevel['grade']; maxDelay: Seconds; label: string }[], |
| 579 | value: Seconds, |
| 580 | verfahren: Bewertungsverfahren, |
| 581 | kriterium: ServiceLevel['kriterium'], |
| 582 | ): ServiceLevel { |
| 583 | for (const step of tafel) { |
| 584 | if (value <= step.maxDelay) { |
| 585 | return { grade: step.grade, label: step.label, scale: verfahren, kriterium }; |
| 586 | } |
| 587 | } |
| 588 | const last = tafel[tafel.length - 1]!; |
| 589 | return { grade: last.grade, label: last.label, scale: verfahren, kriterium }; |
| 590 | } |
| 591 | |
| 592 | function resolveAbflusszeit(input: DelayInput, green: Seconds, cycleTime: Seconds): Seconds { |
| 593 | if (isFiniteNumber(input.abflusszeit) && input.abflusszeit >= 0) { |
| 594 | return Math.min(cycleTime, input.abflusszeit); |
| 595 | } |
| 596 | if (input.verfahren === 'HBS' && green > 0) return Math.min(cycleTime, green + 1); |
| 597 | return green; |
| 598 | } |
| 599 | |
| 600 | function resolveLongestRed(input: DelayInput, green: Seconds, cycleTime: Seconds): Seconds { |
| 601 | if (isFiniteNumber(input.longestRed) && input.longestRed >= 0) { |
| 602 | return Math.min(cycleTime, input.longestRed); |
| 603 | } |
| 604 | return Math.max(0, cycleTime - green); |
| 605 | } |
| 606 | |
| 607 | function resolveInstationaritaet(input: DelayInput, notes: CalculationNote[]): Ratio { |
| 608 | const fin = input.instationaritaetsfaktor; |
| 609 | if (fin === undefined) return RILSA_DEFAULTS.capacity.instationaritaetsfaktor; |
| 610 | if (!isFiniteNumber(fin) || fin < 1) { |
| 611 | notes.push({ |
| 612 | severity: 'warnung', |
| 613 | code: 'instationaritaetsfaktor-ungueltig', |
| 614 | message: |
| 615 | 'Der Instationaritätsfaktor muss mindestens 1,0 betragen (gleichmäßiger Zufluss). ' + |
| 616 | `Angesetzt wird der Vorgabewert ${formatRatio(RILSA_DEFAULTS.capacity.instationaritaetsfaktor)}.`, |
| 617 | }); |
| 618 | return RILSA_DEFAULTS.capacity.instationaritaetsfaktor; |
| 619 | } |
| 620 | return fin; |
| 621 | } |
| 622 | |
| 623 | function clampGreen(greenTime: Seconds, cycleTime: Seconds, notes: CalculationNote[]): Seconds { |
| 624 | if (!isFiniteNumber(greenTime) || greenTime < 0) { |
| 625 | notes.push({ |
| 626 | severity: 'fehler', |
| 627 | code: 'freigabezeit-ungueltig', |
| 628 | message: 'Die Freigabezeit ist kein gültiger Wert. Angesetzt wird 0 s.', |
| 629 | }); |
| 630 | return 0; |
| 631 | } |
| 632 | if (greenTime > cycleTime) { |
| 633 | notes.push({ |
| 634 | severity: 'fehler', |
| 635 | code: 'freigabezeit-ueber-umlaufzeit', |
| 636 | message: 'Die Freigabezeit ist länger als die Umlaufzeit. Angesetzt wird die Umlaufzeit.', |
| 637 | }); |
| 638 | return cycleTime; |
| 639 | } |
| 640 | return greenTime; |
| 641 | } |
| 642 | |
| 643 | function emptyResult( |
| 644 | input: DelayInput, |
| 645 | notes: CalculationNote[], |
| 646 | maximumDelay: Seconds = Number.POSITIVE_INFINITY, |
| 647 | ): DelayResult { |
| 648 | const q = isFiniteNumber(input.demand) && input.demand > 0 ? input.demand : 0; |
| 649 | // Ohne Kapazitaet und mit Nachfrage ist der Strom ueberlastet (q > C = 0): |
| 650 | // Stufe F wegen Ueberlastung. Ohne Nachfrage ist nichts zu bewerten - dann |
| 651 | // gibt es keine Stufe; eine Stufe aus einer unendlichen Wartezeit liesse |
| 652 | // sich aus keiner gedruckten Groesse nachvollziehen. |
| 653 | const x = q > 0 ? Number.POSITIVE_INFINITY : 0; |
| 654 | return { |
| 655 | verfahren: input.verfahren, |
| 656 | averageDelay: Number.POSITIVE_INFINITY, |
| 657 | uniformDelay: Number.POSITIVE_INFINITY, |
| 658 | incrementalDelay: Number.POSITIVE_INFINITY, |
| 659 | degreeOfSaturation: x, |
| 660 | maximumDelay, |
| 661 | serviceLevel: |
| 662 | q > 0 |
| 663 | ? serviceLevelFor(Number.POSITIVE_INFINITY, input.verfahren, { |
| 664 | mode: input.mode, |
| 665 | degreeOfSaturation: x, |
| 666 | }) |
| 667 | : null, |
| 668 | rechenweg: { |
| 669 | abflusszeit: 0, |
| 670 | abflussanteil: 0, |
| 671 | betrachtungszeitraum: |
| 672 | input.verfahren === 'HBS' ? HBS_BETRACHTUNGSZEITRAUM_HOURS : HCM_ANALYSIS_PERIOD_HOURS, |
| 673 | instationaritaetsfaktor: null, |
| 674 | reststau: null, |
| 675 | hcmK: null, |
| 676 | hcmI: null, |
| 677 | }, |
| 678 | notes, |
| 679 | }; |
| 680 | } |
| 681 | |
| 682 | function formatRatio(value: number): string { |
| 683 | if (!Number.isFinite(value)) return 'unendlich'; |
| 684 | return roundTo(value, 2).toFixed(2).replace('.', ','); |
| 685 | } |
| 686 | |
| 687 | /** |
| 688 | * Rueckstaulaenge einer Signalgruppe: die wartenden Fahrzeuge, verteilt auf |
| 689 | * ihre Fahrstreifen. |
| 690 | * |
| 691 | * DIE FAHRSTREIFENZAHL GEHOERT DAZU (Fassung 5.31.0). Bis dahin rechnete die |
| 692 | * Simulation `queue * 7` und gab das als Meterangabe aus - fuer eine |
| 693 | * zweistreifige Zufahrt also die doppelte Laenge. Die |
| 694 | * Leistungsfaehigkeitsrechnung derselben Anwendung teilt an der entsprechenden |
| 695 | * Stelle (`saturationFlow` in `capacity.ts`) seit jeher durch die |
| 696 | * Fahrstreifenzahl; die beiden Rechnungen widersprachen sich. |
| 697 | * |
| 698 | * HIERHER GEZOGEN (Fassung 5.31.0): Sie stand in `simulation/simulation.ts` |
| 699 | * und wird jetzt von zwei Rechnungen gebraucht - von der Simulation fuer ihre |
| 700 | * Momentaufnahme und von der Bemessung fuer den mittleren Rueckstau NGE. Zwei |
| 701 | * Fassungen derselben Umrechnung waeren genau der Fehler, den der Altbestand |
| 702 | * mit seinen vier Zwischenzeitformeln vorgemacht hat. |
| 703 | * |
| 704 | * Der Kennwert selbst steht in `constants.ts`, seine Herkunft in `quellen.ts` |
| 705 | * unter `praxis-stauraum-je-fahrzeug`. |
| 706 | */ |
| 707 | export function rueckstaulaenge(fahrzeuge: number, fahrstreifen: number): Meters { |
| 708 | if (!isFiniteNumber(fahrzeuge) || fahrzeuge <= 0) return 0; |
| 709 | // Dieselbe Behandlung wie in `saturationFlow`: Eine fehlende oder |
| 710 | // unsinnige Angabe heisst ein Fahrstreifen, nicht null. |
| 711 | const streifen = isFiniteNumber(fahrstreifen) && fahrstreifen > 0 ? fahrstreifen : 1; |
| 712 | return roundTo((fahrzeuge * STAURAUMBEDARF_JE_FAHRZEUG) / streifen, 1); |
| 713 | } |
| 714 | |
| 715 | /** Der Rueckstau einer Signalgruppe, gemessen am vorhandenen Stauraum. */ |
| 716 | export interface Rueckstaubewertung { |
| 717 | /** Mittlerer Rueckstau bei Freigabezeitende NGE in Kfz (HBS Gl. 2-10). */ |
| 718 | readonly fahrzeuge: number; |
| 719 | /** Daraus gebildete Laenge je Fahrstreifen in Metern. */ |
| 720 | readonly laenge: Meters; |
| 721 | /** Vorhandener Stauraum in Metern - null, wenn keiner erfasst ist. */ |
| 722 | readonly stauraum: Meters | null; |
| 723 | /** Laenge geteilt durch Stauraum - null ohne Stauraum. */ |
| 724 | readonly auslastung: Ratio | null; |
| 725 | /** Erreicht oder ueberschreitet der MITTLERE Rueckstau den Stauraum? */ |
| 726 | readonly ueberlauf: boolean; |
| 727 | } |
| 728 | |
| 729 | /** |
| 730 | * Den mittleren Rueckstau gegen den vorhandenen Stauraum halten. |
| 731 | * |
| 732 | * WOZU: Die Leistungsfaehigkeitsrechnung fuehrte den Rueckstau seit jeher als |
| 733 | * Fahrzeugzahl, und die Planunterlage druckte sie. Fuer sich genommen ist sie |
| 734 | * unbrauchbar - ob ein Aufstellstreifen ueberlaeuft oder der Rueckstau in den |
| 735 | * vorgelagerten Knotenpunkt reicht, entscheidet eine LAENGE. Genau daran haengt |
| 736 | * eine Abwaegung nach Paragraf 45 StVO: Ein Rueckstau, der den durchgehenden |
| 737 | * Fahrstreifen blockiert oder ueber einen Bahnuebergang reicht, ist ein |
| 738 | * Sicherheitsmangel und gehoert benannt. |
| 739 | * |
| 740 | * WAS DIESE GROESSE IST UND WAS NICHT - und das ist der wichtigste Satz hier: |
| 741 | * NGE ist der MITTLERE Rueckstau bei Freigabezeitende ueber die |
| 742 | * Betrachtungsstunde. In etwa der Haelfte der Umlaeufe steht mehr. Ein |
| 743 | * Stauraum, der gerade eben reicht, reicht also NICHT; er reicht in der |
| 744 | * Haelfte der Faelle. Ein 95-Prozent-Wert waere die richtige Groesse fuer |
| 745 | * diese Frage, und das HBS kennt einen - dieses Programm nicht: Es fuehrt |
| 746 | * allein NGE, und einen Faktor dorthin zu erfinden hiesse, eine Zahl zu |
| 747 | * bilden, die kein Regelwerk deckt. Die Meldung sagt deshalb, WELCHE Groesse |
| 748 | * verglichen wurde, statt eine genauere vorzutaeuschen. |
| 749 | */ |
| 750 | export function rueckstaubewertung( |
| 751 | fahrzeuge: number, |
| 752 | fahrstreifen: number, |
| 753 | stauraum: Meters | null, |
| 754 | ): Rueckstaubewertung { |
| 755 | const laenge = rueckstaulaenge(fahrzeuge, fahrstreifen); |
| 756 | const brauchbar = stauraum !== null && isFiniteNumber(stauraum) && stauraum > 0; |
| 757 | return { |
| 758 | fahrzeuge: isFiniteNumber(fahrzeuge) && fahrzeuge > 0 ? roundTo(fahrzeuge, 3) : 0, |
| 759 | laenge, |
| 760 | stauraum: brauchbar ? stauraum : null, |
| 761 | auslastung: brauchbar ? roundTo(laenge / stauraum, 4) : null, |
| 762 | ueberlauf: brauchbar && laenge >= stauraum, |
| 763 | }; |
| 764 | } |