lsa-planer

LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.

/ tests domain bildausschnitt.test.ts

8,0 KB Rohdatei
tests/domain/bildausschnitt.test.ts — 218 Zeilen
1 import { describe, expect, it } from 'vitest';
2 import { bildzuschnitt, planblatt } from '@/domain/geometrie/planblatt';
3 import type { Lageplan } from '@/domain/geometrie/lageplan';
4
5 /**
6 * Welchen Teil des Luftbilds das Lageplanblatt zeigt.
7 *
8 * WOZU (aus dem Betrieb, 11.09.2026): Das gedruckte Blatt zeigte die
9 * Fahrlinien ohne Bild darunter und trug keine Angabe zur Verortung - kein
10 * Nordpfeil, kein Strassenname, keine Koordinate. Wer es allein vor sich
11 * hatte, konnte nicht sagen, wo die Zeichnung liegt.
12 *
13 * DAS BILD GANZ MITZUGEBEN WAR NIE MOEGLICH: bis zu 24 MiB je Unterlage. Der
14 * Ausweg ist der Zuschnitt - das Blatt zeigt ohnehin nur den gezeichneten
15 * Bereich, und danach haengt die Groesse an der Blattgroesse statt an der
16 * Vorlage. Diese Datei bewacht die Rechnung dahinter.
17 *
18 * DIE GEFAEHRLICHE STELLE ist nicht die Groesse, sondern die DECKUNG: Liegt
19 * das Bild um Bildpunkte verschoben unter den Linien, sieht ein Pruefer
20 * Raeumwege neben der Fahrbahn und haelt die Zeichnung fuer falsch - oder,
21 * schlimmer, er haelt sie fuer richtig und die Fahrbahn fuer woanders.
22 */
23
24 /** Ein Lageplan mit bekanntem Massstab: 100 Bildpunkte sind 20 m. */
25 function lageplan(punkte: readonly { x: number; y: number }[]): Lageplan {
26 return {
27 arbeitsbereiche: [],
28 signalgeber: [],
29 bild: null,
30 kalibrierung: {
31 von: { x: 0, y: 0 },
32 bis: { x: 100, y: 0 },
33 laengeMeter: 20,
34 herkunft: 'georeferenziert',
35 },
36 haltlinien: [],
37 linien: [
38 {
39 id: 'l1',
40 name: 'K1',
41 mode: 'kfz',
42 movement: 'geradeaus',
43 breiteMeter: 3,
44 punkte,
45 haltlinieId: null,
46 startT: 0,
47 signalGroupId: null,
48 },
49 ],
50 };
51 }
52
53 /** Ein quadratisches Blatt macht die Rechnung ohne Verzerrung nachvollziehbar. */
54 function quadratblatt(punkte: readonly { x: number; y: number }[]) {
55 const blatt = planblatt(lageplan(punkte), 200, 200, 20);
56 if (blatt === null) throw new Error('Blatt erwartet');
57 return blatt;
58 }
59
60 describe('Der Bildausschnitt ist die Umkehrung der Abbildung', () => {
61 it('umfasst mehr als das Gezeichnete - naemlich das ganze Blatt', () => {
62 // Gezeichnet sind 100 Bildpunkte, das Blatt ist 200 Punkte breit mit 20
63 // Punkten Rand: Der Faktor ist 160/100 = 1,6, und das ganze Blatt deckt
64 // 200/1,6 = 125 Bildpunkte ab.
65 const blatt = quadratblatt([
66 { x: 0, y: 0 },
67 { x: 100, y: 100 },
68 ]);
69 expect(blatt.bildausschnitt.breite).toBeCloseTo(125, 6);
70 expect(blatt.bildausschnitt.hoehe).toBeCloseTo(125, 6);
71 });
72
73 it('beginnt um den Rand VOR dem Gezeichneten', () => {
74 const blatt = quadratblatt([
75 { x: 40, y: 60 },
76 { x: 140, y: 160 },
77 ]);
78 // 20 Blattpunkte Rand sind 20/1,6 = 12,5 Bildpunkte.
79 expect(blatt.bildausschnitt.x).toBeCloseTo(40 - 12.5, 6);
80 expect(blatt.bildausschnitt.y).toBeCloseTo(60 - 12.5, 6);
81 });
82
83 it('bildet jeden Blattpunkt auf denselben Bildpunkt ab wie die Linien', () => {
84 /*
85 * DER EIGENTLICHE NACHWEIS DER DECKUNG. Wo eine Linie auf dem Blatt liegt,
86 * muss der Ausschnitt denselben Bildpunkt zeigen - sonst liegt das Bild
87 * gegen die Zeichnung verschoben.
88 */
89 const roh = [
90 { x: 10, y: 20 },
91 { x: 210, y: 120 },
92 ];
93 const blatt = quadratblatt(roh);
94 const a = blatt.bildausschnitt;
95 for (const [i, punkt] of blatt.linien[0]!.punkte.entries()) {
96 const zurueck = {
97 x: a.x + (punkt.x / blatt.breite) * a.breite,
98 y: a.y + (punkt.y / blatt.hoehe) * a.hoehe,
99 };
100 expect(zurueck.x).toBeCloseTo(roh[i]!.x, 6);
101 expect(zurueck.y).toBeCloseTo(roh[i]!.y, 6);
102 }
103 });
104
105 it('steht auch dann bereit, wenn das Projekt gar kein Bild fuehrt', () => {
106 // Er beschreibt eine Abbildung, keinen Bestand. Ein wahlfreies Feld
107 // muesste jede Lesestelle abfragen.
108 const blatt = quadratblatt([
109 { x: 0, y: 0 },
110 { x: 50, y: 50 },
111 ]);
112 expect(blatt.bildausschnitt.breite).toBeGreaterThan(0);
113 expect(blatt.bildausschnitt.hoehe).toBeGreaterThan(0);
114 });
115 });
116
117 describe('Der Zuschnitt beschneidet an den Bildkanten', () => {
118 /** Zeichnung mitten in einem grossen Bild - nichts ragt hinaus. */
119 const innen = () =>
120 quadratblatt([
121 { x: 400, y: 400 },
122 { x: 500, y: 500 },
123 ]);
124
125 it('nimmt bei einer Zeichnung mitten im Bild den ganzen Zielbereich ein', () => {
126 const z = bildzuschnitt(innen(), 1000, 1000, 400, 400);
127 expect(z).not.toBeNull();
128 expect(z!.zielX).toBeCloseTo(0, 6);
129 expect(z!.zielY).toBeCloseTo(0, 6);
130 expect(z!.zielBreite).toBeCloseTo(400, 6);
131 expect(z!.zielHoehe).toBeCloseTo(400, 6);
132 });
133
134 it('rechnet den Quellbereich in Bildpunkte des Bildes um', () => {
135 const blatt = innen();
136 const z = bildzuschnitt(blatt, 1000, 1000, 400, 400)!;
137 expect(z.quelleX).toBeCloseTo(blatt.bildausschnitt.x, 6);
138 expect(z.quelleBreite).toBeCloseTo(blatt.bildausschnitt.breite, 6);
139 });
140
141 it('schneidet ab, was ueber die linke obere Bildkante hinausragt', () => {
142 /*
143 * DER REGELFALL BEI EINER ZEICHNUNG AM BILDRAND. Ohne diese Rechnung
144 * verhielten sich die Ausgabewege verschieden: Die Zeichenflaeche des
145 * Browsers beschneidet und verschiebt das Ziel stillschweigend mit, ein
146 * PDF-Erzeuger nicht - und das Bild laege dann verschoben unter den Linien.
147 */
148 const blatt = quadratblatt([
149 { x: 0, y: 0 },
150 { x: 100, y: 100 },
151 ]);
152 // Der Ausschnitt beginnt bei -12,5 - also 12,5 von 125 Bildpunkten
153 // ausserhalb, das sind 10 Prozent.
154 const z = bildzuschnitt(blatt, 1000, 1000, 400, 400)!;
155 expect(z.quelleX).toBe(0);
156 expect(z.quelleY).toBe(0);
157 expect(z.zielX).toBeCloseTo(40, 6);
158 expect(z.zielY).toBeCloseTo(40, 6);
159 expect(z.zielBreite).toBeCloseTo(360, 6);
160 });
161
162 it('schneidet ab, was ueber die rechte untere Bildkante hinausragt', () => {
163 const blatt = quadratblatt([
164 { x: 0, y: 0 },
165 { x: 100, y: 100 },
166 ]);
167 // Bild nur 100 x 100 gross: Der Ausschnitt reicht bis 112,5.
168 const z = bildzuschnitt(blatt, 100, 100, 400, 400)!;
169 expect(z.quelleBreite).toBeCloseTo(100, 6);
170 expect(z.zielX).toBeCloseTo(40, 6);
171 expect(z.zielBreite).toBeCloseTo(320, 6);
172 expect(z.zielX + z.zielBreite).toBeLessThan(400);
173 });
174
175 it('meldet null, wo das Blatt gar nichts vom Bild zeigt', () => {
176 // Kein Fehler: Eine Zeichnung kann vollstaendig neben dem Bild liegen.
177 const blatt = quadratblatt([
178 { x: 5000, y: 5000 },
179 { x: 5100, y: 5100 },
180 ]);
181 expect(bildzuschnitt(blatt, 100, 100, 400, 400)).toBeNull();
182 });
183
184 it('meldet null, wo das Blatt das Bild nur an der Kante beruehrt', () => {
185 /*
186 * Ein Ausschnitt der Breite null ist kein Ausschnitt. Wuerde er
187 * durchgelassen, ginge ein `drawImage` mit Breite 0 an die Zeichenflaeche
188 * und ein Bild ohne Ausdehnung in das PDF - je nach Ausgabeweg ein
189 * unsichtbarer Fehlschlag oder eine Ausnahme mitten im Erzeugen der
190 * Unterlage.
191 *
192 * Gezeichnet ist von 100 bis 180, der Ausschnitt beginnt deshalb bei 90.
193 * Ein Bild von genau 90 Bildpunkten Breite endet dort, wo er anfaengt.
194 */
195 const blatt = quadratblatt([
196 { x: 100, y: 100 },
197 { x: 180, y: 180 },
198 ]);
199 expect(blatt.bildausschnitt.x).toBeCloseTo(90, 6);
200 expect(bildzuschnitt(blatt, 90, 1000, 400, 400)).toBeNull();
201 });
202
203 it('meldet null bei unbrauchbaren Massen, statt zu rechnen', () => {
204 const blatt = innen();
205 expect(bildzuschnitt(blatt, 0, 1000, 400, 400)).toBeNull();
206 expect(bildzuschnitt(blatt, 1000, 1000, 0, 400)).toBeNull();
207 expect(bildzuschnitt(blatt, Number.NaN, 1000, 400, 400)).toBeNull();
208 expect(bildzuschnitt(blatt, 1000, 1000, 400, Number.POSITIVE_INFINITY)).toBeNull();
209 });
210
211 it('haelt Quelle und Ziel im selben Verhaeltnis - sonst waere das Bild verzerrt', () => {
212 const blatt = innen();
213 const z = bildzuschnitt(blatt, 430, 1000, 900, 400)!;
214 const quellVerhaeltnis = z.quelleBreite / z.quelleHoehe;
215 const zielVerhaeltnis = z.zielBreite / z.zielHoehe / (900 / blatt.breite / (400 / blatt.hoehe));
216 expect(zielVerhaeltnis).toBeCloseTo(quellVerhaeltnis, 6);
217 });
218 });