lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ src domain rilsa intergreen.ts
| 1 | import type { KilometersPerHour, Meters, MetersPerSecond, Seconds } from '../units'; |
| 2 | import { ceilSeconds, isFiniteNumber, roundTo } from '../units'; |
| 3 | import { |
| 4 | OEPNV_ANFAHRBESCHLEUNIGUNG, |
| 5 | OEPNV_UEBERFAHRZEIT_STAFFEL, |
| 6 | RILSA_DEFAULTS, |
| 7 | RILSA_ENGSTELLE, |
| 8 | SAFETY_FLOORS, |
| 9 | UEBERFAHR_RAEUMZEIT_ZUSCHLAG, |
| 10 | type RilsaDefaults, |
| 11 | } from './constants'; |
| 12 | import { yellowTime } from './signalTimes'; |
| 13 | import { |
| 14 | engstellenRaeumgeschwindigkeitMs, |
| 15 | engstellenZwischenzeit, |
| 16 | type EngstellenAnnahmen, |
| 17 | } from './engstellensignalisierung'; |
| 18 | import type { |
| 19 | CalculationNote, |
| 20 | ClearingStream, |
| 21 | EnteringStream, |
| 22 | IntergreenResult, |
| 23 | TrafficMode, |
| 24 | VehicleClass, |
| 25 | } from './types'; |
| 26 | |
| 27 | /** |
| 28 | * Zwischenzeitberechnung nach RiLSA 2015. |
| 29 | * |
| 30 | * tz = tue + tr - te |
| 31 | * |
| 32 | * tue Ueberfahrzeit Zeit vom Ende der Freigabezeit bis zum Ueberfahren der |
| 33 | * Haltlinie durch das letzte raeumende Fahrzeug |
| 34 | * tr Raeumzeit sr / vr, mit sr = Raeumweg einschliesslich Fahrzeuglaenge; |
| 35 | * beim OePNV mit Halt vor dem Knotenpunkt stattdessen der |
| 36 | * Anfahransatz nach RiLSA-Fall 4 (siehe anfahrRaeumzeit) |
| 37 | * te Einfahrzeit se / ve |
| 38 | * |
| 39 | * Das Ergebnis wird auf ganze Sekunden AUFGERUNDET und nicht negativ. |
| 40 | * |
| 41 | * Wesentliche Korrekturen gegenueber dem Altbestand: |
| 42 | * |
| 43 | * - Es gab zwei sich widersprechende Formeln. Die Fassung 4.x rechnete an |
| 44 | * einer Stelle "gelbzeit + tr - te" und an einer anderen zusaetzlich eine |
| 45 | * frei erfundene Sicherheitszeit von 1 s. Dieselbe Konfliktbeziehung |
| 46 | * lieferte damit je nach Aufrufweg unterschiedliche Zwischenzeiten. |
| 47 | * - tr und te wurden EINZELN aufgerundet und die Summe danach nochmals. |
| 48 | * Diese Doppelrundung verfaelscht das Ergebnis in beide Richtungen; bei |
| 49 | * abgezogener Einfahrzeit kann sie die Zwischenzeit sogar VERKUERZEN. |
| 50 | * Jetzt wird ausschliesslich das Endergebnis aufgerundet. |
| 51 | * - Die Raeumgeschwindigkeit fuer Kraftfahrzeuge war mit 50 km/h (13,9 m/s) |
| 52 | * deutlich zu hoch angesetzt; die Fahrbeziehung (abbiegend/geradeaus) wurde |
| 53 | * gar nicht beruecksichtigt. |
| 54 | * - Fuer Fussgaenger wurde die in m/s hinterlegte Geschwindigkeit zusaetzlich |
| 55 | * durch 3,6 geteilt, weil die Raeumzeitfunktion ohne Verkehrsart-Argument |
| 56 | * aufgerufen wurde und deshalb immer den Kfz-Zweig nahm. |
| 57 | */ |
| 58 | export function computeIntergreen( |
| 59 | clearing: ClearingStream, |
| 60 | entering: EnteringStream, |
| 61 | defaults: RilsaDefaults = RILSA_DEFAULTS, |
| 62 | ): IntergreenResult { |
| 63 | const notes: CalculationNote[] = []; |
| 64 | |
| 65 | const vehicleLength = resolveVehicleLength(clearing, defaults, notes); |
| 66 | const clearingDistance = requireNonNegative( |
| 67 | clearing.clearingDistance, |
| 68 | 'raeumweg', |
| 69 | 'Räumweg', |
| 70 | notes, |
| 71 | ); |
| 72 | const enteringDistance = requireNonNegative( |
| 73 | entering.enteringDistance, |
| 74 | 'einfahrweg', |
| 75 | 'Einfahrweg', |
| 76 | notes, |
| 77 | ); |
| 78 | |
| 79 | const clearingPath = clearingDistance + vehicleLength; |
| 80 | const clearingSpeed = resolveClearingSpeed(clearing, defaults, notes); |
| 81 | const enteringSpeed = resolveEnteringSpeed(entering, defaults, notes); |
| 82 | const crossingTime = resolveCrossingTime(clearing, defaults, notes); |
| 83 | |
| 84 | /* |
| 85 | * Raeumzeit. |
| 86 | * |
| 87 | * Regelfall: tr = sr / vr mit konstanter Raeumgeschwindigkeit. |
| 88 | * |
| 89 | * OePNV mit Halt vor dem Knotenpunkt (KORREKTUR Fassung 5.4.0, |
| 90 | * Befund A2): RiLSA 2015, Zwischenzeitfall 4 - das Fahrzeug faehrt am Ende |
| 91 | * der Freigabezeit aus dem Stand an. Woertlich (FGSV-Aenderungsblatt vom |
| 92 | * 29.07.2015, Originalseiten 24-25): "tue + tr = Wurzel aus |
| 93 | * (2 * (s0 + lFZ) / a)" fuer (s0 + lFZ) <= Vmax^2/(2 * 3,6^2 * a), sonst |
| 94 | * "tue + tr = Vmax/(3,6 * a) + (s0 + lFZ - Vmax^2/(2 * 3,6^2 * a)) / |
| 95 | * (Vmax/3,6)"; die Ueberfahrzeit betraegt dabei 0 s, sodass die gesamte |
| 96 | * Formel als Raeumzeit gefuehrt wird. |
| 97 | */ |
| 98 | const anfahrbeschleunigung = istOepnvHaltefall(clearing) |
| 99 | ? anfahrbeschleunigungFuer(clearing.vehicleClass) |
| 100 | : null; |
| 101 | const clearingTime = |
| 102 | anfahrbeschleunigung === null |
| 103 | ? clearingPath / clearingSpeed |
| 104 | : anfahrRaeumzeit(clearingPath, anfahrbeschleunigung, clearingSpeed); |
| 105 | const enteringTime = enteringDistance / enteringSpeed; |
| 106 | |
| 107 | /* |
| 108 | * Untergrenze der Ueberfahr- und Raeumzeit (RiLSA 2015, Abschnitt 2.5.2): |
| 109 | * tue + tr >= tG + 1 s, und zwar nur fuer die Faelle 1 und 2 - geradeaus |
| 110 | * fahrende und abbiegende Kraftfahrzeuge. Siehe |
| 111 | * UEBERFAHR_RAEUMZEIT_ZUSCHLAG in constants.ts, warum die Sekunde fest ist |
| 112 | * und warum die Schranke bis 5.26.0 fehlte. |
| 113 | */ |
| 114 | const untergrenze = ueberfahrRaeumzeitUntergrenze(clearing, defaults); |
| 115 | const gerechnet = crossingTime + clearingTime; |
| 116 | const greift = untergrenze !== null && gerechnet < untergrenze - RUNDUNGSRAUSCHEN; |
| 117 | const ueberfahrUndRaeumzeit = greift ? untergrenze : gerechnet; |
| 118 | |
| 119 | if (greift) { |
| 120 | notes.push({ |
| 121 | severity: 'hinweis', |
| 122 | code: 'ueberfahr-raeumzeit-untergrenze', |
| 123 | message: |
| 124 | `Überfahr- und Räumzeit tü + tr = ${formatZahl(gerechnet)} s unterschreiten die ` + |
| 125 | `Untergrenze der RiLSA 2015 (Abschnitt 2.5.2: tü + tr ≥ tG + 1 s). Angesetzt werden ` + |
| 126 | `${formatZahl(untergrenze)} s; die Zwischenzeit fällt dadurch länger aus.`, |
| 127 | }); |
| 128 | } |
| 129 | |
| 130 | const raw = ueberfahrUndRaeumzeit - enteringTime; |
| 131 | const rounded = ceilSeconds(raw); |
| 132 | const clampedToZero = rounded < 0; |
| 133 | const intergreen = clampedToZero ? 0 : rounded; |
| 134 | |
| 135 | if (clampedToZero) { |
| 136 | notes.push({ |
| 137 | severity: 'hinweis', |
| 138 | code: 'zwischenzeit-negativ', |
| 139 | message: |
| 140 | `Die rechnerische Zwischenzeit ist mit ${formatZahl(raw)} s negativ; ` + |
| 141 | 'der einfahrende Strom erreicht den Konfliktbereich später als der räumende ihn verlässt. ' + |
| 142 | 'Angesetzt wird 0 s.', |
| 143 | }); |
| 144 | } |
| 145 | |
| 146 | if (clearingDistance === 0) { |
| 147 | notes.push({ |
| 148 | severity: 'warnung', |
| 149 | code: 'raeumweg-null', |
| 150 | message: 'Der Räumweg beträgt 0 m. Prüfen Sie, ob die Konfliktfläche richtig vermaßt ist.', |
| 151 | }); |
| 152 | } |
| 153 | |
| 154 | return { |
| 155 | verfahren: 'knotenpunkt', |
| 156 | intergreen, |
| 157 | raw: roundTo(raw, 3), |
| 158 | crossingTime, |
| 159 | clearingPath: roundTo(clearingPath, 3), |
| 160 | vehicleLength, |
| 161 | clearingSpeed, |
| 162 | raeumansatz: anfahrbeschleunigung === null ? 'geschwindigkeit' : 'anfahren', |
| 163 | anfahrbeschleunigung, |
| 164 | clearingTime: roundTo(clearingTime, 3), |
| 165 | ueberfahrRaeumzeitUntergrenze: greift ? untergrenze : null, |
| 166 | enteringPath: roundTo(enteringDistance, 3), |
| 167 | enteringSpeed, |
| 168 | enteringTime: roundTo(enteringTime, 3), |
| 169 | clampedToZero, |
| 170 | notes, |
| 171 | }; |
| 172 | } |
| 173 | |
| 174 | /** |
| 175 | * Zwischenzeit einer Engstelle nach RiLSA 2015, Abschnitt 5.2.2 - im |
| 176 | * Ergebnistyp der Knotenpunktrechnung, damit Plan, Pruefbericht und Ausdruck |
| 177 | * unveraendert damit arbeiten. |
| 178 | * |
| 179 | * WAS IN DEN FELDERN STEHT, IST NICHT DASSELBE WIE AM KNOTENPUNKT, und das |
| 180 | * ist der Grund fuer das Feld `verfahren`: |
| 181 | * |
| 182 | * `crossingTime` 4 s, fest (nicht die Gelbzeit, nicht 3/2 s) |
| 183 | * `vehicleLength` 0 m - der Raeumweg der Engstelle ist der Abstand der |
| 184 | * Haltlinien OHNE Fahrzeuglaenge |
| 185 | * `clearingPath` deshalb gleich dem eingetragenen Raeumweg |
| 186 | * `clearingSpeed` Vr/3,6 - die Formel des Abschnitts rechnet in km/h, |
| 187 | * dieses Programm fuehrt SI-Einheiten |
| 188 | * `enteringTime` 0 s - "ohne Beruecksichtigung einer Einfahrzeit" |
| 189 | * |
| 190 | * Der eingetragene EINFAHRWEG geht damit in keine Zahl ein. Das wird gemeldet |
| 191 | * und nicht verschwiegen: Wer ihn erfasst hat, soll wissen, dass er hier |
| 192 | * nichts bewirkt. |
| 193 | */ |
| 194 | export function computeEngstellenIntergreen( |
| 195 | clearing: ClearingStream, |
| 196 | entering: EnteringStream, |
| 197 | defaults: RilsaDefaults = RILSA_DEFAULTS, |
| 198 | annahmen: EngstellenAnnahmen = {}, |
| 199 | ): IntergreenResult { |
| 200 | const notes: CalculationNote[] = []; |
| 201 | const ergebnis = engstellenZwischenzeit(clearing.clearingDistance, clearing.vZul, annahmen); |
| 202 | notes.push(...ergebnis.notes); |
| 203 | |
| 204 | /* |
| 205 | * LANGSAMER ALS DAS REGELWERK DARF MAN IMMER - schneller nie. |
| 206 | * |
| 207 | * Die Raeumgeschwindigkeit des Abschnitts 5.2 steht fest im Regelwerk; sie |
| 208 | * ist kein Vorgabenwert, und der Vorgabenwert der Knotenpunktrechnung |
| 209 | * (10,0 m/s) hat an einer Engstelle nichts zu suchen. Zwei Wege bleiben |
| 210 | * gleichwohl offen, und beide fuehren nur nach unten: |
| 211 | * |
| 212 | * 1. Eine ABWEICHENDE Vorgabe der Raeumgeschwindigkeit - also eine, die |
| 213 | * der Bearbeiter selbst gesetzt hat und die vom Regelwert der |
| 214 | * Knotenpunktrechnung abweicht. Sie ist eine Aussage ueber diese |
| 215 | * Planung und nicht der mitgeschleppte Standardwert; ohne diese |
| 216 | * Unterscheidung gewaennen die 10,0 m/s gegen die 11,1 m/s der Staffel |
| 217 | * und das Verfahren des Abschnitts 5.2 waere wirkungslos. |
| 218 | * 2. Die Eingabe an der EINZELNEN Beziehung. Sie bildet ab, was das |
| 219 | * Programm nicht kennt - eine Steigung, eine Baustellenzufahrt, einen |
| 220 | * schleppenden Schwerlastverkehr - und geht der Vorgabe vor. |
| 221 | * |
| 222 | * Ein HOEHERER Wert verkuerzte die Zwischenzeit gegenueber dem Regelwerk. |
| 223 | * Er wird verworfen und gemeldet. |
| 224 | */ |
| 225 | let clearingSpeed = ergebnis.raeumgeschwindigkeit.vrKmh / 3.6; |
| 226 | const abweichendeVorgabe = |
| 227 | defaults.clearingSpeed.kfzStraight !== RILSA_DEFAULTS.clearingSpeed.kfzStraight |
| 228 | ? defaults.clearingSpeed.kfzStraight |
| 229 | : null; |
| 230 | if (abweichendeVorgabe !== null && abweichendeVorgabe < clearingSpeed) { |
| 231 | clearingSpeed = abweichendeVorgabe; |
| 232 | } |
| 233 | const vorgabe = clearing.clearingSpeedOverride; |
| 234 | if (gueltigeRaeumgeschwindigkeit(vorgabe)) { |
| 235 | if (vorgabe <= clearingSpeed) { |
| 236 | clearingSpeed = vorgabe; |
| 237 | } else { |
| 238 | notes.push({ |
| 239 | severity: 'warnung', |
| 240 | code: 'engstelle-raeumgeschwindigkeit-ueber-regelwert', |
| 241 | message: |
| 242 | `Die eingetragene Räumgeschwindigkeit ${formatZahl(vorgabe)} m/s liegt über der ` + |
| 243 | `angesetzten Räumgeschwindigkeit der Engstelle (${formatZahl(clearingSpeed)} m/s; ` + |
| 244 | `Regelwert ${formatZahl(ergebnis.raeumgeschwindigkeit.vrKmh)} km/h nach RiLSA 2015, ` + |
| 245 | 'Abschnitt 5.2.2). Sie wird nicht angesetzt - ein höherer Wert würde die Zwischenzeit ' + |
| 246 | 'gegenüber dem Regelwerk verkürzen.', |
| 247 | }); |
| 248 | } |
| 249 | } |
| 250 | const clearingTime = clearingSpeed > 0 ? ergebnis.raeumweg / clearingSpeed : 0; |
| 251 | const roh = ergebnis.ueberfahrzeit + clearingTime; |
| 252 | |
| 253 | const einfahrweg = isFiniteNumber(entering.enteringDistance) ? entering.enteringDistance : 0; |
| 254 | if (einfahrweg > 0) { |
| 255 | notes.push({ |
| 256 | severity: 'hinweis', |
| 257 | code: 'engstelle-einfahrweg-ohne-wirkung', |
| 258 | message: |
| 259 | `Der Einfahrweg von ${formatZahl(einfahrweg)} m geht in diese Zwischenzeit nicht ein: ` + |
| 260 | 'An einer Engstelle wird die Zwischenzeit nach RiLSA 2015, Abschnitt 5.2.2 ohne ' + |
| 261 | 'Berücksichtigung einer Einfahrzeit ermittelt.', |
| 262 | }); |
| 263 | } |
| 264 | |
| 265 | if (ergebnis.raeumgeschwindigkeit.grund === 'unter-staffel') { |
| 266 | notes.push({ |
| 267 | severity: 'hinweis', |
| 268 | code: 'engstelle-vzul-ausserhalb-staffel', |
| 269 | message: |
| 270 | `Für die zulässige Höchstgeschwindigkeit liegt keine Stufe der Staffel vor; angesetzt ` + |
| 271 | `werden ${formatZahl(ergebnis.raeumgeschwindigkeit.vrKmh)} km/h. Die RiLSA 2015 nennt in ` + |
| 272 | 'Abschnitt 5.2.2 Räumgeschwindigkeiten für 60, 50 und 40 km/h.', |
| 273 | }); |
| 274 | } |
| 275 | |
| 276 | return { |
| 277 | verfahren: 'engstelle', |
| 278 | intergreen: ceilSeconds(roh), |
| 279 | raw: roundTo(roh, 3), |
| 280 | crossingTime: ergebnis.ueberfahrzeit, |
| 281 | clearingPath: roundTo(ergebnis.raeumweg, 3), |
| 282 | vehicleLength: 0, |
| 283 | clearingSpeed: roundTo(clearingSpeed, 3), |
| 284 | raeumansatz: 'geschwindigkeit', |
| 285 | anfahrbeschleunigung: null, |
| 286 | clearingTime: roundTo(clearingTime, 3), |
| 287 | ueberfahrRaeumzeitUntergrenze: null, |
| 288 | enteringPath: 0, |
| 289 | enteringSpeed: 0, |
| 290 | enteringTime: 0, |
| 291 | clampedToZero: false, |
| 292 | notes, |
| 293 | }; |
| 294 | } |
| 295 | |
| 296 | /** |
| 297 | * Toleranz gegen Gleitkommarauschen beim Vergleich mit der Untergrenze: Eine |
| 298 | * Ueberfahr- und Raeumzeit von exakt tG + 1 darf die Meldung nicht ausloesen, |
| 299 | * auch wenn sie als 3,9999999999999996 s herauskommt. |
| 300 | */ |
| 301 | const RUNDUNGSRAUSCHEN = 1e-9; |
| 302 | |
| 303 | /** |
| 304 | * Untergrenze der Ueberfahr- und Raeumzeit fuer diesen raeumenden Strom, oder |
| 305 | * null, wenn es fuer ihn keine gibt. |
| 306 | * |
| 307 | * Die Schranke der RiLSA 2015 (Abschnitt 2.5.2) gilt ausdruecklich nur den |
| 308 | * Faellen 1 und 2 - geradeaus fahrende und abbiegende KRAFTFAHRZEUGE. Fuer |
| 309 | * Radfahrer (Fall 5), Fussgaenger (Fall 6) und den OePNV (Faelle 3 und 4) |
| 310 | * nennt die RiLSA keine solche Bedingung, und sie hierher zu uebertragen |
| 311 | * hiesse, das Regelwerk zu ergaenzen. Der OePNV bleibt auch deshalb aussen |
| 312 | * vor, weil seine Sondersignale kein Gelb zeigen: tG gaebe es dort nicht. |
| 313 | * |
| 314 | * Massgebend ist die GELBZEIT des raeumenden Stroms, nicht die angesetzte |
| 315 | * Ueberfahrzeit - beim Ansatz "feste Ueberfahrzeit" fallen beide auseinander, |
| 316 | * und die RiLSA schreibt tG. |
| 317 | */ |
| 318 | function ueberfahrRaeumzeitUntergrenze( |
| 319 | clearing: ClearingStream, |
| 320 | defaults: RilsaDefaults, |
| 321 | ): Seconds | null { |
| 322 | if (clearing.mode !== 'kfz') return null; |
| 323 | return yellowTime('kfz', clearing.vZul, defaults) + UEBERFAHR_RAEUMZEIT_ZUSCHLAG; |
| 324 | } |
| 325 | |
| 326 | /** |
| 327 | * Braucht der Raeumweg eines Stroms dieser Verkehrsart einen |
| 328 | * Fahrzeug-Laengenzuschlag von mindestens `SAFETY_FLOORS.minVehicleLengthKfz`? |
| 329 | * |
| 330 | * Nur Kfz- und OePNV-Stroeme. Fuer Fussgaenger gibt es keinen Zuschlag, und |
| 331 | * beim Rad ist ein kurzer Zuschlag der Regelfall; nach OBEN ist ein zu langer |
| 332 | * Zuschlag ohnehin nur unwirtschaftlich, nicht unsicher. |
| 333 | */ |
| 334 | export function istKraftfahrzeugstrom(mode: TrafficMode): boolean { |
| 335 | return mode === 'kfz' || mode === 'oepnv'; |
| 336 | } |
| 337 | |
| 338 | /** |
| 339 | * Haelt der Laengenzuschlag dieser Fahrzeugart die harte Schranke fuer |
| 340 | * Kraftfahrzeuge ein? |
| 341 | * |
| 342 | * Gefragt wird nach dem EINGESTELLTEN Zuschlag und nicht nach einer festen |
| 343 | * Liste von Namen: Er ist ueber die Vorgabenverwaltung aenderbar, und eine |
| 344 | * Namensliste liefe dann daneben. Eine dem Kennwertsatz unbekannte Fahrzeugart |
| 345 | * hat keinen Zuschlag und genuegt damit nicht - fuer sie fuehrt |
| 346 | * `resolveVehicleLength` die eigene Notiz `fahrzeugart-unbekannt`. |
| 347 | * |
| 348 | * NEU (Fassung 5.9.0): Diese Frage wurde an zwei Stellen beantwortet - im |
| 349 | * Auswahlfeld der Signalgruppentabelle und nirgends sonst. Jetzt holen sie |
| 350 | * Einlesen (`model/schema.ts`) und Zwischenzeitrechnung hier; zwei Fassungen |
| 351 | * derselben Schranke laufen auseinander. |
| 352 | */ |
| 353 | export function laengenzuschlagGenuegt( |
| 354 | vehicleClass: VehicleClass, |
| 355 | defaults: RilsaDefaults = RILSA_DEFAULTS, |
| 356 | ): boolean { |
| 357 | const zuschlag = defaults.vehicleLength[vehicleClass] as number | undefined; |
| 358 | return ( |
| 359 | zuschlag !== undefined && |
| 360 | isFiniteNumber(zuschlag) && |
| 361 | zuschlag >= SAFETY_FLOORS.minVehicleLengthKfz |
| 362 | ); |
| 363 | } |
| 364 | |
| 365 | /** |
| 366 | * Laengenzuschlag zum Raeumweg nach Fahrzeugart. |
| 367 | * |
| 368 | * KORREKTUR (Fassung 5.4.0, Befund C8): Hier stand |
| 369 | * `defaults.vehicleLength[clearing.vehicleClass] ?? 0`. Eine Fahrzeugart, die |
| 370 | * der Kennwertsatz nicht kennt (nur ueber geladene Altdaten erreichbar - das |
| 371 | * Schema setzt Unbekanntes beim Einlesen auf die Standardart der Verkehrsart |
| 372 | * und meldet es; dies ist der zweite Riegel), setzte damit stumm 0 m an und |
| 373 | * verkuerzte die Zwischenzeit. Der einzige Eingabefehler ohne Meldung. Jetzt: |
| 374 | * Fehler-Notiz und die RiLSA-Rechenlaenge fuer Kraftfahrzeuge (6 m, |
| 375 | * SAFETY_FLOORS) als konservativer Ansatz. Fussgaenger haben keinen |
| 376 | * Laengenzuschlag; fuer sie bleibt es bei 0 m ohne Notiz - eine unbekannte |
| 377 | * Fahrzeugart aendert dort nichts am Ergebnis. |
| 378 | * |
| 379 | * NEU (Fassung 5.9.0): Dieselbe Verkuerzung erreichte eine BEKANNTE Art mit |
| 380 | * zu kurzem Zuschlag. "Ohne Laengenzuschlag" und "Fahrrad" stehen mit 0 m im |
| 381 | * Kennwertsatz; an einem Kfz- oder OePNV-Strom blieb der Raeumweg damit ohne |
| 382 | * Fahrzeuglaenge, und die Zwischenzeit fiel um rund eine Sekunde kuerzer aus. |
| 383 | * Die Notiz oben griff nicht - 0 m ist ein hinterlegter Wert -, und die harte |
| 384 | * Schranke `SAFETY_FLOORS.minVehicleLengthKfz`, die die Vorgabenverwaltung an |
| 385 | * jedem Kfz-Laengenfeld erzwingt, war damit ueber die Fahrzeugart umgehbar. |
| 386 | * Das Auswahlfeld ist dafuer seit der Fassung 5.9.0 geschlossen; dies ist der |
| 387 | * Riegel im Fachkern, der fuer jeden Aufrufer gilt - auch fuer den, |
| 388 | * der einen raeumenden Strom von Hand zusammensetzt. |
| 389 | */ |
| 390 | function resolveVehicleLength( |
| 391 | clearing: ClearingStream, |
| 392 | defaults: RilsaDefaults, |
| 393 | notes: CalculationNote[], |
| 394 | ): Meters { |
| 395 | const ersatz = SAFETY_FLOORS.minVehicleLengthKfz; |
| 396 | const hinterlegt = defaults.vehicleLength[clearing.vehicleClass] as Meters | undefined; |
| 397 | |
| 398 | if (hinterlegt === undefined || !isFiniteNumber(hinterlegt)) { |
| 399 | if (clearing.mode === 'fuss') return 0; |
| 400 | notes.push({ |
| 401 | severity: 'fehler', |
| 402 | code: 'fahrzeugart-unbekannt', |
| 403 | message: |
| 404 | `Die Fahrzeugart "${String(clearing.vehicleClass)}" des räumenden Stroms ist unbekannt; ` + |
| 405 | 'für sie ist keine Rechenlänge hinterlegt. Angesetzt wird die RiLSA-Rechenlänge für ' + |
| 406 | `Kraftfahrzeuge von ${formatZahl(ersatz)} m. Wählen Sie an der Signalgruppe eine ` + |
| 407 | 'bekannte Fahrzeugart.', |
| 408 | }); |
| 409 | return ersatz; |
| 410 | } |
| 411 | |
| 412 | if ( |
| 413 | !istKraftfahrzeugstrom(clearing.mode) || |
| 414 | laengenzuschlagGenuegt(clearing.vehicleClass, defaults) |
| 415 | ) { |
| 416 | return hinterlegt; |
| 417 | } |
| 418 | |
| 419 | notes.push({ |
| 420 | severity: 'fehler', |
| 421 | code: 'fahrzeugart-laengenzuschlag-zu-kurz', |
| 422 | message: |
| 423 | `Die Fahrzeugart "${String(clearing.vehicleClass)}" des räumenden Stroms hat mit ` + |
| 424 | `${formatZahl(hinterlegt)} m einen Längenzuschlag unter der RiLSA-Rechenlänge für ` + |
| 425 | `Kraftfahrzeuge von ${formatZahl(ersatz)} m; an einem Kraftfahrzeug- oder ÖPNV-Strom ` + |
| 426 | 'würde er den Räumweg und damit die Zwischenzeit unzulässig verkürzen. Angesetzt werden ' + |
| 427 | `${formatZahl(ersatz)} m. Wählen Sie an der Signalgruppe eine Fahrzeugart mit mindestens ` + |
| 428 | 'diesem Längenzuschlag.', |
| 429 | }); |
| 430 | return ersatz; |
| 431 | } |
| 432 | |
| 433 | /** |
| 434 | * Raeumgeschwindigkeit nach Verkehrsart und Fahrbeziehung. |
| 435 | * Abbiegende Kraftfahrzeuge raeumen langsamer und damit ungueenstiger. |
| 436 | * |
| 437 | * KORREKTUR (Fassung 5.9.0): Dieselbe Zusage stand hier als |
| 438 | * Kommentar und wurde von jedem Kennwertsatz gebrochen, in dem |
| 439 | * `kfzStraight` unter `kfzTurning` lag - beide Felder sind von 3 bis 10 m/s |
| 440 | * frei einstellbar. Ebenso beim Fussgaengerpaar: `fussReducedMobility` (bis |
| 441 | * 1,2 m/s) und `fuss` (ab 0,6 m/s) standen unverbunden nebeneinander, sodass |
| 442 | * das Merkmal "erhoehter Zeitbedarf" die Zwischenzeit VERKUERZEN konnte. |
| 443 | * Angesetzt wird jetzt in beiden Paaren der kleinere Wert - dieselbe |
| 444 | * Behandlung wie beim engen Innenradius. resolveRilsaSettings kappt das mit |
| 445 | * eigener Meldung (der erste Riegel); hier steht der zweite fuer jeden |
| 446 | * Aufrufer, der einen Kennwertsatz von Hand zusammensetzt. |
| 447 | * |
| 448 | * `vZul` wird nur fuer den OePNV gebraucht und ist deshalb optional: Fuer |
| 449 | * gesondert signalisierten OePNV gilt vr = Vmax/3,6 (KORREKTUR Fassung 5.4.0, |
| 450 | * Befund A2; RiLSA 2015, Fall 3, woertlich im FGSV-Aenderungsblatt |
| 451 | * vom 29.07.2015: "vr = Vmax/3,6 (Vmax in [km/h])"). Ohne bekannte Vmax |
| 452 | * bleibt es beim konservativen Rueckfallwert aus den Vorgaben. |
| 453 | * |
| 454 | * NEU (Fassung 5.5.0): Ist an der Konfliktbeziehung der enge Innenradius |
| 455 | * gesetzt, raeumt der abbiegende Kfz-Strom mit dem kleineren Wert. Das Merkmal |
| 456 | * ist ein Praxisansatz und wirkt NUR beim Abbiegen - geradeaus gibt es keinen |
| 457 | * Innenradius, und ein Merkmal, das dort etwas veraendert, waere ein |
| 458 | * stillschweigender Zuschlag. `engerRadius` ist optional, damit jeder |
| 459 | * vorhandene Aufrufer unveraendert weiterrechnet. |
| 460 | * |
| 461 | * KORREKTUR (Fassung 5.5.0): Angesetzt wird der KLEINERE der beiden Werte. Die |
| 462 | * Feldschranke der Vorgabenverwaltung misst |
| 463 | * `clearingSpeedKfzTurningEngerRadius` gegen den REGELWERT von 7,0 m/s, die |
| 464 | * wirksame Raeumgeschwindigkeit des Abbiegers ist daneben bis auf 3 m/s |
| 465 | * absenkbar. Mit `kfzTurning` = 4 m/s und dem Regelwert 5,0 m/s des Merkmals |
| 466 | * raeumte ein angehakter enger Innenradius SCHNELLER als derselbe Abbieger ohne |
| 467 | * Merkmal - die Zwischenzeit fiel um 1 s kuerzer aus, waehrend Pruefbericht, |
| 468 | * Ausdruck und Kaestchen das Gegenteil behaupteten. resolveRilsaSettings kappt |
| 469 | * das mit eigener Meldung (der erste Riegel); hier steht der zweite fuer jeden |
| 470 | * Aufrufer, der einen Kennwertsatz von Hand zusammensetzt. Ohne Meldung, weil |
| 471 | * diese Funktion keine fuehrt - resolveClearingSpeed setzt sie ab. |
| 472 | */ |
| 473 | export function clearingSpeedFor( |
| 474 | clearing: Pick<ClearingStream, 'mode' | 'movement' | 'reducedMobility'> & |
| 475 | Partial<Pick<ClearingStream, 'vZul' | 'engerRadius'>>, |
| 476 | defaults: RilsaDefaults = RILSA_DEFAULTS, |
| 477 | ): MetersPerSecond { |
| 478 | switch (clearing.mode) { |
| 479 | case 'kfz': { |
| 480 | const geradeaus = defaults.clearingSpeed.kfzStraight; |
| 481 | if (clearing.movement !== 'links' && clearing.movement !== 'rechts') return geradeaus; |
| 482 | const abbiegend = Math.min(defaults.clearingSpeed.kfzTurning, geradeaus); |
| 483 | return clearing.engerRadius === true |
| 484 | ? Math.min(defaults.clearingSpeed.kfzTurningEngerRadius, abbiegend) |
| 485 | : abbiegend; |
| 486 | } |
| 487 | case 'rad': |
| 488 | return defaults.clearingSpeed.rad; |
| 489 | case 'fuss': |
| 490 | return clearing.reducedMobility === true |
| 491 | ? Math.min(defaults.clearingSpeed.fussReducedMobility, defaults.clearingSpeed.fuss) |
| 492 | : defaults.clearingSpeed.fuss; |
| 493 | case 'oepnv': |
| 494 | return clearing.vZul !== undefined && clearing.vZul > 0 |
| 495 | ? clearing.vZul / 3.6 |
| 496 | : defaults.clearingSpeed.oepnv; |
| 497 | } |
| 498 | } |
| 499 | |
| 500 | /** |
| 501 | * Gilt eine von Hand eingetragene Raeumgeschwindigkeit, oder verwirft der |
| 502 | * Fachkern sie? |
| 503 | * |
| 504 | * DAS PRAEDIKAT STEHT IM FACHKERN: Dieselbe Bedingung entscheidet an drei |
| 505 | * Stellen ueber dieselbe Sache - `resolveClearingSpeed` verwirft einen Eintrag, |
| 506 | * der nicht endlich oder nicht groesser als 0 ist, und rechnet mit dem |
| 507 | * Regelwert weiter; `engerRadiusWirktAn` haengt daran, ob das Merkmal daneben |
| 508 | * ueberhaupt noch in eine Zahl eingeht; und die Pruefregel zum raeumenden Strom |
| 509 | * (validation/rules.ts, `raeumansatzFuer`) muss dieselbe Frage beantworten, um |
| 510 | * den Vergleich der Fahrbeziehungen nicht gegenstandslos zu fuehren. Sie stand |
| 511 | * dort als eigene Fassung, weil es das Praedikat hier noch nicht gab. |
| 512 | * |
| 513 | * `null` und `undefined` heissen beide "nichts eingetragen": Das Modell fuehrt |
| 514 | * die Vorgabe je Beziehung als `number | null` (model/project.ts), der raeumende |
| 515 | * Strom des Fachkerns als optionales Feld. |
| 516 | */ |
| 517 | export function gueltigeRaeumgeschwindigkeit( |
| 518 | vorgabe: MetersPerSecond | null | undefined, |
| 519 | ): vorgabe is MetersPerSecond { |
| 520 | return isFiniteNumber(vorgabe) && vorgabe > 0; |
| 521 | } |
| 522 | |
| 523 | /** |
| 524 | * Gilt eine von Hand eingetragene Ueberfahrzeit? Bedingung aus |
| 525 | * `resolveCrossingTime` weiter unten: endlich und nicht negativ - 0 s ist |
| 526 | * zulaessig, sie ist der Haltefall des OePNV. Steht aus demselben Grund hier |
| 527 | * wie das Praedikat darueber. |
| 528 | */ |
| 529 | export function gueltigeUeberfahrzeit(vorgabe: Seconds | null | undefined): vorgabe is Seconds { |
| 530 | return isFiniteNumber(vorgabe) && vorgabe >= 0; |
| 531 | } |
| 532 | |
| 533 | /** |
| 534 | * Geht das Merkmal "enger Innenradius" an DIESEM Strom in die Zwischenzeit ein? |
| 535 | * |
| 536 | * DREI BEDINGUNGEN, EINE STELLE: Das Merkmal wirkt nur bei einem abbiegenden |
| 537 | * Kraftfahrzeugstrom - und nur, solange daneben keine Raeumgeschwindigkeit von |
| 538 | * Hand eingetragen ist. Die Handeingabe ersetzt in resolveClearingSpeed den |
| 539 | * gesamten Regelwert, also auch den des Merkmals; wer beides setzt, raeumt mit |
| 540 | * der eingetragenen Zahl. |
| 541 | * |
| 542 | * Zuvor fragten Pruefbericht (engine.ts) und Konfliktfenster |
| 543 | * (conflictsView.ts) nur die ersten beiden Bedingungen ab und meldeten bei |
| 544 | * gleichzeitiger Handeingabe von 9 m/s: "raeumt deshalb mit 5 m/s statt mit |
| 545 | * 7 m/s; die Zwischenzeit faellt laenger aus" - gerechnet wurde mit 9 m/s, und |
| 546 | * die Zwischenzeit fiel KUERZER aus als im Regelfall. |
| 547 | * |
| 548 | * Ein ungueltiger Eintrag (nicht endlich, 0 oder negativ) faellt in |
| 549 | * resolveClearingSpeed auf den Regelwert zurueck; dann wirkt das Merkmal |
| 550 | * weiter. Gefragt wird dazu `gueltigeRaeumgeschwindigkeit` - eine zweite, |
| 551 | * anders geschriebene Fassung waere genau der Fehler, den diese Funktion |
| 552 | * beseitigt. |
| 553 | */ |
| 554 | export function engerRadiusWirktAn( |
| 555 | clearing: Pick<ClearingStream, 'mode' | 'movement'> & |
| 556 | Partial<Pick<ClearingStream, 'engerRadius' | 'clearingSpeedOverride'>>, |
| 557 | ): boolean { |
| 558 | if (clearing.engerRadius !== true) return false; |
| 559 | if (clearing.mode !== 'kfz') return false; |
| 560 | if (clearing.movement !== 'links' && clearing.movement !== 'rechts') return false; |
| 561 | return !gueltigeRaeumgeschwindigkeit(clearing.clearingSpeedOverride); |
| 562 | } |
| 563 | |
| 564 | /** |
| 565 | * Harte Obergrenze der Raeumgeschwindigkeit je Verkehrsart. |
| 566 | * |
| 567 | * KORREKTUR (Fassung 5.4.0, Befund A1): Rad wurde bislang gegen die |
| 568 | * Kfz-Schranke geprueft; jetzt gilt der eigene RiLSA-Regelwert als Deckel. |
| 569 | * |
| 570 | * KORREKTUR (Fassung 5.4.0, Befund A2): Fuer den OePNV haengt die |
| 571 | * Schranke am Strom - nicht schneller raeumen, als gefahren werden darf |
| 572 | * (Vmax/3,6), und nie schneller als 70 km/h / 3,6 (oberhalb 70 km/h sollen |
| 573 | * LSA nicht betrieben werden, VwV-StVO zu Par. 37 Rn. 10). Ohne bekannte |
| 574 | * Vmax bleibt die alte Kfz-Schranke von 10 m/s der konservative Deckel. |
| 575 | */ |
| 576 | function hoechsteRaeumgeschwindigkeit( |
| 577 | clearing: Pick<ClearingStream, 'mode' | 'vZul'>, |
| 578 | ): MetersPerSecond { |
| 579 | switch (clearing.mode) { |
| 580 | case 'fuss': |
| 581 | return SAFETY_FLOORS.maxClearingSpeedFuss; |
| 582 | case 'rad': |
| 583 | return SAFETY_FLOORS.maxClearingSpeedRad; |
| 584 | case 'oepnv': |
| 585 | return clearing.vZul > 0 |
| 586 | ? Math.min(SAFETY_FLOORS.maxClearingSpeedOepnv, clearing.vZul / 3.6) |
| 587 | : SAFETY_FLOORS.maxClearingSpeedKfz; |
| 588 | // Kein eigener Zweig fuer 'kfz' - der Linter meldet das als fehlenden |
| 589 | // Fall. `TrafficMode` hat genau vier Glieder (types.ts), drei davon stehen |
| 590 | // oben: Den default erreicht nur 'kfz', und er gibt genau dessen Deckel |
| 591 | // zurueck - ein eigener case waere derselbe Rueckgabewert ein zweites Mal. |
| 592 | // Eine unbekannte Verkehrsart aus einer Altdatei kommt hier nicht an; |
| 593 | // schema.ts setzt sie beim Einlesen mit Warnung auf 'kfz'. |
| 594 | default: |
| 595 | return SAFETY_FLOORS.maxClearingSpeedKfz; |
| 596 | } |
| 597 | } |
| 598 | |
| 599 | /** |
| 600 | * Die Merkmale eines raeumenden Stroms, aus denen sich sein REGELWERT der |
| 601 | * Raeumgeschwindigkeit ergibt - ohne Handeingabe. |
| 602 | */ |
| 603 | export type Raeumstromkennung = Pick<ClearingStream, 'mode' | 'movement' | 'vZul'> & |
| 604 | Partial<Pick<ClearingStream, 'reducedMobility' | 'engerRadius'>>; |
| 605 | |
| 606 | /** |
| 607 | * Nicht schneller raeumen, als gefahren werden darf - der Deckel an der Vmax |
| 608 | * des raeumenden Kraftfahrzeugstroms (Fassung 5.22.0). |
| 609 | * |
| 610 | * DER BEFUND KAM AUS EINEM ANGEORDNETEN PLAN. Einstreifige |
| 611 | * Fuehrung, 100 m Engstelle, 30 km/h: Die behoerdlich abgesegnete Unterlage |
| 612 | * traegt 16 s, dieses Programm rechnete 14 s - zwei Sekunden KUERZER, die |
| 613 | * gefaehrliche Richtung. Mit dem festen Regelwert von 10,0 m/s raeumte ein |
| 614 | * Fahrzeug rechnerisch mit 36 km/h, also schneller, als es fahren darf. |
| 615 | * |
| 616 | * DER GRUNDSATZ STAND SCHON IM PROGRAMM: `maxClearingSpeedOepnv` traegt ihn im |
| 617 | * Kommentar, und fuer den OePNV wird er in `hoechsteRaeumgeschwindigkeit` auch |
| 618 | * angewandt. Fuer Kraftfahrzeuge fehlte er. |
| 619 | * |
| 620 | * NUR NACH UNTEN, und nur beim Kraftfahrzeug: Ueber 36 km/h ist der Regelwert |
| 621 | * ohnehin der kleinere - dort aendert der Deckel nichts, und die Korrektur |
| 622 | * gegenueber der Fassung 4.x (die mit Vmax/3,6 rechnete und deshalb bei |
| 623 | * 50 km/h zu kurze Zwischenzeiten lieferte) bleibt unangetastet. Der |
| 624 | * abbiegende Strom raeumt mit 7,0 m/s ohnehin langsamer. Fussgaenger und |
| 625 | * Radverkehr haben mit der Vmax des Fahrverkehrs nichts zu tun, und der OePNV |
| 626 | * hat seinen eigenen Deckel. |
| 627 | * |
| 628 | * NICHT AUF DIE EINFAHRGESCHWINDIGKEIT UEBERTRAGEN: In tz = tue + tr − te wird |
| 629 | * die Einfahrzeit abgezogen; ein Deckel dort verkuerzte die Zwischenzeit. Die |
| 630 | * Schranke der Einfahrgeschwindigkeit liegt unten (siehe constants.ts). |
| 631 | */ |
| 632 | function vmaxDeckel(clearing: Raeumstromkennung): MetersPerSecond { |
| 633 | if (clearing.mode !== 'kfz') return Number.POSITIVE_INFINITY; |
| 634 | return clearing.vZul > 0 ? clearing.vZul / 3.6 : Number.POSITIVE_INFINITY; |
| 635 | } |
| 636 | |
| 637 | /** Zwischenstand der Regelwertaufloesung, samt der Gruende fuer eine Kappung. */ |
| 638 | interface Regelaufloesung { |
| 639 | /** Harte Obergrenze der Verkehrsart bzw. des Stroms. */ |
| 640 | readonly ceiling: MetersPerSecond; |
| 641 | /** Ungekappter Wert aus Verkehrsart, Fahrbeziehung und Merkmalen. */ |
| 642 | readonly roh: MetersPerSecond; |
| 643 | /** OePNV oberhalb der Staffelgrenze von 70 km/h. */ |
| 644 | readonly oepnvUeber70: boolean; |
| 645 | /** Der Wert aus den Vorgaben liegt ueber der harten Obergrenze. */ |
| 646 | readonly ueberSchranke: boolean; |
| 647 | /** Kfz-Strom, der schneller raeumen wuerde, als er fahren darf. */ |
| 648 | readonly ueberVmax: boolean; |
| 649 | /** Der Deckel an der Vmax, sofern es einen gibt. */ |
| 650 | readonly deckel: MetersPerSecond; |
| 651 | /** Der Regelwert, wie er ohne den Vmax-Deckel angesetzt wuerde. */ |
| 652 | readonly vorDeckel: MetersPerSecond; |
| 653 | /** Angesetzter Regelwert nach allen Kappungen. */ |
| 654 | readonly regular: MetersPerSecond; |
| 655 | } |
| 656 | |
| 657 | function loeseRegelRaeumgeschwindigkeit( |
| 658 | clearing: Raeumstromkennung, |
| 659 | defaults: RilsaDefaults, |
| 660 | ): Regelaufloesung { |
| 661 | const ceiling = hoechsteRaeumgeschwindigkeit(clearing); |
| 662 | const roh = clearingSpeedFor(clearing, defaults); |
| 663 | const oepnvUeber70 = clearing.mode === 'oepnv' && clearing.vZul > 70; |
| 664 | const nachStaffelgrenze = oepnvUeber70 ? ceiling : roh; |
| 665 | const ueberSchranke = nachStaffelgrenze > ceiling; |
| 666 | /* |
| 667 | * Der Vmax-Deckel greift NACH der harten Schranke, nicht statt ihrer: Ein |
| 668 | * Kennwertsatz mit 12 m/s bei 30 km/h ueberschreitet beides, und beides ist |
| 669 | * ein eigener Sachverhalt. Die Meldung zur ueberhoehten Vorgabe darf nicht |
| 670 | * dadurch verschwinden, dass der Deckel ohnehin greift. |
| 671 | */ |
| 672 | const nachSchranke = ueberSchranke ? ceiling : nachStaffelgrenze; |
| 673 | const deckel = vmaxDeckel(clearing); |
| 674 | const ueberVmax = nachSchranke > deckel; |
| 675 | return { |
| 676 | ceiling, |
| 677 | roh, |
| 678 | oepnvUeber70, |
| 679 | ueberSchranke, |
| 680 | ueberVmax, |
| 681 | deckel, |
| 682 | vorDeckel: nachSchranke, |
| 683 | regular: ueberVmax ? deckel : nachSchranke, |
| 684 | }; |
| 685 | } |
| 686 | |
| 687 | /** |
| 688 | * Regelwert der Raeumgeschwindigkeit DIESES Stroms - der Wert, mit dem ohne |
| 689 | * Handeingabe gerechnet wuerde. |
| 690 | * |
| 691 | * NEU (Fassung 5.9.0): Er wird an zwei Stellen gebraucht - als Bezugsgroesse |
| 692 | * der Abweichungsmeldung in resolveClearingSpeed und als Beschriftung des |
| 693 | * Eingabefeldes im Konfliktfenster, das bis dahin die Handeingabe selbst |
| 694 | * "Regelwert" nannte. Beide holen ihn hier und rechnen ihn nicht je fuer sich |
| 695 | * nach; zwei Fassungen derselben Aufloesung laufen auseinander. |
| 696 | */ |
| 697 | export function regelRaeumgeschwindigkeit( |
| 698 | clearing: Raeumstromkennung, |
| 699 | defaults: RilsaDefaults = RILSA_DEFAULTS, |
| 700 | ): MetersPerSecond { |
| 701 | return loeseRegelRaeumgeschwindigkeit(clearing, defaults).regular; |
| 702 | } |
| 703 | |
| 704 | /** |
| 705 | * Die Werte, gegen die der Bearbeiter seine Handeingaben abwaegt - nach dem |
| 706 | * VERFAHREN, mit dem diese Beziehung wirklich gerechnet wird. |
| 707 | * |
| 708 | * WARUM ES DIESE FUNKTION GIBT (Befund aus dem Betrieb, 11.09.2026). Das |
| 709 | * Zwischenzeitfenster nannte unter dem Feld "Raeumgeschwindigkeit" den |
| 710 | * Regelwert der KNOTENPUNKTRECHNUNG - 10,0 m/s - und rechnete zwei Zeilen |
| 711 | * tiefer im selben Fenster mit 11,11 m/s. An einer einstreifigen |
| 712 | * Verkehrsfuehrung gilt naemlich nicht der Vorgabenwert, sondern die |
| 713 | * Raeumgeschwindigkeitsstaffel des Abschnitts 5.2 (siehe |
| 714 | * `computeEngstellenIntergreen`), und `regelRaeumgeschwindigkeit` kennt die |
| 715 | * Anlagenart nicht. Wer seine Eingabe gegen die genannte Zahl abwog, wog gegen |
| 716 | * die falsche ab - und zwar zur unsicheren Seite hin: 10 m/s sieht strenger |
| 717 | * aus, als das Verfahren verlangt, also erschien eine Handeingabe daneben |
| 718 | * harmloser, als sie ist. |
| 719 | * |
| 720 | * Dasselbe an den beiden Nachbarfeldern. Die Ueberfahrzeit der Engstelle steht |
| 721 | * mit 4 s fest im Abschnitt und ist nicht die Gelbzeit. Und die |
| 722 | * EINFAHRGESCHWINDIGKEIT geht dort in gar keine Zahl ein, weil der Abschnitt |
| 723 | * ausdruecklich ohne Einfahrzeit rechnet; ein Regelwert unter einem Feld ohne |
| 724 | * Wirkung ist keine Auskunft, sondern eine Einladung, daran zu drehen. Er ist |
| 725 | * deshalb `null`, und die Ansicht schreibt hin, warum. |
| 726 | * |
| 727 | * Die Zahlen stehen NUR HIER und werden in der Ansicht nicht nachgerechnet - |
| 728 | * eine zweite Fassung liefe beim naechsten Kennwert auseinander. |
| 729 | */ |
| 730 | export interface Bezugswerte { |
| 731 | readonly verfahren: IntergreenResult['verfahren']; |
| 732 | readonly raeumgeschwindigkeit: MetersPerSecond; |
| 733 | /** `null`, wo die Einfahrgeschwindigkeit in keine Zahl eingeht. */ |
| 734 | readonly einfahrgeschwindigkeit: MetersPerSecond | null; |
| 735 | readonly ueberfahrzeit: Seconds; |
| 736 | } |
| 737 | |
| 738 | export function bezugswerte( |
| 739 | verfahren: IntergreenResult['verfahren'], |
| 740 | clearing: Raeumstromkennung & Ueberfahrzeitkennung, |
| 741 | einfahrendeArt: TrafficMode, |
| 742 | defaults: RilsaDefaults = RILSA_DEFAULTS, |
| 743 | ): Bezugswerte { |
| 744 | if (verfahren === 'engstelle') { |
| 745 | return { |
| 746 | verfahren, |
| 747 | // Ohne Annahmen, weil der Signalzeitenplan das Engstellenverfahren |
| 748 | // ebenfalls ohne sie aufruft (buildSignalPlan). |
| 749 | raeumgeschwindigkeit: engstellenRaeumgeschwindigkeitMs(clearing.vZul), |
| 750 | einfahrgeschwindigkeit: null, |
| 751 | ueberfahrzeit: RILSA_ENGSTELLE.ueberfahrzeit, |
| 752 | }; |
| 753 | } |
| 754 | return { |
| 755 | verfahren, |
| 756 | raeumgeschwindigkeit: loeseRegelRaeumgeschwindigkeit(clearing, defaults).regular, |
| 757 | einfahrgeschwindigkeit: defaults.enteringSpeed[einfahrendeArt], |
| 758 | ueberfahrzeit: regelUeberfahrzeit(clearing, defaults), |
| 759 | }; |
| 760 | } |
| 761 | |
| 762 | function resolveClearingSpeed( |
| 763 | clearing: ClearingStream, |
| 764 | defaults: RilsaDefaults, |
| 765 | notes: CalculationNote[], |
| 766 | ): MetersPerSecond { |
| 767 | const { ceiling, roh, oepnvUeber70, ueberSchranke, ueberVmax, deckel, vorDeckel, regular } = |
| 768 | loeseRegelRaeumgeschwindigkeit(clearing, defaults); |
| 769 | |
| 770 | /* |
| 771 | * Enger Innenradius, der nichts verschaerft. clearingSpeedFor setzt den |
| 772 | * kleineren der beiden Werte an; hier steht die Meldung dazu, damit die |
| 773 | * Kappung nicht die stillste Ersetzung von allen wird. Der Regelfall dieses |
| 774 | * Zweiges ist eine abgesenkte Raeumgeschwindigkeit des Abbiegers in den |
| 775 | * Vorgaben - dort wird derselbe Widerspruch bereits beim Aufloesen gemeldet |
| 776 | * und gekappt. |
| 777 | */ |
| 778 | if ( |
| 779 | clearing.mode === 'kfz' && |
| 780 | clearing.engerRadius === true && |
| 781 | (clearing.movement === 'links' || clearing.movement === 'rechts') && |
| 782 | defaults.clearingSpeed.kfzTurningEngerRadius > defaults.clearingSpeed.kfzTurning |
| 783 | ) { |
| 784 | notes.push({ |
| 785 | severity: 'fehler', |
| 786 | code: 'enger-radius-schneller-als-regelfall', |
| 787 | message: |
| 788 | `Die Räumgeschwindigkeit bei engem Innenradius ist mit ` + |
| 789 | `${formatZahl(defaults.clearingSpeed.kfzTurningEngerRadius)} m/s größer als die des ` + |
| 790 | `abbiegenden Kraftfahrzeugstroms ohne dieses Merkmal ` + |
| 791 | `(${formatZahl(defaults.clearingSpeed.kfzTurning)} m/s). Das Merkmal würde die ` + |
| 792 | 'Zwischenzeit verkürzen, statt sie zu verlängern. Angesetzt wird der kleinere Wert.', |
| 793 | }); |
| 794 | } |
| 795 | |
| 796 | /* |
| 797 | * Abbieger, der schneller raeumt als der Geradeausstrom (Fassung 5.9.0). |
| 798 | * Dieselbe Lage wie beim engen Innenradius, nur ein Feld weiter: |
| 799 | * "Raeumgeschwindigkeit Kfz geradeaus" und "Kfz abbiegend" sind beide von 3 |
| 800 | * bis 10 m/s einstellbar, und der Kommentar ueber clearingSpeedFor sagt seit |
| 801 | * jeher zu, abbiegende Kraftfahrzeuge raeumten langsamer. clearingSpeedFor |
| 802 | * setzt den kleineren Wert an; hier steht die Meldung dazu. |
| 803 | */ |
| 804 | if ( |
| 805 | clearing.mode === 'kfz' && |
| 806 | (clearing.movement === 'links' || clearing.movement === 'rechts') && |
| 807 | defaults.clearingSpeed.kfzTurning > defaults.clearingSpeed.kfzStraight |
| 808 | ) { |
| 809 | notes.push({ |
| 810 | severity: 'fehler', |
| 811 | code: 'abbiegend-schneller-als-geradeaus', |
| 812 | message: |
| 813 | `Die Räumgeschwindigkeit abbiegender Kraftfahrzeuge ist mit ` + |
| 814 | `${formatZahl(defaults.clearingSpeed.kfzTurning)} m/s größer als die des ` + |
| 815 | `Geradeausstroms (${formatZahl(defaults.clearingSpeed.kfzStraight)} m/s). Abbiegende ` + |
| 816 | 'Kraftfahrzeuge räumen nicht schneller als geradeaus fahrende; die Zwischenzeit fiele ' + |
| 817 | 'sonst gerade dort kürzer aus. Angesetzt wird der kleinere Wert.', |
| 818 | }); |
| 819 | } |
| 820 | |
| 821 | /* |
| 822 | * Erhoehter Zeitbedarf, der nichts verschaerft (Fassung 5.9.0). |
| 823 | * "Raeumgeschwindigkeit Fussgaenger" ist bis auf 0,6 m/s absenkbar, |
| 824 | * waehrend der Wert fuer den erhoehten Zeitbedarf davon unabhaengig bis |
| 825 | * 1,2 m/s reicht. Lag er darueber, raeumte der mobilitaetseingeschraenkte |
| 826 | * Fussgaenger rechnerisch SCHNELLER als der Regelfall - das angehakte |
| 827 | * Merkmal verkuerzte die Zwischenzeit fuer die schutzbeduerftigste |
| 828 | * Verkehrsart. |
| 829 | */ |
| 830 | if ( |
| 831 | clearing.mode === 'fuss' && |
| 832 | clearing.reducedMobility === true && |
| 833 | defaults.clearingSpeed.fussReducedMobility > defaults.clearingSpeed.fuss |
| 834 | ) { |
| 835 | notes.push({ |
| 836 | severity: 'fehler', |
| 837 | code: 'erhoehter-zeitbedarf-schneller-als-regelfall', |
| 838 | message: |
| 839 | `Die Räumgeschwindigkeit für Fußgänger mit erhöhtem Zeitbedarf ist mit ` + |
| 840 | `${formatZahl(defaults.clearingSpeed.fussReducedMobility)} m/s größer als die der ` + |
| 841 | `Fußgänger ohne dieses Merkmal (${formatZahl(defaults.clearingSpeed.fuss)} m/s). Das ` + |
| 842 | 'Merkmal würde die Zwischenzeit verkürzen, statt sie zu verlängern. Angesetzt wird der ' + |
| 843 | 'kleinere Wert.', |
| 844 | }); |
| 845 | } |
| 846 | |
| 847 | /* |
| 848 | * OePNV mit Vmax oberhalb der Staffelgrenze: vr = Vmax/3,6 wuerde die |
| 849 | * OePNV-Obergrenze von 19,4 m/s ueberschreiten. Gekappt wird mit eigener |
| 850 | * Meldung - die allgemeine Meldung darunter spraeche faelschlich von einem |
| 851 | * Wert "aus den Vorgaben", dabei kommt er aus der Vmax des Stroms. |
| 852 | * |
| 853 | * KORREKTUR (Fassung 5.9.0): Der Satz endete unbedingt mit |
| 854 | * "und die Ueberfahrzeit mit der obersten Staffelstufe angesetzt". Beim Halt |
| 855 | * vor dem Knotenpunkt (RiLSA-Fall 4) betraegt die Ueberfahrzeit aber 0 s - |
| 856 | * die Staffel wird gar nicht gelesen -, und der gekappte Wert ist dort keine |
| 857 | * konstante Raeumgeschwindigkeit, sondern die Grenzgeschwindigkeit des |
| 858 | * Anfahransatzes. Die Unterlage wies damit einen Zeitanteil aus, den die |
| 859 | * Zwischenzeit nicht enthaelt. Denselben Fallunterschied macht die |
| 860 | * Schwestermeldung 'oepnv-vmax-fehlt' weiter unten. |
| 861 | */ |
| 862 | if (oepnvUeber70) { |
| 863 | notes.push({ |
| 864 | severity: 'warnung', |
| 865 | code: 'oepnv-vmax-ueber-70', |
| 866 | message: |
| 867 | `Die zulässige Höchstgeschwindigkeit des räumenden ÖPNV-Stroms beträgt ` + |
| 868 | `${formatZahl(clearing.vZul)} km/h. Oberhalb von 70 km/h sollen Lichtsignalanlagen ` + |
| 869 | 'nicht betrieben werden (VwV-StVO zu § 37); die Räumgeschwindigkeit wird auf ' + |
| 870 | `70 km/h ÷ 3,6 = ${formatZahl(SAFETY_FLOORS.maxClearingSpeedOepnv)} m/s begrenzt` + |
| 871 | (clearing.haltVorKnoten === true |
| 872 | ? ' (Anfahransatz: beschleunigt wird höchstens bis zu diesem Wert); die Überfahrzeit ' + |
| 873 | 'beträgt beim Halt vor dem Knotenpunkt 0 s.' |
| 874 | : ' und die Überfahrzeit mit der obersten Staffelstufe angesetzt.'), |
| 875 | }); |
| 876 | } |
| 877 | |
| 878 | /* |
| 879 | * Auch der Regelwert aus den Vorgaben muss unter der Schranke liegen. |
| 880 | * KORREKTUR (Fassung 5.4.0, Befund A3): Bislang wurde nur die |
| 881 | * stromindividuelle Ueberschreibung gedeckelt - ein ueberhoehter Wert aus der |
| 882 | * Vorgabenverwaltung (etwa 15 m/s fuer den OePNV aus einem Altprojekt) lief |
| 883 | * ungeprueft in die Rechnung, waehrend derselbe Wert als Ueberschreibung mit |
| 884 | * Fehler gekappt worden waere. Eine Schranke, die nur auf einem von zwei |
| 885 | * Wegen gilt, ist keine. Fuer den OePNV betrifft das seit Befund A2 nur noch |
| 886 | * den Rueckfallwert ohne Vmax - im Regelfall ist vr = Vmax/3,6 und liegt |
| 887 | * damit von selbst auf der Schranke. |
| 888 | */ |
| 889 | if (ueberSchranke) { |
| 890 | notes.push({ |
| 891 | severity: 'fehler', |
| 892 | code: 'raeumgeschwindigkeit-vorgabe-zu-hoch', |
| 893 | message: |
| 894 | `Die in den Vorgaben hinterlegte Räumgeschwindigkeit ${formatZahl(roh)} m/s ` + |
| 895 | `überschreitet die zulässige Obergrenze von ${formatZahl(ceiling)} m/s und würde die ` + |
| 896 | 'Zwischenzeit unzulässig verkürzen. Angesetzt wird die Obergrenze.', |
| 897 | }); |
| 898 | } |
| 899 | |
| 900 | /* |
| 901 | * Der Deckel an der Vmax hat gegriffen (Fassung 5.22.0). Ein Hinweis und |
| 902 | * kein Fehler: Das ist der Regelfall an jeder Anlage mit Tempo 30, und das |
| 903 | * Programm tut von sich aus das Sichere. Gemeldet wird er trotzdem, weil das |
| 904 | * Kennwerteverzeichnis derselben Unterlage 10,0 m/s nennt und im Rechenweg |
| 905 | * daneben ein kleinerer Wert stuende - eine stille Ersetzung. |
| 906 | */ |
| 907 | if (ueberVmax) { |
| 908 | notes.push({ |
| 909 | severity: 'hinweis', |
| 910 | code: 'raeumgeschwindigkeit-auf-vmax-gedeckelt', |
| 911 | message: |
| 912 | `Der Regelwert der Räumgeschwindigkeit beträgt ${formatZahl(vorDeckel)} m/s. ` + |
| 913 | `Der räumende Strom darf jedoch nur ${formatZahl(clearing.vZul)} km/h fahren; angesetzt ` + |
| 914 | `werden deshalb ${formatZahl(deckel)} m/s. Ein Fahrzeug räumt nicht schneller, als es ` + |
| 915 | 'fahren darf - mit dem höheren Wert fiele die Zwischenzeit zu kurz aus.', |
| 916 | }); |
| 917 | } |
| 918 | |
| 919 | /* |
| 920 | * Fehlende Vmax beim OePNV: Der Regelfall vr = Vmax/3,6 ist nicht rechenbar. |
| 921 | * Angesetzt wird der Rueckfallwert aus den Vorgaben, durch die Schranke oben |
| 922 | * konservativ auf hoechstens 10 m/s (das bisherige Verhalten) begrenzt. Die |
| 923 | * Warnung steht NACH der Kappung, damit sie den tatsaechlich angesetzten |
| 924 | * Wert nennt. |
| 925 | */ |
| 926 | if (clearing.mode === 'oepnv' && !(clearing.vZul > 0)) { |
| 927 | notes.push({ |
| 928 | severity: 'warnung', |
| 929 | code: 'oepnv-vmax-fehlt', |
| 930 | message: |
| 931 | 'Für den räumenden ÖPNV-Strom ist keine zulässige Höchstgeschwindigkeit (Vmax) ' + |
| 932 | 'angegeben. Die Räumgeschwindigkeit wird konservativ mit dem Rückfallwert ' + |
| 933 | `${formatZahl(regular)} m/s angesetzt` + |
| 934 | (clearing.haltVorKnoten === true |
| 935 | ? ' (Anfahransatz: beschleunigt wird höchstens bis zu diesem Wert).' |
| 936 | : ', die Überfahrzeit mit der obersten Stufe der Vmax-Staffel.') + |
| 937 | ' Tragen Sie die Vmax an der Signalgruppe ein.', |
| 938 | }); |
| 939 | } |
| 940 | |
| 941 | const override = clearing.clearingSpeedOverride; |
| 942 | if (override === undefined) return regular; |
| 943 | |
| 944 | if (!gueltigeRaeumgeschwindigkeit(override)) { |
| 945 | notes.push({ |
| 946 | severity: 'fehler', |
| 947 | code: 'raeumgeschwindigkeit-ungueltig', |
| 948 | message: |
| 949 | 'Die vorgegebene Räumgeschwindigkeit ist kein gültiger Wert größer als 0. ' + |
| 950 | `Angesetzt wird der Regelwert ${formatZahl(regular)} m/s.`, |
| 951 | }); |
| 952 | return regular; |
| 953 | } |
| 954 | |
| 955 | // Eine hoehere Raeumgeschwindigkeit verkuerzt die Raeumzeit und damit die |
| 956 | // Zwischenzeit. Nach oben wird deshalb hart begrenzt; nach unten ist jeder |
| 957 | // Wert zulaessig, weil er auf der sicheren Seite liegt. |
| 958 | const angesetzt = override > ceiling ? ceiling : override; |
| 959 | if (override > ceiling) { |
| 960 | notes.push({ |
| 961 | severity: 'fehler', |
| 962 | code: 'raeumgeschwindigkeit-zu-hoch', |
| 963 | message: |
| 964 | `Die vorgegebene Räumgeschwindigkeit ${formatZahl(override)} m/s überschreitet die ` + |
| 965 | `zulässige Obergrenze von ${formatZahl(ceiling)} m/s und würde die Zwischenzeit ` + |
| 966 | 'unzulässig verkürzen. Angesetzt wird die Obergrenze.', |
| 967 | }); |
| 968 | } |
| 969 | |
| 970 | /* |
| 971 | * HANDEINGABE UEBER DEM REGELWERT DIESES STROMS (Fassung 5.9.0). |
| 972 | * |
| 973 | * Die harte Obergrenze darueber gilt je VERKEHRSART, der Regelwert dagegen |
| 974 | * je Fahrbeziehung und Merkmal. Bei Kfz geradeaus, Rad, Fussgaengern ohne |
| 975 | * Merkmal und dem OePNV fallen beide zusammen; beim abbiegenden Kfz-Strom |
| 976 | * (7,0 gegen 10,0 m/s) und auf der Furt mit erhoehtem Zeitbedarf (1,0 gegen |
| 977 | * 1,2 m/s) klaffen sie auseinander. Genau dazwischen lief eine Handeingabe |
| 978 | * ohne ein einziges Wort durch und verkuerzte die Zwischenzeit - waehrend |
| 979 | * die Hilfe zusagt, das Programm lasse keine hoehere Raeumgeschwindigkeit |
| 980 | * als den Regelwert zu, und die Nachbarstellen (Einfahrgeschwindigkeit, |
| 981 | * Ueberfahrzeit) jede Abweichung in die unsichere Richtung melden. |
| 982 | * |
| 983 | * Verglichen wird der ANGESETZTE Wert: Eine auf die Obergrenze gekappte |
| 984 | * Eingabe liegt beim Abbieger immer noch ueber dessen Regelwert. |
| 985 | * |
| 986 | * BEZUGSGROESSE IST DER REGELWERT OHNE DAS MERKMAL "ENGER INNENRADIUS" |
| 987 | * (Fassung 5.9.0): `regular` enthaelt bei angehaktem Merkmal dessen 5,0 m/s. |
| 988 | * Das ist ein Praxisansatz, den quellen.ts unter 'praxis-enger-radius' mit |
| 989 | * dem Pruefstand "abzugleichen" fuehrt und ausdruecklich nicht als |
| 990 | * Regelwerkswert; das Programm darf ihn nirgends "Regelwert" nennen. Die |
| 991 | * Meldung tat es und behauptete bei Handeingabe 9 "Regelwert 5 m/s dieses |
| 992 | * Stroms", waehrend dasselbe Fenster am Feld korrekt "ohne diesen Eintrag 5 |
| 993 | * m/s" schrieb - und der wirkliche Regelwert des Abbiegers (7,0 m/s) kam in |
| 994 | * der begruendungspflichtigen Abweichung ueberhaupt nicht vor. Gegen das |
| 995 | * Merkmal gemessen wird die Handeingabe dort, wo es hingehoert: in |
| 996 | * 'enger-radius-durch-vorgabe-ersetzt' weiter unten, das denselben |
| 997 | * Sachverhalt samt Richtung bereits meldet. Sonst stuenden zwei Warnungen zu |
| 998 | * einer Tatsache in Pruefbericht und Planunterlage. Der erhoehte Zeitbedarf |
| 999 | * bleibt in der Bezugsgroesse: seine 1,0 m/s sind ein Regelwert, kein |
| 1000 | * Praxisansatz. |
| 1001 | * |
| 1002 | * Warnung und nicht Fehler, wie bei der Einfahrgeschwindigkeit: Die RiLSA |
| 1003 | * laesst begruendete oertliche Ansaetze zu. Unzulaessig ist nur, die |
| 1004 | * Abweichung nicht auszuweisen. |
| 1005 | */ |
| 1006 | const regelOhneMerkmal = loeseRegelRaeumgeschwindigkeit( |
| 1007 | { ...clearing, engerRadius: false }, |
| 1008 | defaults, |
| 1009 | ).regular; |
| 1010 | if (angesetzt > regelOhneMerkmal) { |
| 1011 | notes.push({ |
| 1012 | severity: 'warnung', |
| 1013 | code: 'raeumgeschwindigkeit-ueber-regelwert', |
| 1014 | message: |
| 1015 | `Die angesetzte Räumgeschwindigkeit ${formatZahl(angesetzt)} m/s liegt über dem ` + |
| 1016 | `Regelwert ${formatZahl(regelOhneMerkmal)} m/s dieses Stroms und verkürzt die ` + |
| 1017 | 'Zwischenzeit. Die Abweichung ist zu begründen.', |
| 1018 | }); |
| 1019 | } |
| 1020 | |
| 1021 | /* |
| 1022 | * WELCHE REGEL GEWINNT (Fassung 5.5.0): die Handeingabe. Sie ersetzt den |
| 1023 | * Regelwert vollstaendig - auch den des Merkmals "enger Innenradius", das nur |
| 1024 | * in `regular` eingegangen und hier gerade verworfen worden ist. Das war |
| 1025 | * zuvor nirgends geschrieben, nirgends geprueft und stand in drei Ausgaben |
| 1026 | * genau umgekehrt. Jetzt steht es im Rechenweg der Beziehung, an der es |
| 1027 | * passiert. |
| 1028 | * |
| 1029 | * Warnung und nicht Fehler: Die Handeingabe ist zulaessig und kann sogar |
| 1030 | * schaerfer sein als das Merkmal. Unzulaessig ist nur, beides gleichzeitig |
| 1031 | * zu fuehren und den Ansatz der Unterlage nicht mehr entnehmen zu koennen. |
| 1032 | */ |
| 1033 | if ( |
| 1034 | clearing.mode === 'kfz' && |
| 1035 | clearing.engerRadius === true && |
| 1036 | (clearing.movement === 'links' || clearing.movement === 'rechts') |
| 1037 | ) { |
| 1038 | notes.push({ |
| 1039 | severity: 'warnung', |
| 1040 | code: 'enger-radius-durch-vorgabe-ersetzt', |
| 1041 | message: |
| 1042 | 'An dieser Beziehung sind der enge Innenradius UND eine Räumgeschwindigkeit von Hand ' + |
| 1043 | `gesetzt. Die Handeingabe hat Vorrang: Geräumt wird mit ${formatZahl(angesetzt)} m/s, ` + |
| 1044 | `nicht mit den ${formatZahl(regular)} m/s des Merkmals. ` + |
| 1045 | (angesetzt > regular |
| 1046 | ? 'Die Zwischenzeit fällt damit KÜRZER aus als mit dem Merkmal allein. ' |
| 1047 | : '') + |
| 1048 | 'Entfernen Sie eines von beiden, damit der Ansatz eindeutig bleibt.', |
| 1049 | }); |
| 1050 | } |
| 1051 | return angesetzt; |
| 1052 | } |
| 1053 | |
| 1054 | function resolveEnteringSpeed( |
| 1055 | entering: EnteringStream, |
| 1056 | defaults: RilsaDefaults, |
| 1057 | notes: CalculationNote[], |
| 1058 | ): MetersPerSecond { |
| 1059 | const regular = defaults.enteringSpeed[entering.mode]; |
| 1060 | const override = entering.enteringSpeedOverride; |
| 1061 | if (override === undefined) return regular; |
| 1062 | |
| 1063 | if (!isFiniteNumber(override) || override <= 0) { |
| 1064 | notes.push({ |
| 1065 | severity: 'fehler', |
| 1066 | code: 'einfahrgeschwindigkeit-ungueltig', |
| 1067 | message: |
| 1068 | 'Die vorgegebene Einfahrgeschwindigkeit ist kein gültiger Wert größer als 0. ' + |
| 1069 | `Angesetzt wird der Regelwert ${formatZahl(regular)} m/s.`, |
| 1070 | }); |
| 1071 | return regular; |
| 1072 | } |
| 1073 | |
| 1074 | // Eine niedrigere Einfahrgeschwindigkeit verlaengert die Einfahrzeit und |
| 1075 | // verkuerzt dadurch die Zwischenzeit - das ist die unsichere Richtung. |
| 1076 | if (override < regular) { |
| 1077 | notes.push({ |
| 1078 | severity: 'warnung', |
| 1079 | code: 'einfahrgeschwindigkeit-unter-regelwert', |
| 1080 | message: |
| 1081 | `Die vorgegebene Einfahrgeschwindigkeit ${formatZahl(override)} m/s liegt unter dem ` + |
| 1082 | `Regelwert ${formatZahl(regular)} m/s und verkürzt die Zwischenzeit. ` + |
| 1083 | 'Die Abweichung ist zu begründen.', |
| 1084 | }); |
| 1085 | } |
| 1086 | return override; |
| 1087 | } |
| 1088 | |
| 1089 | function resolveCrossingTime( |
| 1090 | clearing: ClearingStream, |
| 1091 | defaults: RilsaDefaults, |
| 1092 | notes: CalculationNote[], |
| 1093 | ): Seconds { |
| 1094 | const regular = regelUeberfahrzeit(clearing, defaults); |
| 1095 | const override = clearing.crossingTimeOverride; |
| 1096 | if (override === undefined) return regular; |
| 1097 | |
| 1098 | if (!gueltigeUeberfahrzeit(override)) { |
| 1099 | notes.push({ |
| 1100 | severity: 'fehler', |
| 1101 | code: 'ueberfahrzeit-ungueltig', |
| 1102 | message: 'Die vorgegebene Überfahrzeit ist kein gültiger Wert. Angesetzt wird der Regelwert.', |
| 1103 | }); |
| 1104 | return regular; |
| 1105 | } |
| 1106 | |
| 1107 | /* |
| 1108 | * Harte Schranke fuer den Kraftfahrzeugverkehr. |
| 1109 | * |
| 1110 | * Fuer Kfz IST die Ueberfahrzeit die Gelbzeit. Sie ist kein Planungsspielraum, |
| 1111 | * sondern der Wert, den der Signalgeber tatsaechlich zeigt - ein kuerzerer |
| 1112 | * Ansatz rechnet mit einer Gelbzeit, die es nicht gibt. Massgebend ist die |
| 1113 | * Gelbzeit DIESER Signalgruppe: bei 50 km/h 3 s, bei 60 km/h 4 s, bei 70 km/h |
| 1114 | * 5 s. Gegen SAFETY_FLOORS.yellowKfz allein zu pruefen waere zu wenig - bei |
| 1115 | * 70 km/h liesse eine Kappung auf 3 s die Zwischenzeit weiterhin um 2 s zu |
| 1116 | * kurz ausfallen, und der Anwender koennte den Fehler durch Eintragen genau |
| 1117 | * dieser 3 s in eine blosse Warnung verwandeln, ohne dass sich am Ergebnis |
| 1118 | * etwas aendert. |
| 1119 | * |
| 1120 | * Zuvor wurde jeder nichtnegative Vorgabewert ungeprueft uebernommen: Eine |
| 1121 | * eingetragene 0 senkte die Zwischenzeit von 6 s auf 3 s, ohne dass |
| 1122 | * irgendeine Meldung erschien. |
| 1123 | * |
| 1124 | * BEIM ANSATZ 'fest' GILT DIESELBE HAERTE, ABER NICHT DIESELBE BEGRUENDUNG: |
| 1125 | * Massgebend ist dann nicht die Gelbzeit, sondern der feste Wert des |
| 1126 | * Ansatzes. Die Meldung muss das sagen - eine Fehlermeldung, die bei |
| 1127 | * gewaehltem Ansatz 'fest' und 70 km/h von "der Gelbzeit von 3 s" spraeche, |
| 1128 | * waere schlicht falsch. Fehler bleibt es, weil sonst ein Wechsel des |
| 1129 | * Ansatzes zur Abkuerzung taugte: Wer auf 'fest' umschaltet und dann 0 s |
| 1130 | * eintraegt, saehe nur noch eine Warnung. |
| 1131 | * |
| 1132 | * KORREKTUR (Fassung 5.5.0): Die Klammer nannte fest eingeschriebene "3 s |
| 1133 | * geradeaus, 2 s abbiegend". Beide Werte sind Vorgabenfelder (1 bis 10 s). |
| 1134 | * Wer 5 s eintrug, bekam "unterschreitet den Regelwert von 5 s des gewaehlten |
| 1135 | * Ansatzes (3 s geradeaus, 2 s abbiegend)" - einen Satz, der sich selbst |
| 1136 | * widerspricht. Dieselbe Regel wie Befund C21, jetzt auch hier: keine Zahl |
| 1137 | * aus dem Satz. |
| 1138 | */ |
| 1139 | if (clearing.mode === 'kfz' && override < regular) { |
| 1140 | const fest = clearing.ueberfahrzeitAnsatz === 'fest'; |
| 1141 | notes.push({ |
| 1142 | severity: 'fehler', |
| 1143 | code: 'ueberfahrzeit-unter-gelbzeitschranke', |
| 1144 | message: |
| 1145 | `Die vorgegebene Überfahrzeit ${formatZahl(override)} s unterschreitet ` + |
| 1146 | (fest |
| 1147 | ? `den Regelwert von ${formatZahl(regular)} s des gewählten Ansatzes „feste ` + |
| 1148 | `Überfahrzeit" (${formatZahl(defaults.crossingTime.kfzGeradeaus)} s geradeaus, ` + |
| 1149 | `${formatZahl(defaults.crossingTime.kfzAbbiegend)} s abbiegend). Ein kürzerer Wert ` + |
| 1150 | 'verkürzt die Zwischenzeit unzulässig. Angesetzt wird der Regelwert.' |
| 1151 | : `die Gelbzeit von ${formatZahl(regular)} s, die bei ` + |
| 1152 | `${formatZahl(clearing.vZul)} km/h zu zeigen ist. Für den Kraftfahrzeugverkehr ist ` + |
| 1153 | 'die Überfahrzeit die Gelbzeit; ein kürzerer Wert verkürzt die Zwischenzeit ' + |
| 1154 | 'unzulässig. Angesetzt wird die Gelbzeit.'), |
| 1155 | }); |
| 1156 | return regular; |
| 1157 | } |
| 1158 | |
| 1159 | // Uebrige Verkehrsarten: Fuer Fussgaenger und Sondersignale gibt es keine |
| 1160 | // Gelbzeit, der Regelwert ist eine Erfahrungsgroesse bzw. beim OePNV die |
| 1161 | // Vmax-Staffel. Ein kuerzerer Ansatz ist dort begruendungspflichtig, aber |
| 1162 | // nicht unzulaessig - deshalb Warnung statt Fehler. Fuer den OePNV nennt die |
| 1163 | // RiLSA selbst einen zulaessigen Grund (Fall 3): "Wird die Freigabezeit |
| 1164 | // durch eine Abmeldung nach Passieren des Freigabesignals beendet, kann die |
| 1165 | // Ueberfahrzeit bei der Berechnung der Zwischenzeit auf Null gesetzt |
| 1166 | // werden." |
| 1167 | if (override < regular) { |
| 1168 | notes.push({ |
| 1169 | severity: 'warnung', |
| 1170 | code: 'ueberfahrzeit-unter-regelwert', |
| 1171 | message: |
| 1172 | `Die vorgegebene Überfahrzeit ${formatZahl(override)} s liegt unter dem Regelwert ` + |
| 1173 | `${formatZahl(regular)} s und verkürzt die Zwischenzeit. Die Abweichung ist zu begründen.`, |
| 1174 | }); |
| 1175 | } |
| 1176 | return override; |
| 1177 | } |
| 1178 | |
| 1179 | /** |
| 1180 | * Die Merkmale eines raeumenden Stroms, aus denen sich seine REGELWERT- |
| 1181 | * Ueberfahrzeit ergibt - ohne Handeingabe. |
| 1182 | */ |
| 1183 | export type Ueberfahrzeitkennung = Pick<ClearingStream, 'mode' | 'movement' | 'vZul'> & |
| 1184 | Partial<Pick<ClearingStream, 'haltVorKnoten' | 'ueberfahrzeitAnsatz'>>; |
| 1185 | |
| 1186 | /** |
| 1187 | * Ueberfahrzeit ohne Vorgabe des Anwenders. |
| 1188 | * |
| 1189 | * Oeffentlich seit der Fassung 5.9.0, denn das Konfliktfenster |
| 1190 | * beschriftete das Eingabefeld mit "Regelwert" und nannte dabei den |
| 1191 | * ANGESETZTEN Wert - bei gesetzter Handeingabe also die Eingabe selbst. Es |
| 1192 | * holt den Regelwert jetzt hier, statt ihn nachzubauen. |
| 1193 | */ |
| 1194 | export function regelUeberfahrzeit( |
| 1195 | clearing: Ueberfahrzeitkennung, |
| 1196 | defaults: RilsaDefaults = RILSA_DEFAULTS, |
| 1197 | ): Seconds { |
| 1198 | switch (clearing.mode) { |
| 1199 | case 'kfz': |
| 1200 | return kfzUeberfahrzeit(clearing, defaults); |
| 1201 | case 'rad': |
| 1202 | return defaults.crossingTime.radDefault; |
| 1203 | case 'fuss': |
| 1204 | return defaults.crossingTime.fuss; |
| 1205 | case 'oepnv': |
| 1206 | // Haltefall (RiLSA 2015, Fall 4): "Ueberfahrzeit: tue = 0 s" - das |
| 1207 | // Fahrzeug steht am Ende der Freigabezeit, es faehrt nichts mehr ueber |
| 1208 | // die Haltlinie. Sonst gilt die Vmax-Staffel (Fall 3). |
| 1209 | // KORREKTUR (Fassung 5.4.0, Befund A2): Zuvor stand hier fuer |
| 1210 | // JEDEN OePNV-Strom fest 0 s. |
| 1211 | if (clearing.haltVorKnoten === true) return 0; |
| 1212 | return oepnvUeberfahrzeit(clearing.vZul); |
| 1213 | } |
| 1214 | } |
| 1215 | |
| 1216 | /** |
| 1217 | * Ueberfahrzeit des Kraftfahrzeugverkehrs nach dem gewaehlten Ansatz |
| 1218 | * (seit Fassung 5.5.0). |
| 1219 | * |
| 1220 | * 'gelbzeit' (Vorgabe): tue ist die Gelbzeit DIESER Signalgruppe - 3 s bei |
| 1221 | * 50 km/h, 4 s bei 60 km/h, 5 s darueber. |
| 1222 | * 'fest': tue = 3 s geradeaus, 2 s abbiegend, unabhaengig von der |
| 1223 | * zulaessigen Hoechstgeschwindigkeit. |
| 1224 | * |
| 1225 | * Fehlt der Ansatz am Strom - jeder Aufrufer, der ihn nicht durchreicht, und |
| 1226 | * jede Datei bis Schema 11 -, gilt 'gelbzeit'. Das ist der bisherige Stand und |
| 1227 | * die laengere, also sichere Zwischenzeit; ein fehlendes Feld darf nie die |
| 1228 | * kuerzere Rechnung ausloesen. |
| 1229 | */ |
| 1230 | function kfzUeberfahrzeit(clearing: Ueberfahrzeitkennung, defaults: RilsaDefaults): Seconds { |
| 1231 | if (clearing.ueberfahrzeitAnsatz !== 'fest') return yellowTime('kfz', clearing.vZul, defaults); |
| 1232 | return clearing.movement === 'links' || clearing.movement === 'rechts' |
| 1233 | ? defaults.crossingTime.kfzAbbiegend |
| 1234 | : defaults.crossingTime.kfzGeradeaus; |
| 1235 | } |
| 1236 | |
| 1237 | /** |
| 1238 | * Ueberfahrzeit-Staffel des OePNV nach RiLSA 2015, Fall 3 (3/5/7 s nach der |
| 1239 | * zulaessigen Hoechstgeschwindigkeit). Ohne Vmax und oberhalb des |
| 1240 | * Staffelendes von 70 km/h gilt die oberste Stufe - die laengere |
| 1241 | * Ueberfahrzeit verlaengert die Zwischenzeit und liegt auf der sicheren |
| 1242 | * Seite; die zugehoerige Meldung setzt resolveClearingSpeed ab. |
| 1243 | */ |
| 1244 | function oepnvUeberfahrzeit(vZul: KilometersPerHour): Seconds { |
| 1245 | const oberste = OEPNV_UEBERFAHRZEIT_STAFFEL.at(-1)?.crossing ?? 7; |
| 1246 | if (!(vZul > 0)) return oberste; |
| 1247 | for (const stufe of OEPNV_UEBERFAHRZEIT_STAFFEL) { |
| 1248 | if (vZul <= stufe.upToKmh) return stufe.crossing; |
| 1249 | } |
| 1250 | return oberste; |
| 1251 | } |
| 1252 | |
| 1253 | /** Gilt fuer diesen Strom der Anfahransatz nach RiLSA-Fall 4? */ |
| 1254 | function istOepnvHaltefall(clearing: ClearingStream): boolean { |
| 1255 | return clearing.mode === 'oepnv' && clearing.haltVorKnoten === true; |
| 1256 | } |
| 1257 | |
| 1258 | /** |
| 1259 | * Anfahrbeschleunigung je Fahrzeugart (RiLSA 2015, Fall 4). |
| 1260 | * Belegt sind Strassenbahn (1,0 m/s2) und Linienbus (1,2 m/s2); fuer jede |
| 1261 | * andere Fahrzeugart eines OePNV-Stroms gilt der kleinere Wert, weil er die |
| 1262 | * laengere Anfahrzeit ergibt. |
| 1263 | */ |
| 1264 | function anfahrbeschleunigungFuer(vehicleClass: VehicleClass): number { |
| 1265 | return vehicleClass === 'bus' |
| 1266 | ? OEPNV_ANFAHRBESCHLEUNIGUNG.bus |
| 1267 | : OEPNV_ANFAHRBESCHLEUNIGUNG.strassenbahn; |
| 1268 | } |
| 1269 | |
| 1270 | /** |
| 1271 | * Raeumzeit beim Anfahren aus dem Stand (RiLSA 2015, Fall 4). |
| 1272 | * |
| 1273 | * `vGrenz` ist die Geschwindigkeit, auf die hoechstens beschleunigt wird - |
| 1274 | * Vmax/3,6 des Stroms bzw. der konservative Rueckfallwert. Solange der |
| 1275 | * Beschleunigungsweg vGrenz^2/(2a) nicht erreicht ist, gilt die reine |
| 1276 | * Anfahrformel sqrt(2*sr/a); danach wird der Restweg mit vGrenz gefahren. |
| 1277 | * Beide Zweige stossen am Grenzweg stetig aneinander. |
| 1278 | */ |
| 1279 | function anfahrRaeumzeit(sr: Meters, a: number, vGrenz: MetersPerSecond): Seconds { |
| 1280 | const beschleunigungsweg = (vGrenz * vGrenz) / (2 * a); |
| 1281 | if (sr <= beschleunigungsweg) return Math.sqrt((2 * sr) / a); |
| 1282 | return vGrenz / a + (sr - beschleunigungsweg) / vGrenz; |
| 1283 | } |
| 1284 | |
| 1285 | function requireNonNegative( |
| 1286 | value: number, |
| 1287 | code: string, |
| 1288 | label: string, |
| 1289 | notes: CalculationNote[], |
| 1290 | ): number { |
| 1291 | if (!isFiniteNumber(value)) { |
| 1292 | notes.push({ |
| 1293 | severity: 'fehler', |
| 1294 | code: `${code}-fehlt`, |
| 1295 | message: `${label} ist nicht angegeben. Angesetzt wird 0 m; das Ergebnis ist nicht verwendbar.`, |
| 1296 | }); |
| 1297 | return 0; |
| 1298 | } |
| 1299 | if (value < 0) { |
| 1300 | notes.push({ |
| 1301 | severity: 'fehler', |
| 1302 | code: `${code}-negativ`, |
| 1303 | message: `${label} ist mit ${formatZahl(value)} m negativ. Angesetzt wird der Betrag.`, |
| 1304 | }); |
| 1305 | return Math.abs(value); |
| 1306 | } |
| 1307 | return value; |
| 1308 | } |
| 1309 | |
| 1310 | /** |
| 1311 | * Zahl mit Dezimalkomma und hoechstens zwei Nachkommastellen - fuer Sekunden, |
| 1312 | * Meter, m/s und km/h gleichermassen; die Einheit setzt der Aufrufer. Hiess vor |
| 1313 | * der Fassung 5.4.0 formatSeconds und formatierte doch ueberwiegend |
| 1314 | * Geschwindigkeiten und Laengen. |
| 1315 | */ |
| 1316 | function formatZahl(value: number): string { |
| 1317 | return roundTo(value, 2).toString().replace('.', ','); |
| 1318 | } |
| 1319 | |
| 1320 | /** Eine Konfliktbeziehung zwischen zwei Signalgruppen. */ |
| 1321 | export interface IntergreenPair { |
| 1322 | readonly fromId: string; |
| 1323 | readonly toId: string; |
| 1324 | readonly clearing: ClearingStream; |
| 1325 | readonly entering: EnteringStream; |
| 1326 | } |
| 1327 | |
| 1328 | /** Zwischenzeitenmatrix als Zuordnung "fromId -> toId -> Ergebnis". */ |
| 1329 | export type IntergreenMatrix = ReadonlyMap<string, ReadonlyMap<string, IntergreenResult>>; |
| 1330 | |
| 1331 | /** |
| 1332 | * Berechnet die vollstaendige Zwischenzeitenmatrix. |
| 1333 | * Nicht konfliktfreie Beziehungen fehlen in der Matrix; sie sind nicht 0. |
| 1334 | * Diese Unterscheidung ging im Altbestand verloren, weil dort eine dichte |
| 1335 | * Zahlenmatrix mit 0-Eintraegen gefuehrt wurde und "kein Konflikt" nicht von |
| 1336 | * "Zwischenzeit 0 s" zu unterscheiden war. |
| 1337 | */ |
| 1338 | export function computeIntergreenMatrix( |
| 1339 | pairs: readonly IntergreenPair[], |
| 1340 | defaults: RilsaDefaults = RILSA_DEFAULTS, |
| 1341 | ): IntergreenMatrix { |
| 1342 | const matrix = new Map<string, Map<string, IntergreenResult>>(); |
| 1343 | for (const pair of pairs) { |
| 1344 | if (pair.fromId === pair.toId) continue; |
| 1345 | let row = matrix.get(pair.fromId); |
| 1346 | if (!row) { |
| 1347 | row = new Map<string, IntergreenResult>(); |
| 1348 | matrix.set(pair.fromId, row); |
| 1349 | } |
| 1350 | row.set(pair.toId, computeIntergreen(pair.clearing, pair.entering, defaults)); |
| 1351 | } |
| 1352 | return matrix; |
| 1353 | } |
| 1354 | |
| 1355 | /** Liest einen Wert aus der Matrix; `undefined` bedeutet "kein Konflikt". */ |
| 1356 | export function lookupIntergreen( |
| 1357 | matrix: IntergreenMatrix, |
| 1358 | fromId: string, |
| 1359 | toId: string, |
| 1360 | ): IntergreenResult | undefined { |
| 1361 | return matrix.get(fromId)?.get(toId); |
| 1362 | } |