lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests domain messgenauigkeit.test.ts
| 1 | import { describe, expect, it } from 'vitest'; |
| 2 | import { |
| 3 | LEERER_LAGEPLAN, |
| 4 | MESSFEHLER_SCHRANKE, |
| 5 | ZEICHENFEHLER_BILDPUNKTE, |
| 6 | messgenauigkeit, |
| 7 | planungsgrundlage, |
| 8 | type Lageplan, |
| 9 | } from '@/domain/geometrie/lageplan'; |
| 10 | import { RILSA_DEFAULTS, type RilsaDefaults } from '@/domain/rilsa/constants'; |
| 11 | |
| 12 | /** |
| 13 | * Messgenauigkeit der Bildunterlage. |
| 14 | * |
| 15 | * Diese Groesse entscheidet, ob aus einem Luftbild ueberhaupt vermessen werden |
| 16 | * darf. Der Schaden liegt in der unsicheren Richtung: Ein zu grober Ausschnitt, |
| 17 | * auf dem jemand ahnungslos eine Fussgaengerfurt abgreift, erzeugt eine zu |
| 18 | * KURZE Zwischenzeit. Geprueft werden deshalb vor allem die Raender der |
| 19 | * Einstufung - dort faellt die Entscheidung. |
| 20 | */ |
| 21 | |
| 22 | /** |
| 23 | * Lageplan mit einem Massstab von `meterJeBildpunkt`. |
| 24 | * Die Bezugsstrecke ist 100 Bildpunkte lang; ihre Laenge in Metern setzt den |
| 25 | * Massstab. |
| 26 | */ |
| 27 | function mitMassstab(meterJeBildpunkt: number): Lageplan { |
| 28 | return { |
| 29 | arbeitsbereiche: [], |
| 30 | signalgeber: [], |
| 31 | bild: { |
| 32 | datenUrl: 'data:image/png;base64,AAAA', |
| 33 | breite: 1000, |
| 34 | hoehe: 1000, |
| 35 | herkunft: 'Pruefbild', |
| 36 | geladenAm: '2026-03-04T09:15:00.000Z', |
| 37 | }, |
| 38 | kalibrierung: { |
| 39 | von: { x: 0, y: 0 }, |
| 40 | bis: { x: 100, y: 0 }, |
| 41 | laengeMeter: meterJeBildpunkt * 100, |
| 42 | herkunft: 'georeferenziert', |
| 43 | }, |
| 44 | haltlinien: [], |
| 45 | linien: [], |
| 46 | }; |
| 47 | } |
| 48 | |
| 49 | /* |
| 50 | * KENNWERTE, AUS DENEN HIER NACHGERECHNET WIRD |
| 51 | * |
| 52 | * Die Zahlen stehen ausgeschrieben, damit im Test erkennbar bleibt, woher ein |
| 53 | * erwarteter Wert kommt. Sie sind dieselben wie in RILSA_DEFAULTS - laeuft |
| 54 | * eine der beiden Seiten weg, faellt genau dieser Fall auf. |
| 55 | * |
| 56 | * Die PAARUNG ist die eigentliche Aussage: Der Fussgaengerfehler beschreibt den |
| 57 | * Konflikt "Fussgaenger raeumt die Furt, Kraftfahrzeug faehrt ein". Angesetzt |
| 58 | * wird dabei der mobilitaetseingeschraenkte Fussgaenger - er ist der |
| 59 | * Bemessungsfall und die sichere Seite. |
| 60 | */ |
| 61 | /** clearingSpeed.fussReducedMobility - der raeumende Fussgaenger. */ |
| 62 | const V_RAEUM_FUSS = 1.0; |
| 63 | /** clearingSpeed.kfzTurning - abbiegend, der unguenstigere der Kfz-Werte. */ |
| 64 | const V_RAEUM_KFZ = 7.0; |
| 65 | /** enteringSpeed.kfz - das einfahrende Kraftfahrzeug, in BEIDEN Zeilen. */ |
| 66 | const V_EINFAHR_KFZ = 11.1; |
| 67 | |
| 68 | /** Zeitfehler je Meter Bodenaufloesung: 2 * (1/1,0 + 1/11,1) = 2,180180 s/m. */ |
| 69 | const JE_METER_FUSS = ZEICHENFEHLER_BILDPUNKTE * (1 / V_RAEUM_FUSS + 1 / V_EINFAHR_KFZ); |
| 70 | /** Dasselbe fuer den abbiegenden Kfz-Strom: 2 * (1/7,0 + 1/11,1) = 0,465894 s/m. */ |
| 71 | const JE_METER_KFZ = ZEICHENFEHLER_BILDPUNKTE * (1 / V_RAEUM_KFZ + 1 / V_EINFAHR_KFZ); |
| 72 | |
| 73 | /** |
| 74 | * Groebste Unterlage, auf der eine Furt noch abgreifbar ist. |
| 75 | * 0,25 s / 2,180180 s/m = 0,114669 m je Bildpunkt, also gut 11,4 cm. |
| 76 | */ |
| 77 | const SCHRANKE_FUSS_METER = MESSFEHLER_SCHRANKE / JE_METER_FUSS; |
| 78 | |
| 79 | describe('Messgenauigkeit', () => { |
| 80 | it('gibt ohne Massstab keine Genauigkeit an', () => { |
| 81 | // Ohne Kalibrierung ist unbekannt, wie viel ein Bildpunkt am Boden misst. |
| 82 | // Eine Einstufung waere hier frei erfunden. |
| 83 | expect(messgenauigkeit(LEERER_LAGEPLAN)).toBeNull(); |
| 84 | expect(messgenauigkeit({ ...mitMassstab(0.05), kalibrierung: null })).toBeNull(); |
| 85 | }); |
| 86 | |
| 87 | it('rechnet den Zeitfehler aus Weg und Geschwindigkeit', () => { |
| 88 | // 0,10 m je Bildpunkt, zwei Bildpunkte Zeichenfehler: 0,20 m auf dem |
| 89 | // Raeumweg und noch einmal 0,20 m auf dem Einfahrweg. |
| 90 | const g = ZEICHENFEHLER_BILDPUNKTE * 0.1; |
| 91 | const genauigkeit = messgenauigkeit(mitMassstab(0.1))!; |
| 92 | |
| 93 | expect(genauigkeit.meterJeBildpunkt).toBeCloseTo(0.1, 9); |
| 94 | // Furt: 1,0 m/s raeumend, 11,1 m/s einfahrend - 0,2 * (1 + 0,090090) = |
| 95 | // 0,218018 s. |
| 96 | expect(genauigkeit.fehlerFussSekunden).toBeCloseTo( |
| 97 | g * (1 / V_RAEUM_FUSS + 1 / V_EINFAHR_KFZ), |
| 98 | 6, |
| 99 | ); |
| 100 | // Abbiegende Kfz: 7,0 m/s raeumend, 11,1 m/s einfahrend - 0,2 * 0,232947 = |
| 101 | // 0,046589 s. |
| 102 | expect(genauigkeit.fehlerKfzSekunden).toBeCloseTo(g * (1 / V_RAEUM_KFZ + 1 / V_EINFAHR_KFZ), 6); |
| 103 | }); |
| 104 | |
| 105 | it('paart den raeumenden Fussgaenger mit dem einfahrenden Kraftfahrzeug', () => { |
| 106 | // DIE PAARUNG SELBST - der Fehler, der nicht zurueckkommen darf. |
| 107 | // |
| 108 | // Abgebildet ist der Konflikt "Fussgaenger raeumt die Furt, waehrend ein |
| 109 | // KRAFTFAHRZEUG einfaehrt". Der umgekehrte Fall - ein Fussgaenger faehrt |
| 110 | // in die Furt ein, die ein Fussgaenger raeumt - kommt als signalisierter |
| 111 | // Konflikt praktisch nicht vor. Stand er hier, war der Fehler um zwei |
| 112 | // Drittel zu gross: 1/1,2 + 1/1,5 = 1,50 statt 1/1,0 + 1/11,1 = 1,09 - und |
| 113 | // keine reale Unterlage erreichte je die Stufe "voll". |
| 114 | const plan = mitMassstab(0.1); |
| 115 | const g = ZEICHENFEHLER_BILDPUNKTE * 0.1; |
| 116 | |
| 117 | // Die Einfahrgeschwindigkeit der KRAFTFAHRZEUGE geht ein: Halbiert auf |
| 118 | // 5,55 m/s ergibt sie 0,2 * (1/1,0 + 1/5,55) = 0,236036 s statt 0,218018 s. |
| 119 | const langsamereKfz: RilsaDefaults = { |
| 120 | ...RILSA_DEFAULTS, |
| 121 | enteringSpeed: { ...RILSA_DEFAULTS.enteringSpeed, kfz: V_EINFAHR_KFZ / 2 }, |
| 122 | }; |
| 123 | expect(messgenauigkeit(plan, langsamereKfz)!.fehlerFussSekunden).toBeCloseTo( |
| 124 | g * (1 / V_RAEUM_FUSS + 2 / V_EINFAHR_KFZ), |
| 125 | 6, |
| 126 | ); |
| 127 | |
| 128 | // Die der Fussgaenger geht NICHT ein: Ein anderer Wert laesst den Fehler |
| 129 | // unberuehrt. |
| 130 | const andereFussEinfahrt: RilsaDefaults = { |
| 131 | ...RILSA_DEFAULTS, |
| 132 | enteringSpeed: { ...RILSA_DEFAULTS.enteringSpeed, fuss: 0.8 }, |
| 133 | }; |
| 134 | expect(messgenauigkeit(plan, andereFussEinfahrt)!.fehlerFussSekunden).toBe( |
| 135 | messgenauigkeit(plan)!.fehlerFussSekunden, |
| 136 | ); |
| 137 | |
| 138 | // Und geraeumt wird mit dem mobilitaetseingeschraenkten Fussgaenger, nicht |
| 139 | // mit dem Regelfussgaenger: Auch dessen Kennwert bleibt ohne Wirkung. |
| 140 | const andererRegelfussgaenger: RilsaDefaults = { |
| 141 | ...RILSA_DEFAULTS, |
| 142 | clearingSpeed: { ...RILSA_DEFAULTS.clearingSpeed, fuss: 0.9 }, |
| 143 | }; |
| 144 | expect(messgenauigkeit(plan, andererRegelfussgaenger)!.fehlerFussSekunden).toBe( |
| 145 | messgenauigkeit(plan)!.fehlerFussSekunden, |
| 146 | ); |
| 147 | }); |
| 148 | |
| 149 | it('trifft die Geschwindigkeit im Nenner - der Fussgaengerfehler ist der grosse', () => { |
| 150 | // Der eigentliche Grund fuer die ganze Groesse: Derselbe Zeichenfehler |
| 151 | // kostet an der Furt ein Vielfaches dessen, was er beim Kfz kostet. |
| 152 | // Ein Bild, das fuer Fahrspuren reicht, reicht deshalb nicht fuer eine Furt. |
| 153 | // |
| 154 | // Beide Zeilen haben dieselbe Einfahrgeschwindigkeit; der Unterschied kommt |
| 155 | // allein aus dem Raeumen mit 1,0 statt 7,0 m/s. Das Verhaeltnis |
| 156 | // (1/1,0 + 1/11,1) / (1/7,0 + 1/11,1) = 4,68 haengt nicht am Massstab. |
| 157 | const genauigkeit = messgenauigkeit(mitMassstab(0.2))!; |
| 158 | const verhaeltnis = JE_METER_FUSS / JE_METER_KFZ; |
| 159 | |
| 160 | expect(verhaeltnis).toBeCloseTo(4.68, 2); |
| 161 | expect(genauigkeit.fehlerFussSekunden / genauigkeit.fehlerKfzSekunden).toBeCloseTo( |
| 162 | verhaeltnis, |
| 163 | 4, |
| 164 | ); |
| 165 | expect(genauigkeit.fehlerFussSekunden).toBeGreaterThan(genauigkeit.fehlerKfzSekunden * 4); |
| 166 | }); |
| 167 | |
| 168 | it('stuft genau auf der Schranke noch als voll geeignet ein', () => { |
| 169 | // 0,114669 m je Bildpunkt: 2 Bildpunkte mal (1/1,0 + 1/11,1) ergeben exakt |
| 170 | // 0,25 s. Die Schranke ist eine Obergrenze - sie einzuhalten genuegt. |
| 171 | const genauigkeit = messgenauigkeit(mitMassstab(SCHRANKE_FUSS_METER))!; |
| 172 | |
| 173 | expect(SCHRANKE_FUSS_METER).toBeCloseTo(0.114669, 6); |
| 174 | expect(genauigkeit.fehlerFussSekunden).toBe(MESSFEHLER_SCHRANKE); |
| 175 | expect(genauigkeit.eignung).toBe('voll'); |
| 176 | }); |
| 177 | |
| 178 | it('kippt unmittelbar oberhalb der Schranke auf nur-kfz', () => { |
| 179 | // Ein Hundertstel Meter je Bildpunkt groeber - und die Furt ist nicht mehr |
| 180 | // abgreifbar: 2 * 0,124669 * 2,180180 = 0,271802 s. Die Kfz-Stroeme bleiben |
| 181 | // es mit weitem Abstand (0,058083 s). |
| 182 | const genauigkeit = messgenauigkeit(mitMassstab(SCHRANKE_FUSS_METER + 0.01))!; |
| 183 | |
| 184 | expect(genauigkeit.fehlerFussSekunden).toBeCloseTo( |
| 185 | (SCHRANKE_FUSS_METER + 0.01) * JE_METER_FUSS, |
| 186 | 6, |
| 187 | ); |
| 188 | expect(genauigkeit.fehlerFussSekunden).toBeGreaterThan(MESSFEHLER_SCHRANKE); |
| 189 | expect(genauigkeit.fehlerKfzSekunden).toBeLessThan(MESSFEHLER_SCHRANKE); |
| 190 | expect(genauigkeit.eignung).toBe('nur-kfz'); |
| 191 | }); |
| 192 | |
| 193 | it('haelt bei einem Massstab knapp unter der Schranke die volle Eignung', () => { |
| 194 | // Einen Millimeter je Bildpunkt feiner: 0,24782 s, noch unter 0,25 s. |
| 195 | const genauigkeit = messgenauigkeit(mitMassstab(SCHRANKE_FUSS_METER - 0.001))!; |
| 196 | |
| 197 | expect(genauigkeit.fehlerFussSekunden).toBeCloseTo( |
| 198 | (SCHRANKE_FUSS_METER - 0.001) * JE_METER_FUSS, |
| 199 | 6, |
| 200 | ); |
| 201 | expect(genauigkeit.fehlerFussSekunden).toBeLessThan(MESSFEHLER_SCHRANKE); |
| 202 | expect(genauigkeit.eignung).toBe('voll'); |
| 203 | }); |
| 204 | |
| 205 | it('erklaert einen sehr groben Ausschnitt zum Uebersichtsbild', () => { |
| 206 | // Hier haelt auch der Kfz-Fehler die Schranke nicht mehr. Aus einem solchen |
| 207 | // Bild darf ueberhaupt nicht vermessen werden. |
| 208 | const grob = messgenauigkeit(mitMassstab(1.0))!; |
| 209 | |
| 210 | expect(grob.fehlerKfzSekunden).toBeGreaterThan(MESSFEHLER_SCHRANKE); |
| 211 | expect(grob.eignung).toBe('uebersicht'); |
| 212 | }); |
| 213 | |
| 214 | it('stuft die Raender des Kfz-Bereichs richtig ein', () => { |
| 215 | // Grenzmassstab fuer Kfz: 2 * m * (1/7,0 + 1/11,1) = 0,25 s. |
| 216 | const grenze = MESSFEHLER_SCHRANKE / (ZEICHENFEHLER_BILDPUNKTE * (1 / 7.0 + 1 / 11.1)); |
| 217 | |
| 218 | expect(messgenauigkeit(mitMassstab(grenze - 0.001))!.eignung).toBe('nur-kfz'); |
| 219 | expect(messgenauigkeit(mitMassstab(grenze + 0.001))!.eignung).toBe('uebersicht'); |
| 220 | }); |
| 221 | |
| 222 | it('bildet die Ausschnittsgroessen des Anwendungsfalls ab', () => { |
| 223 | // 2000 m Kantenlaenge auf 7000 Bildpunkten sind 28,6 cm je Bildpunkt: ein |
| 224 | // Uebersichtsbild fuer die Gruene Welle - Kfz-Wege bleiben abgreifbar, |
| 225 | // Furten nicht. |
| 226 | const uebersicht = messgenauigkeit(mitMassstab(2000 / 7000))!; |
| 227 | expect(uebersicht.eignung).toBe('nur-kfz'); |
| 228 | // 0,285714 m je Bildpunkt mal 2,180180 s/m = 0,622909 s an der Furt - das |
| 229 | // Zweieinhalbfache der hinnehmbaren 0,25 s. |
| 230 | expect(uebersicht.fehlerFussSekunden).toBeCloseTo((2000 / 7000) * JE_METER_FUSS, 6); |
| 231 | expect(uebersicht.fehlerFussSekunden).toBeGreaterThan(MESSFEHLER_SCHRANKE * 2); |
| 232 | |
| 233 | // 100 m Kantenlaenge auf 2000 Bildpunkten sind 5 cm je Bildpunkt: der |
| 234 | // Knotenpunkt in nativer Aufloesung, auch die Furt ist abgreifbar. |
| 235 | expect(messgenauigkeit(mitMassstab(100 / 2000))!.eignung).toBe('voll'); |
| 236 | }); |
| 237 | |
| 238 | it('folgt angepassten Kennwerten, statt sie ein zweites Mal zu fuehren', () => { |
| 239 | // Der Anwender darf die Raeumgeschwindigkeiten anpassen. Eine langsamer |
| 240 | // angesetzte Fussgaengergeschwindigkeit vergroessert den Zeitfehler und |
| 241 | // verschaerft damit die Anforderung an das Bild. Waeren die Zahlen hier |
| 242 | // abgeschrieben, bliebe die Einstufung stehen und wuerde falsch. |
| 243 | // |
| 244 | // Veraendert wird clearingSpeed.fussReducedMobility (0,6 m/s ist ein |
| 245 | // zulaessiger Wert): Das ist der Kennwert, der in die Einstufung eingeht. |
| 246 | // Frueher stand hier clearingSpeed.fuss - seit der Fussgaengerfehler den |
| 247 | // Bemessungsfall ansetzt, wird der gar nicht mehr gelesen, und der Fall |
| 248 | // pruefte damit nichts mehr. Die Zusage bleibt dieselbe. |
| 249 | // |
| 250 | // Auf der Schrankenunterlage: 2 * 0,114669 * (1/0,6 + 1/11,1) = 0,402893 s |
| 251 | // statt 0,25 s - die volle Eignung faellt auf nur-kfz. |
| 252 | const langsamer: RilsaDefaults = { |
| 253 | ...RILSA_DEFAULTS, |
| 254 | clearingSpeed: { ...RILSA_DEFAULTS.clearingSpeed, fussReducedMobility: 0.6 }, |
| 255 | }; |
| 256 | const plan = mitMassstab(SCHRANKE_FUSS_METER); |
| 257 | |
| 258 | expect(messgenauigkeit(plan)!.eignung).toBe('voll'); |
| 259 | expect(messgenauigkeit(plan, langsamer)!.eignung).toBe('nur-kfz'); |
| 260 | expect(messgenauigkeit(plan, langsamer)!.fehlerFussSekunden).toBeCloseTo( |
| 261 | SCHRANKE_FUSS_METER * ZEICHENFEHLER_BILDPUNKTE * (1 / 0.6 + 1 / V_EINFAHR_KFZ), |
| 262 | 6, |
| 263 | ); |
| 264 | expect(messgenauigkeit(plan, langsamer)!.fehlerFussSekunden).toBeGreaterThan( |
| 265 | messgenauigkeit(plan)!.fehlerFussSekunden, |
| 266 | ); |
| 267 | }); |
| 268 | |
| 269 | it('stuft bei einer Geschwindigkeit von 0 zur sicheren Seite ein', () => { |
| 270 | // Ein solcher Kennwertsatz kommt aus der Vorgabenpruefung nicht heraus. |
| 271 | // Falls doch: kein Absturz und keine geschoenten 0 s, sondern die |
| 272 | // schlechteste Einstufung. |
| 273 | // |
| 274 | // Auf 0 zu setzen sind die Kennwerte, die tatsaechlich gelesen werden - |
| 275 | // fussReducedMobility und kfzTurning. Ueber clearingSpeed.fuss ist der |
| 276 | // entartete Fall seit der neuen Paarung nicht mehr erreichbar. |
| 277 | const entartet: RilsaDefaults = { |
| 278 | ...RILSA_DEFAULTS, |
| 279 | clearingSpeed: { ...RILSA_DEFAULTS.clearingSpeed, fussReducedMobility: 0, kfzTurning: 0 }, |
| 280 | }; |
| 281 | const genauigkeit = messgenauigkeit(mitMassstab(0.05), entartet)!; |
| 282 | |
| 283 | expect(genauigkeit.eignung).toBe('uebersicht'); |
| 284 | expect(Number.isFinite(genauigkeit.fehlerFussSekunden)).toBe(false); |
| 285 | }); |
| 286 | }); |
| 287 | |
| 288 | describe('Messgenauigkeit in der Planungsgrundlage', () => { |
| 289 | it('wird mit dem Nachweis zusammen gefuehrt', () => { |
| 290 | const grundlage = planungsgrundlage(mitMassstab(0.05)); |
| 291 | |
| 292 | expect(grundlage.messgenauigkeit).not.toBeNull(); |
| 293 | expect(grundlage.messgenauigkeit!.eignung).toBe('voll'); |
| 294 | // Dieselbe Zahl wie im uebrigen Nachweis - zwei Massstaebe im selben |
| 295 | // Ausdruck waeren nicht nachrechenbar. |
| 296 | expect(grundlage.messgenauigkeit!.meterJeBildpunkt).toBe(grundlage.meterJeBildpunkt); |
| 297 | }); |
| 298 | |
| 299 | it('bleibt ohne Massstab leer, statt eine Eignung zu behaupten', () => { |
| 300 | expect(planungsgrundlage(LEERER_LAGEPLAN).messgenauigkeit).toBeNull(); |
| 301 | }); |
| 302 | |
| 303 | it('reicht angepasste Kennwerte durch', () => { |
| 304 | // Wie oben: Massgebend ist die Raeumgeschwindigkeit des |
| 305 | // mobilitaetseingeschraenkten Fussgaengers. Auf der Schrankenunterlage |
| 306 | // wird aus 0,25 s ein Fehler von 0,402893 s - und aus "voll" wird |
| 307 | // "nur-kfz", auch auf dem Weg ueber die Planungsgrundlage. |
| 308 | const langsamer: RilsaDefaults = { |
| 309 | ...RILSA_DEFAULTS, |
| 310 | clearingSpeed: { ...RILSA_DEFAULTS.clearingSpeed, fussReducedMobility: 0.6 }, |
| 311 | }; |
| 312 | const plan = mitMassstab(SCHRANKE_FUSS_METER); |
| 313 | |
| 314 | expect(planungsgrundlage(plan).messgenauigkeit!.eignung).toBe('voll'); |
| 315 | expect(planungsgrundlage(plan, langsamer).messgenauigkeit!.eignung).toBe('nur-kfz'); |
| 316 | }); |
| 317 | }); |