lsa-planer

LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.

/ src domain rilsa intergreen.ts

57,7 KB Rohdatei
src/domain/rilsa/intergreen.ts — 1362 Zeilen
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 }