import { describe, expect, it } from 'vitest'; import { bildzuschnitt, planblatt } from '@/domain/geometrie/planblatt'; import type { Lageplan } from '@/domain/geometrie/lageplan'; /** * Welchen Teil des Luftbilds das Lageplanblatt zeigt. * * WOZU (aus dem Betrieb, 11.09.2026): Das gedruckte Blatt zeigte die * Fahrlinien ohne Bild darunter und trug keine Angabe zur Verortung - kein * Nordpfeil, kein Strassenname, keine Koordinate. Wer es allein vor sich * hatte, konnte nicht sagen, wo die Zeichnung liegt. * * DAS BILD GANZ MITZUGEBEN WAR NIE MOEGLICH: bis zu 24 MiB je Unterlage. Der * Ausweg ist der Zuschnitt - das Blatt zeigt ohnehin nur den gezeichneten * Bereich, und danach haengt die Groesse an der Blattgroesse statt an der * Vorlage. Diese Datei bewacht die Rechnung dahinter. * * DIE GEFAEHRLICHE STELLE ist nicht die Groesse, sondern die DECKUNG: Liegt * das Bild um Bildpunkte verschoben unter den Linien, sieht ein Pruefer * Raeumwege neben der Fahrbahn und haelt die Zeichnung fuer falsch - oder, * schlimmer, er haelt sie fuer richtig und die Fahrbahn fuer woanders. */ /** Ein Lageplan mit bekanntem Massstab: 100 Bildpunkte sind 20 m. */ function lageplan(punkte: readonly { x: number; y: number }[]): Lageplan { return { arbeitsbereiche: [], signalgeber: [], bild: null, kalibrierung: { von: { x: 0, y: 0 }, bis: { x: 100, y: 0 }, laengeMeter: 20, herkunft: 'georeferenziert', }, haltlinien: [], linien: [ { id: 'l1', name: 'K1', mode: 'kfz', movement: 'geradeaus', breiteMeter: 3, punkte, haltlinieId: null, startT: 0, signalGroupId: null, }, ], }; } /** Ein quadratisches Blatt macht die Rechnung ohne Verzerrung nachvollziehbar. */ function quadratblatt(punkte: readonly { x: number; y: number }[]) { const blatt = planblatt(lageplan(punkte), 200, 200, 20); if (blatt === null) throw new Error('Blatt erwartet'); return blatt; } describe('Der Bildausschnitt ist die Umkehrung der Abbildung', () => { it('umfasst mehr als das Gezeichnete - naemlich das ganze Blatt', () => { // Gezeichnet sind 100 Bildpunkte, das Blatt ist 200 Punkte breit mit 20 // Punkten Rand: Der Faktor ist 160/100 = 1,6, und das ganze Blatt deckt // 200/1,6 = 125 Bildpunkte ab. const blatt = quadratblatt([ { x: 0, y: 0 }, { x: 100, y: 100 }, ]); expect(blatt.bildausschnitt.breite).toBeCloseTo(125, 6); expect(blatt.bildausschnitt.hoehe).toBeCloseTo(125, 6); }); it('beginnt um den Rand VOR dem Gezeichneten', () => { const blatt = quadratblatt([ { x: 40, y: 60 }, { x: 140, y: 160 }, ]); // 20 Blattpunkte Rand sind 20/1,6 = 12,5 Bildpunkte. expect(blatt.bildausschnitt.x).toBeCloseTo(40 - 12.5, 6); expect(blatt.bildausschnitt.y).toBeCloseTo(60 - 12.5, 6); }); it('bildet jeden Blattpunkt auf denselben Bildpunkt ab wie die Linien', () => { /* * DER EIGENTLICHE NACHWEIS DER DECKUNG. Wo eine Linie auf dem Blatt liegt, * muss der Ausschnitt denselben Bildpunkt zeigen - sonst liegt das Bild * gegen die Zeichnung verschoben. */ const roh = [ { x: 10, y: 20 }, { x: 210, y: 120 }, ]; const blatt = quadratblatt(roh); const a = blatt.bildausschnitt; for (const [i, punkt] of blatt.linien[0]!.punkte.entries()) { const zurueck = { x: a.x + (punkt.x / blatt.breite) * a.breite, y: a.y + (punkt.y / blatt.hoehe) * a.hoehe, }; expect(zurueck.x).toBeCloseTo(roh[i]!.x, 6); expect(zurueck.y).toBeCloseTo(roh[i]!.y, 6); } }); it('steht auch dann bereit, wenn das Projekt gar kein Bild fuehrt', () => { // Er beschreibt eine Abbildung, keinen Bestand. Ein wahlfreies Feld // muesste jede Lesestelle abfragen. const blatt = quadratblatt([ { x: 0, y: 0 }, { x: 50, y: 50 }, ]); expect(blatt.bildausschnitt.breite).toBeGreaterThan(0); expect(blatt.bildausschnitt.hoehe).toBeGreaterThan(0); }); }); describe('Der Zuschnitt beschneidet an den Bildkanten', () => { /** Zeichnung mitten in einem grossen Bild - nichts ragt hinaus. */ const innen = () => quadratblatt([ { x: 400, y: 400 }, { x: 500, y: 500 }, ]); it('nimmt bei einer Zeichnung mitten im Bild den ganzen Zielbereich ein', () => { const z = bildzuschnitt(innen(), 1000, 1000, 400, 400); expect(z).not.toBeNull(); expect(z!.zielX).toBeCloseTo(0, 6); expect(z!.zielY).toBeCloseTo(0, 6); expect(z!.zielBreite).toBeCloseTo(400, 6); expect(z!.zielHoehe).toBeCloseTo(400, 6); }); it('rechnet den Quellbereich in Bildpunkte des Bildes um', () => { const blatt = innen(); const z = bildzuschnitt(blatt, 1000, 1000, 400, 400)!; expect(z.quelleX).toBeCloseTo(blatt.bildausschnitt.x, 6); expect(z.quelleBreite).toBeCloseTo(blatt.bildausschnitt.breite, 6); }); it('schneidet ab, was ueber die linke obere Bildkante hinausragt', () => { /* * DER REGELFALL BEI EINER ZEICHNUNG AM BILDRAND. Ohne diese Rechnung * verhielten sich die Ausgabewege verschieden: Die Zeichenflaeche des * Browsers beschneidet und verschiebt das Ziel stillschweigend mit, ein * PDF-Erzeuger nicht - und das Bild laege dann verschoben unter den Linien. */ const blatt = quadratblatt([ { x: 0, y: 0 }, { x: 100, y: 100 }, ]); // Der Ausschnitt beginnt bei -12,5 - also 12,5 von 125 Bildpunkten // ausserhalb, das sind 10 Prozent. const z = bildzuschnitt(blatt, 1000, 1000, 400, 400)!; expect(z.quelleX).toBe(0); expect(z.quelleY).toBe(0); expect(z.zielX).toBeCloseTo(40, 6); expect(z.zielY).toBeCloseTo(40, 6); expect(z.zielBreite).toBeCloseTo(360, 6); }); it('schneidet ab, was ueber die rechte untere Bildkante hinausragt', () => { const blatt = quadratblatt([ { x: 0, y: 0 }, { x: 100, y: 100 }, ]); // Bild nur 100 x 100 gross: Der Ausschnitt reicht bis 112,5. const z = bildzuschnitt(blatt, 100, 100, 400, 400)!; expect(z.quelleBreite).toBeCloseTo(100, 6); expect(z.zielX).toBeCloseTo(40, 6); expect(z.zielBreite).toBeCloseTo(320, 6); expect(z.zielX + z.zielBreite).toBeLessThan(400); }); it('meldet null, wo das Blatt gar nichts vom Bild zeigt', () => { // Kein Fehler: Eine Zeichnung kann vollstaendig neben dem Bild liegen. const blatt = quadratblatt([ { x: 5000, y: 5000 }, { x: 5100, y: 5100 }, ]); expect(bildzuschnitt(blatt, 100, 100, 400, 400)).toBeNull(); }); it('meldet null, wo das Blatt das Bild nur an der Kante beruehrt', () => { /* * Ein Ausschnitt der Breite null ist kein Ausschnitt. Wuerde er * durchgelassen, ginge ein `drawImage` mit Breite 0 an die Zeichenflaeche * und ein Bild ohne Ausdehnung in das PDF - je nach Ausgabeweg ein * unsichtbarer Fehlschlag oder eine Ausnahme mitten im Erzeugen der * Unterlage. * * Gezeichnet ist von 100 bis 180, der Ausschnitt beginnt deshalb bei 90. * Ein Bild von genau 90 Bildpunkten Breite endet dort, wo er anfaengt. */ const blatt = quadratblatt([ { x: 100, y: 100 }, { x: 180, y: 180 }, ]); expect(blatt.bildausschnitt.x).toBeCloseTo(90, 6); expect(bildzuschnitt(blatt, 90, 1000, 400, 400)).toBeNull(); }); it('meldet null bei unbrauchbaren Massen, statt zu rechnen', () => { const blatt = innen(); expect(bildzuschnitt(blatt, 0, 1000, 400, 400)).toBeNull(); expect(bildzuschnitt(blatt, 1000, 1000, 0, 400)).toBeNull(); expect(bildzuschnitt(blatt, Number.NaN, 1000, 400, 400)).toBeNull(); expect(bildzuschnitt(blatt, 1000, 1000, 400, Number.POSITIVE_INFINITY)).toBeNull(); }); it('haelt Quelle und Ziel im selben Verhaeltnis - sonst waere das Bild verzerrt', () => { const blatt = innen(); const z = bildzuschnitt(blatt, 430, 1000, 900, 400)!; const quellVerhaeltnis = z.quelleBreite / z.quelleHoehe; const zielVerhaeltnis = z.zielBreite / z.zielHoehe / (900 / blatt.breite / (400 / blatt.hoehe)); expect(zielVerhaeltnis).toBeCloseTo(quellVerhaeltnis, 6); }); });