import type { Meters } from '../units'; import { isFiniteNumber, roundTo } from '../units'; import type { Haltlinie, Lageplan, Planlinie } from './lageplan'; import { massstabVon } from './lageplan'; import { massstabsbalken, type Massstabsbalken, type Punkt } from './vermessung'; /** * Der Lageplan als massstaebliches Blatt - alles, was zum Zeichnen noetig ist, * und nichts, was mit dem Zeichnen zu tun hat. * * WOZU: Die Zwischenzeiten dieser Anlage beruhen auf gezeichneten Fahrlinien, * und von diesen Linien stand vor der Fassung 5.32.0 keine einzige im Ausdruck. * Ein Pruefer konnte einen Raeumweg von 34,2 m nicht nachvollziehen, sondern * nur glauben. Das ist derselbe Gedanke, aus dem der Rechenweg der * Zwischenzeiten gedruckt wird. * * WARUM EINE REINE FUNKTION UND KEINE ZEICHENROUTINE: Die Hausregel warnt vor * zwei Zeichenroutinen fuer denselben Gegenstand, und der Altbestand ist genau * daran gescheitert. Der Bildschirm zeichnet den Lageplan als EDITOR - mit * Schieben, Zoomen, Greifpunkten, Treffererkennung und dem Luftbild darunter; * das Blatt ist eine statische, verkleinerte Wiedergabe. Zwei verschiedene * Darstellungen also, aber EINE Quelle fuer das, was sie zeigen: Diese * Funktion bestimmt Abbildung, Massstab und Balken, und beide Seiten nehmen * ihre Zahlen von hier. * * WAS SIE NICHT LIEFERT: die Bilddaten. Sie sind eine Rastergrafik von bis zu * 24 MiB, und ein Blatt, das sie mitfuehrte, vervielfachte die Groesse jeder * Planunterlage. Was sie liefert, ist `bildausschnitt` - die Angabe, WELCHER * Teil des Bildes auf dem Blatt zu sehen waere. Damit kann die Ausgabeschicht * zuschneiden und verkleinern, statt die ganze Aufnahme mitzuschleppen; die * Kosten haengen danach an der Blattgroesse und nicht mehr an der Vorlage. * * Die Fahrlinien bleiben der Nachweis. Das Bild ist die Verortung, und dafuer * gibt es ein eigenes Blatt. */ /** * Der Teil des Lageplanbilds, den das Blatt zeigt - in Bildpunkten. * * ER IST NICHT DER ZUSCHNITT DES GEZEICHNETEN, sondern der des ganzen Blattes * einschliesslich Rand und Beifang: Was das Blatt an Flaeche hergibt, zeigt es * auch vom Bild. Der Bereich kann deshalb ueber die Bildkanten hinausreichen - * bei einer Zeichnung am Bildrand tut er das regelmaessig. Wer ihn verwendet, * beschneidet ihn selbst (siehe `bildzuschnitt`). */ export interface Bildausschnitt { readonly x: number; readonly y: number; readonly breite: number; readonly hoehe: number; } export interface Blattlinie { readonly id: string; readonly name: string; readonly mode: Planlinie['mode']; /** Punkte in Blattkoordinaten. */ readonly punkte: readonly Punkt[]; } export interface Blatthaltlinie { readonly id: string; readonly name: string; readonly art: Haltlinie['art']; readonly von: Punkt; readonly bis: Punkt; } /** * Ein Arbeitsbereich auf dem Blatt. * * Er traegt seine Breite in Blattpunkten mit: Anders als eine Fahrlinie, deren * Korridor auf dem Blatt nicht gezeichnet wird, IST das Band die Aussage. Wer * es in fester Strichstaerke zeichnete, druckte auf jedem Massstab dieselbe * Breite und damit eine falsche. */ export interface Blattarbeitsbereich { readonly id: string; readonly name: string; readonly punkte: readonly Punkt[]; /** Breite des Bandes in Blattpunkten. */ readonly breite: number; } /** * Ein Signalgeber auf dem Blatt. * * Die Ausrichtung wandert unveraendert mit: Die Abbildung auf das Blatt * verschiebt und verkleinert nur, sie dreht nicht (siehe `planblatt`). Ein * gedrehter Blattausschnitt braeuchte hier eine Umrechnung - und es gibt * keinen. */ export interface Blattsignalgeber { readonly id: string; readonly name: string; readonly punkt: Punkt; readonly ausrichtung: number; } export interface Planblatt { /** Meter je Blattpunkt - die Grundlage jeder Laengenangabe auf dem Blatt. */ readonly meterJeBlattpunkt: number; /** * Der Blattmassstab als Nenner: 250 heisst 1:250. * * `null`, wenn er sich nicht bilden laesst - dann fehlt die Kalibrierung. * NUR EINE ZUSATZANGABE: Massgebend ist der Balken. Ein Verhaeltnis auf * Papier gilt nur fuer die Blattgroesse, in der es gesetzt wurde; wer das * Blatt verkleinert kopiert, liest daraus falsche Laengen ab. Der Balken * verkleinert sich mit. */ readonly massstabsnenner: number | null; readonly balken: Massstabsbalken; readonly linien: readonly Blattlinie[]; readonly haltlinien: readonly Blatthaltlinie[]; readonly arbeitsbereiche: readonly Blattarbeitsbereich[]; /** Gezeichnete Signalgeberstandorte in Blattkoordinaten. */ readonly signalgeber: readonly Blattsignalgeber[]; /** * Der Teil des Bildes, der unter diesem Blatt laege. * * Immer gefuellt, auch wenn das Projekt gar kein Bild fuehrt: Er beschreibt * eine Abbildung und nicht einen Bestand. Ein wahlfreies Feld muesste jede * Lesestelle abfragen, und der Fall "kein Bild" wird ohnehin dort * entschieden, wo das Bild geholt wird. */ readonly bildausschnitt: Bildausschnitt; /** Umfasste Breite und Hoehe der Zeichnung in Blattpunkten. */ readonly breite: number; readonly hoehe: number; } /** * Ein Blatt laesst sich nur zeichnen, wenn Linien UND ein Massstab vorliegen. * * DER ARBEITSBEREICH ZAEHLT HIER NICHT MIT, und das ist Absicht: Ein Blatt, das * nur eine Baustelle zeigt und keine einzige Fahrlinie, beantwortet keine Frage * der Planunterlage. Gezeichnet wird er, sobald es ein Blatt gibt. */ export function planblattMoeglich(lageplan: Lageplan): boolean { return ( massstabVon(lageplan) !== null && (lageplan.linien.length > 0 || lageplan.haltlinien.length > 0) ); } /** * Bildet den Lageplan auf ein Blatt der angegebenen Groesse ab. * * Der Zuschnitt folgt dem GEZEICHNETEN, nicht dem Bild: Umfasst werden alle * Fahrlinien und Haltlinien, nicht die Kanten des Luftbilds. Ein Anwender, der * eine grosse Karte geladen und darin einen kleinen Knotenpunkt gezeichnet * hat, bekaeme sonst ein Blatt, auf dem seine Zeichnung ein Fleck ist. * * `null`, wo sich kein Blatt bilden laesst - ohne Kalibrierung oder ohne eine * einzige Linie. */ export function planblatt( lageplan: Lageplan, blattBreite: number, blattHoehe: number, rand = 18, ): Planblatt | null { const massstab = massstabVon(lageplan); if (massstab === null) return null; if (!isFiniteNumber(blattBreite) || !isFiniteNumber(blattHoehe)) return null; if (blattBreite <= 2 * rand || blattHoehe <= 2 * rand) return null; const alle: Punkt[] = []; for (const linie of lageplan.linien) alle.push(...linie.punkte); for (const haltlinie of lageplan.haltlinien) alle.push(haltlinie.von, haltlinie.bis); // Die Arbeitsbereiche bestimmen den Zuschnitt mit: Ein Band laengs der // Fahrbahn reicht regelmaessig ueber die Haltlinien hinaus, und ragte es // ueber den Blattrand, waere gerade sein Ende nicht abzulesen. for (const bereich of lageplan.arbeitsbereiche) alle.push(...bereich.punkte); // Und die Signalgeber (Schema 19), aus demselben Grund: Ein Geber steht // neben der Fahrbahn und damit regelmaessig ausserhalb der Fahrlinien. Waere // er nicht dabei, ragte er ueber den Blattrand - und ein Signalgeber, den // das Blatt abschneidet, ist schlimmer als keiner. for (const geber of lageplan.signalgeber) alle.push(geber.punkt); if (alle.length === 0) return null; const minX = Math.min(...alle.map((p) => p.x)); const maxX = Math.max(...alle.map((p) => p.x)); const minY = Math.min(...alle.map((p) => p.y)); const maxY = Math.max(...alle.map((p) => p.y)); const spanneX = Math.max(maxX - minX, 1e-6); const spanneY = Math.max(maxY - minY, 1e-6); const nutzBreite = blattBreite - 2 * rand; const nutzHoehe = blattHoehe - 2 * rand; // DERSELBE FAKTOR IN BEIDEN RICHTUNGEN. Ein Blatt mit zwei Massstaeben ist // kein massstaebliches Blatt: Ein Raeumweg quer zur Fahrtrichtung liesse // sich daraus nicht abgreifen. const faktor = Math.min(nutzBreite / spanneX, nutzHoehe / spanneY); const versatzX = rand + (nutzBreite - spanneX * faktor) / 2; const versatzY = rand + (nutzHoehe - spanneY * faktor) / 2; const auf = (p: Punkt): Punkt => ({ x: versatzX + (p.x - minX) * faktor, y: versatzY + (p.y - minY) * faktor, }); const meterJeBlattpunkt = massstab.meterJeBildpunkt / faktor; return { meterJeBlattpunkt, massstabsnenner: blattmassstab(meterJeBlattpunkt), // Ein Ziel von rund einem Drittel der Blattbreite: lang genug zum // Abgreifen, kurz genug, um nicht in die Zeichnung zu ragen. balken: massstabsbalken(meterJeBlattpunkt, nutzBreite / 3), linien: lageplan.linien.map((linie) => ({ id: linie.id, name: linie.name, mode: linie.mode, punkte: linie.punkte.map(auf), })), haltlinien: lageplan.haltlinien.map((haltlinie) => ({ id: haltlinie.id, name: haltlinie.name, art: haltlinie.art, von: auf(haltlinie.von), bis: auf(haltlinie.bis), })), arbeitsbereiche: lageplan.arbeitsbereiche .filter((bereich) => bereich.punkte.length >= 2) .map((bereich) => ({ id: bereich.id, name: bereich.name, punkte: bereich.punkte.map(auf), // Meter in Blattpunkte: dieselbe Umrechnung wie bei jeder Laenge des // Blattes, damit das Band bei jedem Massstab die richtige Breite hat. breite: bereich.breiteMeter / meterJeBlattpunkt, })), signalgeber: lageplan.signalgeber.map((geber) => ({ id: geber.id, name: geber.name, punkt: auf(geber.punkt), ausrichtung: geber.ausrichtung, })), /* * Die Umkehrung von `auf` ueber das GANZE Blatt: Blattpunkt 0 liegt bei * `minX - versatzX / faktor` im Bild, und ein Blattpunkt ist `1 / faktor` * Bildpunkte breit. Damit steht der Ausschnitt in denselben Bildpunkten, * in denen die Linien gespeichert sind - eine zweite Umrechnung, die * auseinanderlaufen koennte, entsteht nicht. */ bildausschnitt: { x: minX - versatzX / faktor, y: minY - versatzY / faktor, breite: blattBreite / faktor, hoehe: blattHoehe / faktor, }, breite: blattBreite, hoehe: blattHoehe, }; } /** * Das Bild auf das Blatt: Quellbereich und Zielbereich, beide beschnitten. * * WOZU EINE EIGENE FUNKTION UND NICHT EIN `drawImage` MIT DEM AUSSCHNITT: Der * Ausschnitt reicht regelmaessig ueber die Bildkanten hinaus. Uebergibt man * ihn ungeprueft, verhalten sich die Ausgabewege verschieden - die * Zeichenflaeche des Browsers beschneidet und verschiebt das Ziel dabei * stillschweigend, ein PDF-Erzeuger nicht. Hier wird beides einmal gerechnet: * Was vom Bild vorhanden ist, und wohin genau es auf der Zielflaeche gehoert. * Der Rest der Zielflaeche bleibt leer und ist damit ehrlich - dort liegt * keine Aufnahme. * * `null`, wo das Blatt gar nichts vom Bild zeigt. Das ist kein Fehler: Eine * Zeichnung kann vollstaendig neben dem geladenen Bild liegen. * * Alle Masse in Bildpunkten der jeweiligen Flaeche - die Quelle im Bild, das * Ziel in der Zielflaeche. */ export interface Bildzuschnitt { readonly quelleX: number; readonly quelleY: number; readonly quelleBreite: number; readonly quelleHoehe: number; readonly zielX: number; readonly zielY: number; readonly zielBreite: number; readonly zielHoehe: number; } export function bildzuschnitt( blatt: Planblatt, bildBreite: number, bildHoehe: number, zielBreite: number, zielHoehe: number, ): Bildzuschnitt | null { const a = blatt.bildausschnitt; if (!isFiniteNumber(bildBreite) || !isFiniteNumber(bildHoehe)) return null; if (!isFiniteNumber(zielBreite) || !isFiniteNumber(zielHoehe)) return null; if (bildBreite <= 0 || bildHoehe <= 0 || zielBreite <= 0 || zielHoehe <= 0) return null; if (a.breite <= 0 || a.hoehe <= 0) return null; const sx = zielBreite / a.breite; const sy = zielHoehe / a.hoehe; const x0 = Math.max(a.x, 0); const y0 = Math.max(a.y, 0); const x1 = Math.min(a.x + a.breite, bildBreite); const y1 = Math.min(a.y + a.hoehe, bildHoehe); if (x1 <= x0 || y1 <= y0) return null; return { quelleX: x0, quelleY: y0, quelleBreite: x1 - x0, quelleHoehe: y1 - y0, zielX: (x0 - a.x) * sx, zielY: (y0 - a.y) * sy, zielBreite: (x1 - x0) * sx, zielHoehe: (y1 - y0) * sy, }; } /** * Der Blattmassstab als Nenner. * * Ein Punkt der Ausgabe ist 1/72 Zoll, also 25,4/72 mm. Bei * `meterJeBlattpunkt` Metern je Punkt entspricht ein Millimeter auf dem Blatt * `meterJeBlattpunkt * 72 / 25,4` Metern in der Wirklichkeit; der Nenner ist * das Tausendfache davon. * * GERUNDET AUF EINE RUNDE ZAHL, und nicht, weil es huebscher aussieht: "1:250" * ist eine Angabe, die ein Pruefer mit einem Massstabsdreieck nachmessen kann, * "1:247" nicht. Gerundet wird IMMER NACH OBEN - der so genannte Massstab ist * dann kleiner als der wirkliche, und wer aus dem Blatt eine Laenge abgreift, * bekommt eher zu wenig als zu viel. */ function blattmassstab(meterJeBlattpunkt: number): number | null { if (!isFiniteNumber(meterJeBlattpunkt) || meterJeBlattpunkt <= 0) return null; const roh = (meterJeBlattpunkt * 72 * 1000) / 25.4; const stufen = [50, 100, 200, 250, 500, 1000, 2000, 2500, 5000, 10000]; const gewaehlt = stufen.find((s) => s >= roh); return gewaehlt ?? Math.ceil(roh / 10000) * 10000; } /** Laenge einer Blattstrecke in Metern - fuer Prueffaelle und Beschriftungen. */ export function blattstreckeInMeter(blatt: Planblatt, von: Punkt, bis: Punkt): Meters { const dx = bis.x - von.x; const dy = bis.y - von.y; return roundTo(Math.hypot(dx, dy) * blatt.meterJeBlattpunkt, 2); }