import { describe, expect, it } from 'vitest'; import { LEERER_LAGEPLAN, MESSFEHLER_SCHRANKE, ZEICHENFEHLER_BILDPUNKTE, messgenauigkeit, planungsgrundlage, type Lageplan, } from '@/domain/geometrie/lageplan'; import { RILSA_DEFAULTS, type RilsaDefaults } from '@/domain/rilsa/constants'; /** * Messgenauigkeit der Bildunterlage. * * Diese Groesse entscheidet, ob aus einem Luftbild ueberhaupt vermessen werden * darf. Der Schaden liegt in der unsicheren Richtung: Ein zu grober Ausschnitt, * auf dem jemand ahnungslos eine Fussgaengerfurt abgreift, erzeugt eine zu * KURZE Zwischenzeit. Geprueft werden deshalb vor allem die Raender der * Einstufung - dort faellt die Entscheidung. */ /** * Lageplan mit einem Massstab von `meterJeBildpunkt`. * Die Bezugsstrecke ist 100 Bildpunkte lang; ihre Laenge in Metern setzt den * Massstab. */ function mitMassstab(meterJeBildpunkt: number): Lageplan { return { arbeitsbereiche: [], signalgeber: [], bild: { datenUrl: 'data:image/png;base64,AAAA', breite: 1000, hoehe: 1000, herkunft: 'Pruefbild', geladenAm: '2026-03-04T09:15:00.000Z', }, kalibrierung: { von: { x: 0, y: 0 }, bis: { x: 100, y: 0 }, laengeMeter: meterJeBildpunkt * 100, herkunft: 'georeferenziert', }, haltlinien: [], linien: [], }; } /* * KENNWERTE, AUS DENEN HIER NACHGERECHNET WIRD * * Die Zahlen stehen ausgeschrieben, damit im Test erkennbar bleibt, woher ein * erwarteter Wert kommt. Sie sind dieselben wie in RILSA_DEFAULTS - laeuft * eine der beiden Seiten weg, faellt genau dieser Fall auf. * * Die PAARUNG ist die eigentliche Aussage: Der Fussgaengerfehler beschreibt den * Konflikt "Fussgaenger raeumt die Furt, Kraftfahrzeug faehrt ein". Angesetzt * wird dabei der mobilitaetseingeschraenkte Fussgaenger - er ist der * Bemessungsfall und die sichere Seite. */ /** clearingSpeed.fussReducedMobility - der raeumende Fussgaenger. */ const V_RAEUM_FUSS = 1.0; /** clearingSpeed.kfzTurning - abbiegend, der unguenstigere der Kfz-Werte. */ const V_RAEUM_KFZ = 7.0; /** enteringSpeed.kfz - das einfahrende Kraftfahrzeug, in BEIDEN Zeilen. */ const V_EINFAHR_KFZ = 11.1; /** Zeitfehler je Meter Bodenaufloesung: 2 * (1/1,0 + 1/11,1) = 2,180180 s/m. */ const JE_METER_FUSS = ZEICHENFEHLER_BILDPUNKTE * (1 / V_RAEUM_FUSS + 1 / V_EINFAHR_KFZ); /** Dasselbe fuer den abbiegenden Kfz-Strom: 2 * (1/7,0 + 1/11,1) = 0,465894 s/m. */ const JE_METER_KFZ = ZEICHENFEHLER_BILDPUNKTE * (1 / V_RAEUM_KFZ + 1 / V_EINFAHR_KFZ); /** * Groebste Unterlage, auf der eine Furt noch abgreifbar ist. * 0,25 s / 2,180180 s/m = 0,114669 m je Bildpunkt, also gut 11,4 cm. */ const SCHRANKE_FUSS_METER = MESSFEHLER_SCHRANKE / JE_METER_FUSS; describe('Messgenauigkeit', () => { it('gibt ohne Massstab keine Genauigkeit an', () => { // Ohne Kalibrierung ist unbekannt, wie viel ein Bildpunkt am Boden misst. // Eine Einstufung waere hier frei erfunden. expect(messgenauigkeit(LEERER_LAGEPLAN)).toBeNull(); expect(messgenauigkeit({ ...mitMassstab(0.05), kalibrierung: null })).toBeNull(); }); it('rechnet den Zeitfehler aus Weg und Geschwindigkeit', () => { // 0,10 m je Bildpunkt, zwei Bildpunkte Zeichenfehler: 0,20 m auf dem // Raeumweg und noch einmal 0,20 m auf dem Einfahrweg. const g = ZEICHENFEHLER_BILDPUNKTE * 0.1; const genauigkeit = messgenauigkeit(mitMassstab(0.1))!; expect(genauigkeit.meterJeBildpunkt).toBeCloseTo(0.1, 9); // Furt: 1,0 m/s raeumend, 11,1 m/s einfahrend - 0,2 * (1 + 0,090090) = // 0,218018 s. expect(genauigkeit.fehlerFussSekunden).toBeCloseTo( g * (1 / V_RAEUM_FUSS + 1 / V_EINFAHR_KFZ), 6, ); // Abbiegende Kfz: 7,0 m/s raeumend, 11,1 m/s einfahrend - 0,2 * 0,232947 = // 0,046589 s. expect(genauigkeit.fehlerKfzSekunden).toBeCloseTo(g * (1 / V_RAEUM_KFZ + 1 / V_EINFAHR_KFZ), 6); }); it('paart den raeumenden Fussgaenger mit dem einfahrenden Kraftfahrzeug', () => { // DIE PAARUNG SELBST - der Fehler, der nicht zurueckkommen darf. // // Abgebildet ist der Konflikt "Fussgaenger raeumt die Furt, waehrend ein // KRAFTFAHRZEUG einfaehrt". Der umgekehrte Fall - ein Fussgaenger faehrt // in die Furt ein, die ein Fussgaenger raeumt - kommt als signalisierter // Konflikt praktisch nicht vor. Stand er hier, war der Fehler um zwei // Drittel zu gross: 1/1,2 + 1/1,5 = 1,50 statt 1/1,0 + 1/11,1 = 1,09 - und // keine reale Unterlage erreichte je die Stufe "voll". const plan = mitMassstab(0.1); const g = ZEICHENFEHLER_BILDPUNKTE * 0.1; // Die Einfahrgeschwindigkeit der KRAFTFAHRZEUGE geht ein: Halbiert auf // 5,55 m/s ergibt sie 0,2 * (1/1,0 + 1/5,55) = 0,236036 s statt 0,218018 s. const langsamereKfz: RilsaDefaults = { ...RILSA_DEFAULTS, enteringSpeed: { ...RILSA_DEFAULTS.enteringSpeed, kfz: V_EINFAHR_KFZ / 2 }, }; expect(messgenauigkeit(plan, langsamereKfz)!.fehlerFussSekunden).toBeCloseTo( g * (1 / V_RAEUM_FUSS + 2 / V_EINFAHR_KFZ), 6, ); // Die der Fussgaenger geht NICHT ein: Ein anderer Wert laesst den Fehler // unberuehrt. const andereFussEinfahrt: RilsaDefaults = { ...RILSA_DEFAULTS, enteringSpeed: { ...RILSA_DEFAULTS.enteringSpeed, fuss: 0.8 }, }; expect(messgenauigkeit(plan, andereFussEinfahrt)!.fehlerFussSekunden).toBe( messgenauigkeit(plan)!.fehlerFussSekunden, ); // Und geraeumt wird mit dem mobilitaetseingeschraenkten Fussgaenger, nicht // mit dem Regelfussgaenger: Auch dessen Kennwert bleibt ohne Wirkung. const andererRegelfussgaenger: RilsaDefaults = { ...RILSA_DEFAULTS, clearingSpeed: { ...RILSA_DEFAULTS.clearingSpeed, fuss: 0.9 }, }; expect(messgenauigkeit(plan, andererRegelfussgaenger)!.fehlerFussSekunden).toBe( messgenauigkeit(plan)!.fehlerFussSekunden, ); }); it('trifft die Geschwindigkeit im Nenner - der Fussgaengerfehler ist der grosse', () => { // Der eigentliche Grund fuer die ganze Groesse: Derselbe Zeichenfehler // kostet an der Furt ein Vielfaches dessen, was er beim Kfz kostet. // Ein Bild, das fuer Fahrspuren reicht, reicht deshalb nicht fuer eine Furt. // // Beide Zeilen haben dieselbe Einfahrgeschwindigkeit; der Unterschied kommt // allein aus dem Raeumen mit 1,0 statt 7,0 m/s. Das Verhaeltnis // (1/1,0 + 1/11,1) / (1/7,0 + 1/11,1) = 4,68 haengt nicht am Massstab. const genauigkeit = messgenauigkeit(mitMassstab(0.2))!; const verhaeltnis = JE_METER_FUSS / JE_METER_KFZ; expect(verhaeltnis).toBeCloseTo(4.68, 2); expect(genauigkeit.fehlerFussSekunden / genauigkeit.fehlerKfzSekunden).toBeCloseTo( verhaeltnis, 4, ); expect(genauigkeit.fehlerFussSekunden).toBeGreaterThan(genauigkeit.fehlerKfzSekunden * 4); }); it('stuft genau auf der Schranke noch als voll geeignet ein', () => { // 0,114669 m je Bildpunkt: 2 Bildpunkte mal (1/1,0 + 1/11,1) ergeben exakt // 0,25 s. Die Schranke ist eine Obergrenze - sie einzuhalten genuegt. const genauigkeit = messgenauigkeit(mitMassstab(SCHRANKE_FUSS_METER))!; expect(SCHRANKE_FUSS_METER).toBeCloseTo(0.114669, 6); expect(genauigkeit.fehlerFussSekunden).toBe(MESSFEHLER_SCHRANKE); expect(genauigkeit.eignung).toBe('voll'); }); it('kippt unmittelbar oberhalb der Schranke auf nur-kfz', () => { // Ein Hundertstel Meter je Bildpunkt groeber - und die Furt ist nicht mehr // abgreifbar: 2 * 0,124669 * 2,180180 = 0,271802 s. Die Kfz-Stroeme bleiben // es mit weitem Abstand (0,058083 s). const genauigkeit = messgenauigkeit(mitMassstab(SCHRANKE_FUSS_METER + 0.01))!; expect(genauigkeit.fehlerFussSekunden).toBeCloseTo( (SCHRANKE_FUSS_METER + 0.01) * JE_METER_FUSS, 6, ); expect(genauigkeit.fehlerFussSekunden).toBeGreaterThan(MESSFEHLER_SCHRANKE); expect(genauigkeit.fehlerKfzSekunden).toBeLessThan(MESSFEHLER_SCHRANKE); expect(genauigkeit.eignung).toBe('nur-kfz'); }); it('haelt bei einem Massstab knapp unter der Schranke die volle Eignung', () => { // Einen Millimeter je Bildpunkt feiner: 0,24782 s, noch unter 0,25 s. const genauigkeit = messgenauigkeit(mitMassstab(SCHRANKE_FUSS_METER - 0.001))!; expect(genauigkeit.fehlerFussSekunden).toBeCloseTo( (SCHRANKE_FUSS_METER - 0.001) * JE_METER_FUSS, 6, ); expect(genauigkeit.fehlerFussSekunden).toBeLessThan(MESSFEHLER_SCHRANKE); expect(genauigkeit.eignung).toBe('voll'); }); it('erklaert einen sehr groben Ausschnitt zum Uebersichtsbild', () => { // Hier haelt auch der Kfz-Fehler die Schranke nicht mehr. Aus einem solchen // Bild darf ueberhaupt nicht vermessen werden. const grob = messgenauigkeit(mitMassstab(1.0))!; expect(grob.fehlerKfzSekunden).toBeGreaterThan(MESSFEHLER_SCHRANKE); expect(grob.eignung).toBe('uebersicht'); }); it('stuft die Raender des Kfz-Bereichs richtig ein', () => { // Grenzmassstab fuer Kfz: 2 * m * (1/7,0 + 1/11,1) = 0,25 s. const grenze = MESSFEHLER_SCHRANKE / (ZEICHENFEHLER_BILDPUNKTE * (1 / 7.0 + 1 / 11.1)); expect(messgenauigkeit(mitMassstab(grenze - 0.001))!.eignung).toBe('nur-kfz'); expect(messgenauigkeit(mitMassstab(grenze + 0.001))!.eignung).toBe('uebersicht'); }); it('bildet die Ausschnittsgroessen des Anwendungsfalls ab', () => { // 2000 m Kantenlaenge auf 7000 Bildpunkten sind 28,6 cm je Bildpunkt: ein // Uebersichtsbild fuer die Gruene Welle - Kfz-Wege bleiben abgreifbar, // Furten nicht. const uebersicht = messgenauigkeit(mitMassstab(2000 / 7000))!; expect(uebersicht.eignung).toBe('nur-kfz'); // 0,285714 m je Bildpunkt mal 2,180180 s/m = 0,622909 s an der Furt - das // Zweieinhalbfache der hinnehmbaren 0,25 s. expect(uebersicht.fehlerFussSekunden).toBeCloseTo((2000 / 7000) * JE_METER_FUSS, 6); expect(uebersicht.fehlerFussSekunden).toBeGreaterThan(MESSFEHLER_SCHRANKE * 2); // 100 m Kantenlaenge auf 2000 Bildpunkten sind 5 cm je Bildpunkt: der // Knotenpunkt in nativer Aufloesung, auch die Furt ist abgreifbar. expect(messgenauigkeit(mitMassstab(100 / 2000))!.eignung).toBe('voll'); }); it('folgt angepassten Kennwerten, statt sie ein zweites Mal zu fuehren', () => { // Der Anwender darf die Raeumgeschwindigkeiten anpassen. Eine langsamer // angesetzte Fussgaengergeschwindigkeit vergroessert den Zeitfehler und // verschaerft damit die Anforderung an das Bild. Waeren die Zahlen hier // abgeschrieben, bliebe die Einstufung stehen und wuerde falsch. // // Veraendert wird clearingSpeed.fussReducedMobility (0,6 m/s ist ein // zulaessiger Wert): Das ist der Kennwert, der in die Einstufung eingeht. // Frueher stand hier clearingSpeed.fuss - seit der Fussgaengerfehler den // Bemessungsfall ansetzt, wird der gar nicht mehr gelesen, und der Fall // pruefte damit nichts mehr. Die Zusage bleibt dieselbe. // // Auf der Schrankenunterlage: 2 * 0,114669 * (1/0,6 + 1/11,1) = 0,402893 s // statt 0,25 s - die volle Eignung faellt auf nur-kfz. const langsamer: RilsaDefaults = { ...RILSA_DEFAULTS, clearingSpeed: { ...RILSA_DEFAULTS.clearingSpeed, fussReducedMobility: 0.6 }, }; const plan = mitMassstab(SCHRANKE_FUSS_METER); expect(messgenauigkeit(plan)!.eignung).toBe('voll'); expect(messgenauigkeit(plan, langsamer)!.eignung).toBe('nur-kfz'); expect(messgenauigkeit(plan, langsamer)!.fehlerFussSekunden).toBeCloseTo( SCHRANKE_FUSS_METER * ZEICHENFEHLER_BILDPUNKTE * (1 / 0.6 + 1 / V_EINFAHR_KFZ), 6, ); expect(messgenauigkeit(plan, langsamer)!.fehlerFussSekunden).toBeGreaterThan( messgenauigkeit(plan)!.fehlerFussSekunden, ); }); it('stuft bei einer Geschwindigkeit von 0 zur sicheren Seite ein', () => { // Ein solcher Kennwertsatz kommt aus der Vorgabenpruefung nicht heraus. // Falls doch: kein Absturz und keine geschoenten 0 s, sondern die // schlechteste Einstufung. // // Auf 0 zu setzen sind die Kennwerte, die tatsaechlich gelesen werden - // fussReducedMobility und kfzTurning. Ueber clearingSpeed.fuss ist der // entartete Fall seit der neuen Paarung nicht mehr erreichbar. const entartet: RilsaDefaults = { ...RILSA_DEFAULTS, clearingSpeed: { ...RILSA_DEFAULTS.clearingSpeed, fussReducedMobility: 0, kfzTurning: 0 }, }; const genauigkeit = messgenauigkeit(mitMassstab(0.05), entartet)!; expect(genauigkeit.eignung).toBe('uebersicht'); expect(Number.isFinite(genauigkeit.fehlerFussSekunden)).toBe(false); }); }); describe('Messgenauigkeit in der Planungsgrundlage', () => { it('wird mit dem Nachweis zusammen gefuehrt', () => { const grundlage = planungsgrundlage(mitMassstab(0.05)); expect(grundlage.messgenauigkeit).not.toBeNull(); expect(grundlage.messgenauigkeit!.eignung).toBe('voll'); // Dieselbe Zahl wie im uebrigen Nachweis - zwei Massstaebe im selben // Ausdruck waeren nicht nachrechenbar. expect(grundlage.messgenauigkeit!.meterJeBildpunkt).toBe(grundlage.meterJeBildpunkt); }); it('bleibt ohne Massstab leer, statt eine Eignung zu behaupten', () => { expect(planungsgrundlage(LEERER_LAGEPLAN).messgenauigkeit).toBeNull(); }); it('reicht angepasste Kennwerte durch', () => { // Wie oben: Massgebend ist die Raeumgeschwindigkeit des // mobilitaetseingeschraenkten Fussgaengers. Auf der Schrankenunterlage // wird aus 0,25 s ein Fehler von 0,402893 s - und aus "voll" wird // "nur-kfz", auch auf dem Weg ueber die Planungsgrundlage. const langsamer: RilsaDefaults = { ...RILSA_DEFAULTS, clearingSpeed: { ...RILSA_DEFAULTS.clearingSpeed, fussReducedMobility: 0.6 }, }; const plan = mitMassstab(SCHRANKE_FUSS_METER); expect(planungsgrundlage(plan).messgenauigkeit!.eignung).toBe('voll'); expect(planungsgrundlage(plan, langsamer).messgenauigkeit!.eignung).toBe('nur-kfz'); }); });