lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ src domain geometrie planblatt.ts
| 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 | } |