3d2d4d6321
Neuer GPU-Renderer fuer den Grundriss (src/plan/glPlan/): Earcut-Tessellierung (konkav-faehig), gehrte Linienzuege (Miter), echte Papier-mm-Strichbreiten im Massstab (repliziert den SVG-printStrokeVb-Pfad), Hybrid mit scharfem SVG-Text- Overlay. GPU ist der Standardpfad; der SVG-Renderer bleibt automatischer Fallback, falls WebGL2/Shader nicht verfuegbar sind. Imperativer Pan (rAF + CSS-transform) fuer fluessige Interaktion ohne React-Re-Render je Frame. Enthaelt zudem den bisher nicht committeten Arbeitsstand des Browser-BIM (Oeffnungen, Treppen, Raeume, Decken, DXF-Export, Materialbibliothek, Kontext- Import, Tauri-Compute-Boundary-PoC).
183 lines
5.1 KiB
TypeScript
183 lines
5.1 KiB
TypeScript
// LV95 ↔ WGS84 Koordinaten-Transformation + Ursprungs-Verschiebung.
|
|
//
|
|
// Schweizer Landeskoordinaten (LV95 / EPSG:2056) liegen im Millionenbereich
|
|
// (Ost ~2'600'000 m, Nord ~1'200'000 m). Für die numerische Genauigkeit im
|
|
// CAD (float32 in three.js!) verschieben wir importierte Geometrie in die Nähe
|
|
// des Modell-Ursprungs (0,0). Der einmal berechnete Offset wird mitgeführt, so
|
|
// dass ALLE Objekte eines Imports dieselbe Verschiebung teilen und korrekt
|
|
// zueinander liegen.
|
|
//
|
|
// Die Umrechnung LV95↔WGS84 verwendet die von swisstopo publizierten
|
|
// Näherungsformeln („Bezugsrahmenwechsel und Kartenprojektionen", genauigkeit
|
|
// im Meter-Bereich, für Kontext-Geometrie völlig ausreichend). Referenz-
|
|
// Rundlauf (siehe Unit-Check im Report) < 1 m.
|
|
//
|
|
// Bezeichner englisch, Kommentare deutsch (CONVENTIONS.md). Einheiten: Meter
|
|
// bzw. Dezimalgrad.
|
|
|
|
/** Geographische Koordinate (WGS84), Dezimalgrad. */
|
|
export interface LatLon {
|
|
lat: number;
|
|
lon: number;
|
|
}
|
|
|
|
/** Schweizer Landeskoordinate LV95 (EPSG:2056), Meter. */
|
|
export interface LV95 {
|
|
/** Ostwert (E), ~2'600'000. */
|
|
e: number;
|
|
/** Nordwert (N), ~1'200'000. */
|
|
n: number;
|
|
}
|
|
|
|
/**
|
|
* WGS84 → LV95 (approximativ, swisstopo-Näherungsformeln).
|
|
* Eingabe Dezimalgrad, Ausgabe Meter (E/N).
|
|
*/
|
|
export function wgs84ToLv95(lat: number, lon: number): LV95 {
|
|
// Breite/Länge in Sexagesimalsekunden.
|
|
const phi = lat * 3600;
|
|
const lam = lon * 3600;
|
|
// Hilfsgrößen relativ zum Bezugspunkt Bern (46°57'08.66" N, 7°26'22.50" E).
|
|
const p = (phi - 169028.66) / 10000;
|
|
const l = (lam - 26782.5) / 10000;
|
|
const p2 = p * p;
|
|
const l2 = l * l;
|
|
|
|
// Ostwert (Y in LV03 + 2'000'000 für LV95).
|
|
const y =
|
|
2600072.37 +
|
|
211455.93 * l -
|
|
10938.51 * l * p -
|
|
0.36 * l * p2 -
|
|
44.54 * l * l2;
|
|
|
|
// Nordwert (X in LV03 + 1'000'000 für LV95).
|
|
const x =
|
|
1200147.07 +
|
|
308807.95 * p +
|
|
3745.25 * l2 +
|
|
76.63 * p2 -
|
|
194.56 * l2 * p +
|
|
119.79 * p * p2;
|
|
|
|
return { e: y, n: x };
|
|
}
|
|
|
|
/**
|
|
* LV95 → WGS84 (approximativ, swisstopo-Näherungsformeln).
|
|
* Eingabe Meter (E/N), Ausgabe Dezimalgrad.
|
|
*/
|
|
export function lv95ToWgs84(e: number, n: number): LatLon {
|
|
// Relativ zum Projektions-Ursprung, in 1'000'000-Einheiten.
|
|
const y = (e - 2600000) / 1000000;
|
|
const x = (n - 1200000) / 1000000;
|
|
const y2 = y * y;
|
|
const x2 = x * x;
|
|
|
|
// Breite/Länge in 10'000"-Einheiten (Näherungspolynome).
|
|
const lam =
|
|
2.6779094 +
|
|
4.728982 * y +
|
|
0.791484 * y * x +
|
|
0.1306 * y * x2 -
|
|
0.0436 * y * y2;
|
|
|
|
const phi =
|
|
16.9023892 +
|
|
3.238272 * x -
|
|
0.270978 * y2 -
|
|
0.002528 * x2 -
|
|
0.0447 * y2 * x -
|
|
0.014 * x * x2;
|
|
|
|
// Zurück in Dezimalgrad (Faktor 100/36).
|
|
return {
|
|
lat: (phi * 100) / 36,
|
|
lon: (lam * 100) / 36,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Eine Ursprungs-Verschiebung: der LV95-Punkt, der im Modell auf (0,0)
|
|
* abgebildet wird. Alle importierten Objekte eines Imports teilen dieselbe
|
|
* Origin, damit sie zueinander lagerichtig bleiben.
|
|
*/
|
|
export interface GeoOrigin {
|
|
e: number;
|
|
n: number;
|
|
}
|
|
|
|
/** Erzeugt eine Origin aus einem LV95-Zentrum. */
|
|
export function makeOrigin(center: LV95): GeoOrigin {
|
|
return { e: center.e, n: center.n };
|
|
}
|
|
|
|
/**
|
|
* LV95 → lokale Modell-Meter (Ost→X, Nord→Y) relativ zur Origin. LV95 ist bereits
|
|
* metrisch und (nahezu) nordorientiert, daher genügt eine reine Translation.
|
|
*/
|
|
export function lv95ToLocal(pt: LV95, origin: GeoOrigin): { x: number; y: number } {
|
|
return { x: pt.e - origin.e, y: pt.n - origin.n };
|
|
}
|
|
|
|
/** Lokale Modell-Meter → LV95 (Umkehrung von {@link lv95ToLocal}). */
|
|
export function localToLv95(
|
|
x: number,
|
|
y: number,
|
|
origin: GeoOrigin,
|
|
): LV95 {
|
|
return { e: x + origin.e, n: y + origin.n };
|
|
}
|
|
|
|
/**
|
|
* WGS84 → lokale Modell-Meter (über LV95 und die Origin). Praktisch für
|
|
* OSM-Punkte (lon/lat), die zuerst nach LV95 projiziert und dann verschoben
|
|
* werden — so teilen OSM- und swisstopo-Import exakt dasselbe Bezugssystem.
|
|
*/
|
|
export function wgs84ToLocal(
|
|
lat: number,
|
|
lon: number,
|
|
origin: GeoOrigin,
|
|
): { x: number; y: number } {
|
|
return lv95ToLocal(wgs84ToLv95(lat, lon), origin);
|
|
}
|
|
|
|
/**
|
|
* Baut eine LV95-Bounding-Box (quadratisch) um ein Zentrum mit gegebenem Radius
|
|
* (Meter). Reihenfolge [eMin, nMin, eMax, nMax].
|
|
*/
|
|
export function bboxAround(
|
|
center: LV95,
|
|
radius: number,
|
|
): [number, number, number, number] {
|
|
return [
|
|
center.e - radius,
|
|
center.n - radius,
|
|
center.e + radius,
|
|
center.n + radius,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Wandelt eine LV95-Bounding-Box in eine WGS84-Box (für Overpass:
|
|
* [south, west, north, east]). Nutzt die vier Ecken und nimmt die Extremwerte,
|
|
* damit die leichte Nord-Rotation der LV95-Projektion abgedeckt ist.
|
|
*/
|
|
export function lv95BboxToWgs84(
|
|
bbox: [number, number, number, number],
|
|
): { south: number; west: number; north: number; east: number } {
|
|
const [eMin, nMin, eMax, nMax] = bbox;
|
|
const corners: LatLon[] = [
|
|
lv95ToWgs84(eMin, nMin),
|
|
lv95ToWgs84(eMax, nMin),
|
|
lv95ToWgs84(eMin, nMax),
|
|
lv95ToWgs84(eMax, nMax),
|
|
];
|
|
return {
|
|
south: Math.min(...corners.map((c) => c.lat)),
|
|
west: Math.min(...corners.map((c) => c.lon)),
|
|
north: Math.max(...corners.map((c) => c.lat)),
|
|
east: Math.max(...corners.map((c) => c.lon)),
|
|
};
|
|
}
|