lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
| 1 | import { readFileSync, writeFileSync } from 'node:fs'; |
| 2 | |
| 3 | /** |
| 4 | * Schriftgroesse des Anwendungsfensters - Stufen, Pruefung und Sicherung. |
| 5 | * |
| 6 | * Fassung 5.7.0, Befund L11 (EN 301 549 Kap. 11.7, "Nutzereinstellungen"): |
| 7 | * Vergroessern war bis hierher ausschliesslich ueber die Chromium-Vorgabe |
| 8 | * Strg+Plus moeglich, und die eingestellte Stufe hielt genau eine Sitzung. Sie |
| 9 | * war nach jedem Programmstart, nach jedem "Neu laden" und nach jedem Neustart |
| 10 | * aus einem Absturz heraus (main.ts, `mainWindow.reload()`) wieder auf 100 %. |
| 11 | * Wer 175 % braucht - in Behoerden die Kerngruppe -, stellte sie also mehrmals |
| 12 | * taeglich neu ein. WCAG 1.4.4 ist davon nicht beruehrt: Die Vergroesserung war |
| 13 | * moeglich. 11.7 verlangt darueber hinaus, dass eine getroffene Einstellung |
| 14 | * erhalten bleibt. |
| 15 | * |
| 16 | * WARUM DIE STUFE IM HAUPTPROZESS LIEGT UND NICHT IN `AppSettings` |
| 17 | * |
| 18 | * Die Programmeinstellungen des Anzeigeprozesses (Erscheinungsbild, |
| 19 | * Selbstsicherung, Sprache, Fundstellen) liegen im localStorage und werden ueber |
| 20 | * `loadAppSettings()` gelesen - der Hauptprozess kommt dort nicht heran. Die |
| 21 | * Vergroesserungsstufe muss aber schon feststehen, BEVOR das Fenster entsteht: |
| 22 | * Sie geht als `webPreferences.zoomFactor` in `new BrowserWindow(...)` ein, |
| 23 | * damit die erste Darstellung bereits in der richtigen Groesse erscheint statt |
| 24 | * sichtbar von 100 % auf 175 % zu springen. Sie ist ausserdem keine Eigenschaft |
| 25 | * des Dokuments, sondern des Fensters - wie Fenstergroesse und Fensterlage, die |
| 26 | * kuenftig in dieselbe Datei gehoeren. |
| 27 | * |
| 28 | * Die Art der Sicherung folgt bewusst `loadAppSettings()`: fester Satz zulaessiger |
| 29 | * Werte, jede Abweichung faellt auf die Vorgabe zurueck, ein Lesefehler bleibt |
| 30 | * folgenlos. Fremde Schluessel der Datei bleiben beim Schreiben erhalten, damit |
| 31 | * eine spaetere Fenstereinstellung nicht die hier gesicherte ueberschreibt. |
| 32 | */ |
| 33 | |
| 34 | /** |
| 35 | * Waehlbare Stufen in Prozent. |
| 36 | * |
| 37 | * 200 % ist die von WCAG 1.4.4 geforderte Vergroesserung, 250 % die Reserve fuer |
| 38 | * Anwender mit staerkerer Sehbehinderung; darunter bleiben 75 % und 90 % |
| 39 | * erhalten, weil Chromiums "Verkleinern" bisher unter 100 % gehen konnte und |
| 40 | * dieser Weg nicht verloren gehen darf. |
| 41 | */ |
| 42 | export const SCHRIFT_STUFEN: readonly number[] = [75, 90, 100, 125, 150, 175, 200, 250]; |
| 43 | |
| 44 | export const SCHRIFT_STANDARD = 100; |
| 45 | |
| 46 | /** Name der Datei im Benutzerdatenverzeichnis. */ |
| 47 | export const ANSICHT_DATEINAME = 'ansicht.json'; |
| 48 | |
| 49 | /** Schluessel in der Datei. */ |
| 50 | const SCHLUESSEL = 'schriftgroesseProzent'; |
| 51 | |
| 52 | /** |
| 53 | * Naechstgelegene zulaessige Stufe. |
| 54 | * |
| 55 | * Alles, was keine Zahl ist, wird zur Vorgabe. Ein von Hand eingetragener oder |
| 56 | * ueber Strg+Mausrad entstandener Zwischenwert wird auf die naechste Stufe |
| 57 | * gerundet - so bleibt es bei genau einer Buchfuehrung ueber die Groesse. |
| 58 | */ |
| 59 | export function gueltigeStufe(wert: unknown): number { |
| 60 | if (typeof wert !== 'number' || !Number.isFinite(wert)) return SCHRIFT_STANDARD; |
| 61 | return SCHRIFT_STUFEN[stufenIndex(wert)] ?? SCHRIFT_STANDARD; |
| 62 | } |
| 63 | |
| 64 | /** Eine Stufe groesser (`1`) oder kleiner (`-1`); an den Enden bleibt es stehen. */ |
| 65 | export function nachbarstufe(aktuell: number, richtung: 1 | -1): number { |
| 66 | const ziel = stufenIndex(aktuell) + richtung; |
| 67 | const begrenzt = Math.min(SCHRIFT_STUFEN.length - 1, Math.max(0, ziel)); |
| 68 | return SCHRIFT_STUFEN[begrenzt] ?? SCHRIFT_STANDARD; |
| 69 | } |
| 70 | |
| 71 | function stufenIndex(prozent: number): number { |
| 72 | let beste = 0; |
| 73 | let abstand = Number.POSITIVE_INFINITY; |
| 74 | for (let i = 0; i < SCHRIFT_STUFEN.length; i += 1) { |
| 75 | const stufe = SCHRIFT_STUFEN[i]; |
| 76 | if (stufe === undefined) continue; |
| 77 | const naehe = Math.abs(stufe - prozent); |
| 78 | if (naehe < abstand) { |
| 79 | abstand = naehe; |
| 80 | beste = i; |
| 81 | } |
| 82 | } |
| 83 | return beste; |
| 84 | } |
| 85 | |
| 86 | /** Liest die gesicherte Stufe. Schlaegt niemals fehl. */ |
| 87 | export function liesSchriftgroesse(datei: string): number { |
| 88 | return gueltigeStufe(leseDatei(datei)[SCHLUESSEL]); |
| 89 | } |
| 90 | |
| 91 | /** |
| 92 | * Sichert die Stufe. Schlaegt niemals fehl - eine Ansichtseinstellung ist |
| 93 | * nichts, wofuer die Anwendung stehenbleiben darf. |
| 94 | * |
| 95 | * Zurueck kommt, ob wirklich geschrieben wurde. Der Anwender erfaehrt von einem |
| 96 | * Fehlschlag nichts, die Buchfuehrung in `schriftsicherung` aber schon: Ein |
| 97 | * Fehlschlag, der als Erfolg verbucht wird, kostet die Einstellung fuer die |
| 98 | * ganze Sitzung (Befund 11). |
| 99 | */ |
| 100 | export function schreibeSchriftgroesse(datei: string, prozent: number): boolean { |
| 101 | const inhalt = { ...leseDatei(datei), [SCHLUESSEL]: gueltigeStufe(prozent) }; |
| 102 | try { |
| 103 | writeFileSync(datei, `${JSON.stringify(inhalt, null, 2)}\n`, 'utf8'); |
| 104 | return true; |
| 105 | } catch { |
| 106 | /* Kein Schreibrecht, Datentraeger voll: Die Anwendung laeuft ohne Sicherung weiter. */ |
| 107 | return false; |
| 108 | } |
| 109 | } |
| 110 | |
| 111 | /** |
| 112 | * Verzoegerte Sicherung - eine Schreibung je Bedienvorgang, nicht je Raste. |
| 113 | * |
| 114 | * BEFUND (Fassung 5.7.0): `zoom-changed` feuert bei jeder Raste von |
| 115 | * Strg+Mausrad, und jede Raste lief bis hierher in ein `writeFileSync` im |
| 116 | * HAUPTPROZESS - also in genau den Prozess, der die Fenster bedient. Wer das |
| 117 | * Rad einmal durchdreht, loeste ein Dutzend Schreibungen aus; auf einem |
| 118 | * servergespeicherten Benutzerprofil, in Behoerden der Regelfall, blockiert |
| 119 | * jede davon die Oberflaeche. Geschrieben wurde ausserdem auch dann, wenn sich |
| 120 | * gar nichts geaendert hatte: An beiden Enden der Stufenliste gibt |
| 121 | * `nachbarstufe` denselben Wert zurueck. |
| 122 | * |
| 123 | * Zwei Massnahmen, beide hier, damit jeder Weg zur Stufe sie bekommt: |
| 124 | * |
| 125 | * - `plane()` schreibt nur, wenn der Wert vom zuletzt gesicherten abweicht. |
| 126 | * - Es wartet die Ruhezeit ab. Wer weiterdreht, verschiebt sie; erst wenn er |
| 127 | * aufhoert, geht eine einzige Schreibung heraus. |
| 128 | * |
| 129 | * `jetzt()` fuer den Programmschluss: Eine Einstellung, die eine halbe Sekunde |
| 130 | * vor dem Beenden getroffen wurde, darf nicht verlorengehen. |
| 131 | * |
| 132 | * Der zuletzt gesicherte Wert wird gemerkt und nicht aus der Datei gelesen - |
| 133 | * ein Lesevorgang je Raste waere derselbe Fehler mit umgekehrtem Vorzeichen. |
| 134 | * Gemerkt wird er erst, wenn das Schreiben wirklich gelungen ist: Sonst stuende |
| 135 | * ein misslungener Versuch als gesichert in der Buchfuehrung, und `jetzt()` |
| 136 | * haette beim Programmende nichts mehr nachzuholen (Befund 11). |
| 137 | */ |
| 138 | export interface Schriftsicherung { |
| 139 | /** Merkt die Stufe vor; geschrieben wird nach der Ruhezeit. */ |
| 140 | plane: (prozent: number) => void; |
| 141 | /** Schreibt sofort, was noch aussteht - fuer das Programmende. */ |
| 142 | jetzt: () => void; |
| 143 | } |
| 144 | |
| 145 | export const SICHERUNG_RUHEZEIT_MS = 750; |
| 146 | |
| 147 | export function schriftsicherung( |
| 148 | datei: string, |
| 149 | gesichert: number, |
| 150 | ruhezeitMs: number = SICHERUNG_RUHEZEIT_MS, |
| 151 | ): Schriftsicherung { |
| 152 | let letzteGesicherte = gueltigeStufe(gesichert); |
| 153 | let offen: number | null = null; |
| 154 | let uhr: ReturnType<typeof setTimeout> | null = null; |
| 155 | |
| 156 | const schreibe = (): void => { |
| 157 | if (uhr !== null) { |
| 158 | clearTimeout(uhr); |
| 159 | uhr = null; |
| 160 | } |
| 161 | if (offen === null) return; |
| 162 | const wert = offen; |
| 163 | // Erst der Versuch, dann die Buchfuehrung. Misslingt das Schreiben - kein |
| 164 | // Schreibrecht, Profilabgleich, Virenwaechter -, bleibt der Wert |
| 165 | // vorgemerkt: `jetzt()` beim Programmende und jede weitere Raste versuchen |
| 166 | // es dann noch einmal. Andersherum galt er als gesichert, obwohl nichts auf |
| 167 | // dem Datentraeger stand, und die Einstellung war fuer die ganze Sitzung |
| 168 | // verloren. |
| 169 | if (!schreibeSchriftgroesse(datei, wert)) return; |
| 170 | offen = null; |
| 171 | letzteGesicherte = wert; |
| 172 | }; |
| 173 | |
| 174 | return { |
| 175 | plane: (prozent) => { |
| 176 | const wert = gueltigeStufe(prozent); |
| 177 | if (wert === letzteGesicherte) { |
| 178 | // Auch eine bereits vorgemerkte Aenderung faellt weg, wenn der Anwender |
| 179 | // wieder auf dem gesicherten Wert landet - hin und zurueck ist keine |
| 180 | // Aenderung. |
| 181 | offen = null; |
| 182 | if (uhr !== null) { |
| 183 | clearTimeout(uhr); |
| 184 | uhr = null; |
| 185 | } |
| 186 | return; |
| 187 | } |
| 188 | offen = wert; |
| 189 | if (uhr !== null) clearTimeout(uhr); |
| 190 | uhr = setTimeout(schreibe, ruhezeitMs); |
| 191 | // Ein Zeitgeber im Hauptprozess darf das Beenden nicht hinauszoegern. |
| 192 | // Das Fragezeichen beanstandet der Linter, weil Node den Zeitgeber als |
| 193 | // Timeout mit unref fuehrt. Es bleibt: Unter einer Testnachbildung oder |
| 194 | // einem Browser-setTimeout kommt eine blosse Zahl zurueck, und dann |
| 195 | // duerfte das Ablegen des Schriftgrads nicht daran scheitern. |
| 196 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- unref gibt es nur bei Node-Zeitgebern; siehe darueber |
| 197 | uhr.unref?.(); |
| 198 | }, |
| 199 | jetzt: schreibe, |
| 200 | }; |
| 201 | } |
| 202 | |
| 203 | function leseDatei(datei: string): Record<string, unknown> { |
| 204 | try { |
| 205 | const gelesen: unknown = JSON.parse(readFileSync(datei, 'utf8')); |
| 206 | if (gelesen === null || typeof gelesen !== 'object' || Array.isArray(gelesen)) return {}; |
| 207 | return gelesen as Record<string, unknown>; |
| 208 | } catch { |
| 209 | return {}; |
| 210 | } |
| 211 | } |