// 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)), }; }