lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
| 1 | /** |
| 2 | * Einheiten und Umrechnungen. |
| 3 | * |
| 4 | * Im Altbestand wurden Geschwindigkeiten mal in km/h und mal in m/s in denselben |
| 5 | * Feldern gefuehrt; die Umrechnung /3.6 wurde teils doppelt, teils gar nicht |
| 6 | * angewendet. Deshalb sind hier alle Einheiten im Typnamen sichtbar und es gibt |
| 7 | * genau eine Stelle fuer jede Umrechnung. |
| 8 | * |
| 9 | * Rechnerisch gilt projektweit: intern wird ausschliesslich in SI-Einheiten |
| 10 | * gerechnet (Meter, Sekunden, Meter je Sekunde). km/h existiert nur an der |
| 11 | * Benutzerschnittstelle. |
| 12 | */ |
| 13 | |
| 14 | /** Zeit in Sekunden. */ |
| 15 | export type Seconds = number; |
| 16 | /** Laenge in Metern. */ |
| 17 | export type Meters = number; |
| 18 | /** Geschwindigkeit in Metern je Sekunde (interne Rechengroesse). */ |
| 19 | export type MetersPerSecond = number; |
| 20 | /** Geschwindigkeit in Kilometern je Stunde (nur Ein-/Ausgabe). */ |
| 21 | export type KilometersPerHour = number; |
| 22 | /** Verkehrsstaerke in Fahrzeugen je Stunde. */ |
| 23 | export type VehiclesPerHour = number; |
| 24 | /** Dimensionsloser Anteil im Bereich 0..1. */ |
| 25 | export type Ratio = number; |
| 26 | |
| 27 | const SECONDS_PER_HOUR = 3600; |
| 28 | const METERS_PER_KILOMETER = 1000; |
| 29 | /** Exakter Umrechnungsfaktor km/h -> m/s (= 1000/3600). */ |
| 30 | export const KMH_TO_MS = METERS_PER_KILOMETER / SECONDS_PER_HOUR; |
| 31 | |
| 32 | /** Rechnet km/h in m/s um. */ |
| 33 | export function kmhToMs(v: KilometersPerHour): MetersPerSecond { |
| 34 | return v * KMH_TO_MS; |
| 35 | } |
| 36 | |
| 37 | /** Rechnet m/s in km/h um. */ |
| 38 | export function msToKmh(v: MetersPerSecond): KilometersPerHour { |
| 39 | return v / KMH_TO_MS; |
| 40 | } |
| 41 | |
| 42 | /** |
| 43 | * Rundet auf ganze Sekunden auf. |
| 44 | * |
| 45 | * Sicherheitsrelevante Zeiten (Zwischenzeit, Raeumzeit) duerfen nie abgerundet |
| 46 | * werden. Der Rundungsfehler-Ausgleich faengt Gleitkomma-Artefakte ab: ohne ihn |
| 47 | * wuerde z. B. 3.0000000000000004 auf 4 s aufgerundet. |
| 48 | * |
| 49 | * KEINE NEGATIVE NULL (Fassung 5.10.0): Math.ceil gibt fuer jeden Wert zwischen |
| 50 | * -1 und 0 die negative Null zurueck. Sie rechnet wie 0, druckt sich aber nicht |
| 51 | * wie 0 - Intl formatiert sie als "-0", und so stand sie in der |
| 52 | * Zwischenzeitenmatrix der Planunterlage, im Rechenweg des Konfliktfensters und |
| 53 | * in der CSV-Ausgabe. Abgestreift wird sie hier, an der einen Stelle, die |
| 54 | * aufrundet, statt an jeder Ausgabe einzeln. Die Nachbarzeile roundTo faengt |
| 55 | * daneben den Vorzeichenfall von Math.round ab, streift die negative Null aber |
| 56 | * NICHT ab: roundTo(-0,4; 0) ergibt -0. Wer mit 0 Nachkommastellen rundet, muss |
| 57 | * sie dort weiter selbst erwarten. |
| 58 | */ |
| 59 | export function ceilSeconds(t: Seconds): Seconds { |
| 60 | const corrected = roundTo(t, 9); |
| 61 | const aufgerundet = Math.ceil(corrected); |
| 62 | return aufgerundet === 0 ? 0 : aufgerundet; |
| 63 | } |
| 64 | |
| 65 | /** Kaufmaennisches Runden auf eine feste Anzahl Nachkommastellen. */ |
| 66 | export function roundTo(value: number, decimals: number): number { |
| 67 | if (!Number.isFinite(value)) return value; |
| 68 | const factor = 10 ** decimals; |
| 69 | // Math.round(-0.5) ergibt -0; die Vorzeichenbehandlung haelt negative Werte stabil. |
| 70 | return Math.sign(value) * (Math.round(Math.abs(value) * factor) / factor); |
| 71 | } |
| 72 | |
| 73 | /** Rundet auf das naechste Vielfache von `step` (kaufmaennisch). */ |
| 74 | export function roundToStep(value: number, step: number): number { |
| 75 | if (step <= 0) throw new RangeError('roundToStep: step muss groesser als 0 sein'); |
| 76 | return Math.round(value / step) * step; |
| 77 | } |
| 78 | |
| 79 | /** Rundet auf das naechste Vielfache von `step` auf. */ |
| 80 | export function ceilToStep(value: number, step: number): number { |
| 81 | if (step <= 0) throw new RangeError('ceilToStep: step muss groesser als 0 sein'); |
| 82 | return Math.ceil(roundTo(value / step, 9)) * step; |
| 83 | } |
| 84 | |
| 85 | /** Begrenzt einen Wert auf das Intervall [min, max]. */ |
| 86 | export function clamp(value: number, min: number, max: number): number { |
| 87 | if (min > max) throw new RangeError('clamp: min darf nicht groesser als max sein'); |
| 88 | return Math.min(max, Math.max(min, value)); |
| 89 | } |
| 90 | |
| 91 | /** |
| 92 | * Prueft auf eine endliche Zahl. |
| 93 | * |
| 94 | * Der Altbestand hat mit parseFloat('') === NaN weitergerechnet; NaN pflanzt |
| 95 | * sich durch die gesamte Berechnung fort und erscheint am Ende stumm als leeres |
| 96 | * Feld im Plan. Alle Eingaenge der Fachlogik werden deshalb hier geprueft. |
| 97 | */ |
| 98 | export function isFiniteNumber(value: unknown): value is number { |
| 99 | return typeof value === 'number' && Number.isFinite(value); |
| 100 | } |
| 101 | |
| 102 | /** |
| 103 | * Liest eine Zahl aus beliebiger Eingabe (Formularfeld, importiertes JSON). |
| 104 | * Akzeptiert deutsches Dezimalkomma. Gibt `undefined` zurueck, wenn kein |
| 105 | * gueltiger Zahlenwert vorliegt - nie stillschweigend 0. |
| 106 | */ |
| 107 | export function parseNumber(input: unknown): number | undefined { |
| 108 | if (typeof input === 'number') return Number.isFinite(input) ? input : undefined; |
| 109 | if (typeof input !== 'string') return undefined; |
| 110 | const normalized = input.trim().replace(/\s/g, '').replace(',', '.'); |
| 111 | if (normalized === '') return undefined; |
| 112 | // Number() ist strenger als parseFloat: '12abc' ergibt NaN statt 12. |
| 113 | const value = Number(normalized); |
| 114 | return Number.isFinite(value) ? value : undefined; |
| 115 | } |