lsa-planer

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

/ src domain geometrie planblatt.ts

13,4 KB Rohdatei
src/domain/geometrie/planblatt.ts — 347 Zeilen
1 import type { Meters } from '../units';
2 import { isFiniteNumber, roundTo } from '../units';
3 import type { Haltlinie, Lageplan, Planlinie } from './lageplan';
4 import { massstabVon } from './lageplan';
5 import { massstabsbalken, type Massstabsbalken, type Punkt } from './vermessung';
6
7 /**
8 * Der Lageplan als massstaebliches Blatt - alles, was zum Zeichnen noetig ist,
9 * und nichts, was mit dem Zeichnen zu tun hat.
10 *
11 * WOZU: Die Zwischenzeiten dieser Anlage beruhen auf gezeichneten Fahrlinien,
12 * und von diesen Linien stand vor der Fassung 5.32.0 keine einzige im Ausdruck.
13 * Ein Pruefer konnte einen Raeumweg von 34,2 m nicht nachvollziehen, sondern
14 * nur glauben. Das ist derselbe Gedanke, aus dem der Rechenweg der
15 * Zwischenzeiten gedruckt wird.
16 *
17 * WARUM EINE REINE FUNKTION UND KEINE ZEICHENROUTINE: Die Hausregel warnt vor
18 * zwei Zeichenroutinen fuer denselben Gegenstand, und der Altbestand ist genau
19 * daran gescheitert. Der Bildschirm zeichnet den Lageplan als EDITOR - mit
20 * Schieben, Zoomen, Greifpunkten, Treffererkennung und dem Luftbild darunter;
21 * das Blatt ist eine statische, verkleinerte Wiedergabe. Zwei verschiedene
22 * Darstellungen also, aber EINE Quelle fuer das, was sie zeigen: Diese
23 * Funktion bestimmt Abbildung, Massstab und Balken, und beide Seiten nehmen
24 * ihre Zahlen von hier.
25 *
26 * WAS SIE NICHT LIEFERT: die Bilddaten. Sie sind eine Rastergrafik von bis zu
27 * 24 MiB, und ein Blatt, das sie mitfuehrte, vervielfachte die Groesse jeder
28 * Planunterlage. Was sie liefert, ist `bildausschnitt` - die Angabe, WELCHER
29 * Teil des Bildes auf dem Blatt zu sehen waere. Damit kann die Ausgabeschicht
30 * zuschneiden und verkleinern, statt die ganze Aufnahme mitzuschleppen; die
31 * Kosten haengen danach an der Blattgroesse und nicht mehr an der Vorlage.
32 *
33 * Die Fahrlinien bleiben der Nachweis. Das Bild ist die Verortung, und dafuer
34 * gibt es ein eigenes Blatt.
35 */
36 /**
37 * Der Teil des Lageplanbilds, den das Blatt zeigt - in Bildpunkten.
38 *
39 * ER IST NICHT DER ZUSCHNITT DES GEZEICHNETEN, sondern der des ganzen Blattes
40 * einschliesslich Rand und Beifang: Was das Blatt an Flaeche hergibt, zeigt es
41 * auch vom Bild. Der Bereich kann deshalb ueber die Bildkanten hinausreichen -
42 * bei einer Zeichnung am Bildrand tut er das regelmaessig. Wer ihn verwendet,
43 * beschneidet ihn selbst (siehe `bildzuschnitt`).
44 */
45 export interface Bildausschnitt {
46 readonly x: number;
47 readonly y: number;
48 readonly breite: number;
49 readonly hoehe: number;
50 }
51
52 export interface Blattlinie {
53 readonly id: string;
54 readonly name: string;
55 readonly mode: Planlinie['mode'];
56 /** Punkte in Blattkoordinaten. */
57 readonly punkte: readonly Punkt[];
58 }
59
60 export interface Blatthaltlinie {
61 readonly id: string;
62 readonly name: string;
63 readonly art: Haltlinie['art'];
64 readonly von: Punkt;
65 readonly bis: Punkt;
66 }
67
68 /**
69 * Ein Arbeitsbereich auf dem Blatt.
70 *
71 * Er traegt seine Breite in Blattpunkten mit: Anders als eine Fahrlinie, deren
72 * Korridor auf dem Blatt nicht gezeichnet wird, IST das Band die Aussage. Wer
73 * es in fester Strichstaerke zeichnete, druckte auf jedem Massstab dieselbe
74 * Breite und damit eine falsche.
75 */
76 export interface Blattarbeitsbereich {
77 readonly id: string;
78 readonly name: string;
79 readonly punkte: readonly Punkt[];
80 /** Breite des Bandes in Blattpunkten. */
81 readonly breite: number;
82 }
83
84 /**
85 * Ein Signalgeber auf dem Blatt.
86 *
87 * Die Ausrichtung wandert unveraendert mit: Die Abbildung auf das Blatt
88 * verschiebt und verkleinert nur, sie dreht nicht (siehe `planblatt`). Ein
89 * gedrehter Blattausschnitt braeuchte hier eine Umrechnung - und es gibt
90 * keinen.
91 */
92 export interface Blattsignalgeber {
93 readonly id: string;
94 readonly name: string;
95 readonly punkt: Punkt;
96 readonly ausrichtung: number;
97 }
98
99 export interface Planblatt {
100 /** Meter je Blattpunkt - die Grundlage jeder Laengenangabe auf dem Blatt. */
101 readonly meterJeBlattpunkt: number;
102 /**
103 * Der Blattmassstab als Nenner: 250 heisst 1:250.
104 *
105 * `null`, wenn er sich nicht bilden laesst - dann fehlt die Kalibrierung.
106 * NUR EINE ZUSATZANGABE: Massgebend ist der Balken. Ein Verhaeltnis auf
107 * Papier gilt nur fuer die Blattgroesse, in der es gesetzt wurde; wer das
108 * Blatt verkleinert kopiert, liest daraus falsche Laengen ab. Der Balken
109 * verkleinert sich mit.
110 */
111 readonly massstabsnenner: number | null;
112 readonly balken: Massstabsbalken;
113 readonly linien: readonly Blattlinie[];
114 readonly haltlinien: readonly Blatthaltlinie[];
115 readonly arbeitsbereiche: readonly Blattarbeitsbereich[];
116 /** Gezeichnete Signalgeberstandorte in Blattkoordinaten. */
117 readonly signalgeber: readonly Blattsignalgeber[];
118 /**
119 * Der Teil des Bildes, der unter diesem Blatt laege.
120 *
121 * Immer gefuellt, auch wenn das Projekt gar kein Bild fuehrt: Er beschreibt
122 * eine Abbildung und nicht einen Bestand. Ein wahlfreies Feld muesste jede
123 * Lesestelle abfragen, und der Fall "kein Bild" wird ohnehin dort
124 * entschieden, wo das Bild geholt wird.
125 */
126 readonly bildausschnitt: Bildausschnitt;
127 /** Umfasste Breite und Hoehe der Zeichnung in Blattpunkten. */
128 readonly breite: number;
129 readonly hoehe: number;
130 }
131
132 /**
133 * Ein Blatt laesst sich nur zeichnen, wenn Linien UND ein Massstab vorliegen.
134 *
135 * DER ARBEITSBEREICH ZAEHLT HIER NICHT MIT, und das ist Absicht: Ein Blatt, das
136 * nur eine Baustelle zeigt und keine einzige Fahrlinie, beantwortet keine Frage
137 * der Planunterlage. Gezeichnet wird er, sobald es ein Blatt gibt.
138 */
139 export function planblattMoeglich(lageplan: Lageplan): boolean {
140 return (
141 massstabVon(lageplan) !== null && (lageplan.linien.length > 0 || lageplan.haltlinien.length > 0)
142 );
143 }
144
145 /**
146 * Bildet den Lageplan auf ein Blatt der angegebenen Groesse ab.
147 *
148 * Der Zuschnitt folgt dem GEZEICHNETEN, nicht dem Bild: Umfasst werden alle
149 * Fahrlinien und Haltlinien, nicht die Kanten des Luftbilds. Ein Anwender, der
150 * eine grosse Karte geladen und darin einen kleinen Knotenpunkt gezeichnet
151 * hat, bekaeme sonst ein Blatt, auf dem seine Zeichnung ein Fleck ist.
152 *
153 * `null`, wo sich kein Blatt bilden laesst - ohne Kalibrierung oder ohne eine
154 * einzige Linie.
155 */
156 export function planblatt(
157 lageplan: Lageplan,
158 blattBreite: number,
159 blattHoehe: number,
160 rand = 18,
161 ): Planblatt | null {
162 const massstab = massstabVon(lageplan);
163 if (massstab === null) return null;
164 if (!isFiniteNumber(blattBreite) || !isFiniteNumber(blattHoehe)) return null;
165 if (blattBreite <= 2 * rand || blattHoehe <= 2 * rand) return null;
166
167 const alle: Punkt[] = [];
168 for (const linie of lageplan.linien) alle.push(...linie.punkte);
169 for (const haltlinie of lageplan.haltlinien) alle.push(haltlinie.von, haltlinie.bis);
170 // Die Arbeitsbereiche bestimmen den Zuschnitt mit: Ein Band laengs der
171 // Fahrbahn reicht regelmaessig ueber die Haltlinien hinaus, und ragte es
172 // ueber den Blattrand, waere gerade sein Ende nicht abzulesen.
173 for (const bereich of lageplan.arbeitsbereiche) alle.push(...bereich.punkte);
174 // Und die Signalgeber (Schema 19), aus demselben Grund: Ein Geber steht
175 // neben der Fahrbahn und damit regelmaessig ausserhalb der Fahrlinien. Waere
176 // er nicht dabei, ragte er ueber den Blattrand - und ein Signalgeber, den
177 // das Blatt abschneidet, ist schlimmer als keiner.
178 for (const geber of lageplan.signalgeber) alle.push(geber.punkt);
179 if (alle.length === 0) return null;
180
181 const minX = Math.min(...alle.map((p) => p.x));
182 const maxX = Math.max(...alle.map((p) => p.x));
183 const minY = Math.min(...alle.map((p) => p.y));
184 const maxY = Math.max(...alle.map((p) => p.y));
185
186 const spanneX = Math.max(maxX - minX, 1e-6);
187 const spanneY = Math.max(maxY - minY, 1e-6);
188 const nutzBreite = blattBreite - 2 * rand;
189 const nutzHoehe = blattHoehe - 2 * rand;
190 // DERSELBE FAKTOR IN BEIDEN RICHTUNGEN. Ein Blatt mit zwei Massstaeben ist
191 // kein massstaebliches Blatt: Ein Raeumweg quer zur Fahrtrichtung liesse
192 // sich daraus nicht abgreifen.
193 const faktor = Math.min(nutzBreite / spanneX, nutzHoehe / spanneY);
194
195 const versatzX = rand + (nutzBreite - spanneX * faktor) / 2;
196 const versatzY = rand + (nutzHoehe - spanneY * faktor) / 2;
197 const auf = (p: Punkt): Punkt => ({
198 x: versatzX + (p.x - minX) * faktor,
199 y: versatzY + (p.y - minY) * faktor,
200 });
201
202 const meterJeBlattpunkt = massstab.meterJeBildpunkt / faktor;
203
204 return {
205 meterJeBlattpunkt,
206 massstabsnenner: blattmassstab(meterJeBlattpunkt),
207 // Ein Ziel von rund einem Drittel der Blattbreite: lang genug zum
208 // Abgreifen, kurz genug, um nicht in die Zeichnung zu ragen.
209 balken: massstabsbalken(meterJeBlattpunkt, nutzBreite / 3),
210 linien: lageplan.linien.map((linie) => ({
211 id: linie.id,
212 name: linie.name,
213 mode: linie.mode,
214 punkte: linie.punkte.map(auf),
215 })),
216 haltlinien: lageplan.haltlinien.map((haltlinie) => ({
217 id: haltlinie.id,
218 name: haltlinie.name,
219 art: haltlinie.art,
220 von: auf(haltlinie.von),
221 bis: auf(haltlinie.bis),
222 })),
223 arbeitsbereiche: lageplan.arbeitsbereiche
224 .filter((bereich) => bereich.punkte.length >= 2)
225 .map((bereich) => ({
226 id: bereich.id,
227 name: bereich.name,
228 punkte: bereich.punkte.map(auf),
229 // Meter in Blattpunkte: dieselbe Umrechnung wie bei jeder Laenge des
230 // Blattes, damit das Band bei jedem Massstab die richtige Breite hat.
231 breite: bereich.breiteMeter / meterJeBlattpunkt,
232 })),
233 signalgeber: lageplan.signalgeber.map((geber) => ({
234 id: geber.id,
235 name: geber.name,
236 punkt: auf(geber.punkt),
237 ausrichtung: geber.ausrichtung,
238 })),
239 /*
240 * Die Umkehrung von `auf` ueber das GANZE Blatt: Blattpunkt 0 liegt bei
241 * `minX - versatzX / faktor` im Bild, und ein Blattpunkt ist `1 / faktor`
242 * Bildpunkte breit. Damit steht der Ausschnitt in denselben Bildpunkten,
243 * in denen die Linien gespeichert sind - eine zweite Umrechnung, die
244 * auseinanderlaufen koennte, entsteht nicht.
245 */
246 bildausschnitt: {
247 x: minX - versatzX / faktor,
248 y: minY - versatzY / faktor,
249 breite: blattBreite / faktor,
250 hoehe: blattHoehe / faktor,
251 },
252 breite: blattBreite,
253 hoehe: blattHoehe,
254 };
255 }
256
257 /**
258 * Das Bild auf das Blatt: Quellbereich und Zielbereich, beide beschnitten.
259 *
260 * WOZU EINE EIGENE FUNKTION UND NICHT EIN `drawImage` MIT DEM AUSSCHNITT: Der
261 * Ausschnitt reicht regelmaessig ueber die Bildkanten hinaus. Uebergibt man
262 * ihn ungeprueft, verhalten sich die Ausgabewege verschieden - die
263 * Zeichenflaeche des Browsers beschneidet und verschiebt das Ziel dabei
264 * stillschweigend, ein PDF-Erzeuger nicht. Hier wird beides einmal gerechnet:
265 * Was vom Bild vorhanden ist, und wohin genau es auf der Zielflaeche gehoert.
266 * Der Rest der Zielflaeche bleibt leer und ist damit ehrlich - dort liegt
267 * keine Aufnahme.
268 *
269 * `null`, wo das Blatt gar nichts vom Bild zeigt. Das ist kein Fehler: Eine
270 * Zeichnung kann vollstaendig neben dem geladenen Bild liegen.
271 *
272 * Alle Masse in Bildpunkten der jeweiligen Flaeche - die Quelle im Bild, das
273 * Ziel in der Zielflaeche.
274 */
275 export interface Bildzuschnitt {
276 readonly quelleX: number;
277 readonly quelleY: number;
278 readonly quelleBreite: number;
279 readonly quelleHoehe: number;
280 readonly zielX: number;
281 readonly zielY: number;
282 readonly zielBreite: number;
283 readonly zielHoehe: number;
284 }
285
286 export function bildzuschnitt(
287 blatt: Planblatt,
288 bildBreite: number,
289 bildHoehe: number,
290 zielBreite: number,
291 zielHoehe: number,
292 ): Bildzuschnitt | null {
293 const a = blatt.bildausschnitt;
294 if (!isFiniteNumber(bildBreite) || !isFiniteNumber(bildHoehe)) return null;
295 if (!isFiniteNumber(zielBreite) || !isFiniteNumber(zielHoehe)) return null;
296 if (bildBreite <= 0 || bildHoehe <= 0 || zielBreite <= 0 || zielHoehe <= 0) return null;
297 if (a.breite <= 0 || a.hoehe <= 0) return null;
298
299 const sx = zielBreite / a.breite;
300 const sy = zielHoehe / a.hoehe;
301
302 const x0 = Math.max(a.x, 0);
303 const y0 = Math.max(a.y, 0);
304 const x1 = Math.min(a.x + a.breite, bildBreite);
305 const y1 = Math.min(a.y + a.hoehe, bildHoehe);
306 if (x1 <= x0 || y1 <= y0) return null;
307
308 return {
309 quelleX: x0,
310 quelleY: y0,
311 quelleBreite: x1 - x0,
312 quelleHoehe: y1 - y0,
313 zielX: (x0 - a.x) * sx,
314 zielY: (y0 - a.y) * sy,
315 zielBreite: (x1 - x0) * sx,
316 zielHoehe: (y1 - y0) * sy,
317 };
318 }
319
320 /**
321 * Der Blattmassstab als Nenner.
322 *
323 * Ein Punkt der Ausgabe ist 1/72 Zoll, also 25,4/72 mm. Bei
324 * `meterJeBlattpunkt` Metern je Punkt entspricht ein Millimeter auf dem Blatt
325 * `meterJeBlattpunkt * 72 / 25,4` Metern in der Wirklichkeit; der Nenner ist
326 * das Tausendfache davon.
327 *
328 * GERUNDET AUF EINE RUNDE ZAHL, und nicht, weil es huebscher aussieht: "1:250"
329 * ist eine Angabe, die ein Pruefer mit einem Massstabsdreieck nachmessen kann,
330 * "1:247" nicht. Gerundet wird IMMER NACH OBEN - der so genannte Massstab ist
331 * dann kleiner als der wirkliche, und wer aus dem Blatt eine Laenge abgreift,
332 * bekommt eher zu wenig als zu viel.
333 */
334 function blattmassstab(meterJeBlattpunkt: number): number | null {
335 if (!isFiniteNumber(meterJeBlattpunkt) || meterJeBlattpunkt <= 0) return null;
336 const roh = (meterJeBlattpunkt * 72 * 1000) / 25.4;
337 const stufen = [50, 100, 200, 250, 500, 1000, 2000, 2500, 5000, 10000];
338 const gewaehlt = stufen.find((s) => s >= roh);
339 return gewaehlt ?? Math.ceil(roh / 10000) * 10000;
340 }
341
342 /** Laenge einer Blattstrecke in Metern - fuer Prueffaelle und Beschriftungen. */
343 export function blattstreckeInMeter(blatt: Planblatt, von: Punkt, bis: Punkt): Meters {
344 const dx = bis.x - von.x;
345 const dy = bis.y - von.y;
346 return roundTo(Math.hypot(dx, dy) * blatt.meterJeBlattpunkt, 2);
347 }