2D-Plan-Renderer auf WebGL2 (GPU) + akkumulierter Funktionsstand
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).
This commit is contained in:
+139
@@ -0,0 +1,139 @@
|
||||
// OpenStreetMap-Anbindung (Overpass API) für den Standort-Import.
|
||||
//
|
||||
// Holt für eine LV95-Box Gebäude / Strassen / Wasser / Grünflächen und wandelt
|
||||
// sie in Polylinien in LOKALEN Modell-Metern (über LV95 + Origin, siehe
|
||||
// lv95.ts). Overpass sendet je nach Mirror unzuverlässige CORS-Header, daher
|
||||
// läuft der Abruf über den openbureau-core Geo-Proxy (`viaProxy`), sofern
|
||||
// `VITE_GEO_PROXY` gesetzt ist — sonst Direktabruf gegen den CORS-fähigen
|
||||
// Mirror.
|
||||
//
|
||||
// Bezeichner englisch, Kommentare deutsch (CONVENTIONS.md). Einheiten: Meter.
|
||||
|
||||
import { lv95BboxToWgs84, makeOrigin, wgs84ToLocal } from "./lv95";
|
||||
import type { GeoOrigin, LV95 } from "./lv95";
|
||||
import { bboxAround } from "./lv95";
|
||||
import type { GeoCategory, GeoFeature } from "./geoContext";
|
||||
import { viaProxy } from "./geoContext";
|
||||
|
||||
// Direkter Mirror mit CORS `*` (Fallback, falls kein Proxy konfiguriert ist).
|
||||
const OVERPASS = "https://maps.mail.ru/osm/tools/overpass/api/interpreter";
|
||||
|
||||
/** Welche OSM-Kategorien der Aufrufer möchte. */
|
||||
export interface OsmSelection {
|
||||
buildings: boolean;
|
||||
roads: boolean;
|
||||
water: boolean;
|
||||
green: boolean;
|
||||
}
|
||||
|
||||
/** Ein Overpass-Element mit inline-Geometrie (`out geom`). */
|
||||
interface OverpassElement {
|
||||
type: "node" | "way" | "relation";
|
||||
id: number;
|
||||
tags?: Record<string, string>;
|
||||
geometry?: { lat: number; lon: number }[];
|
||||
members?: {
|
||||
type: string;
|
||||
role: string;
|
||||
geometry?: { lat: number; lon: number }[];
|
||||
}[];
|
||||
}
|
||||
|
||||
/** Baut die Overpass-QL-Abfrage aus der WGS84-Box + Auswahl. */
|
||||
function buildQuery(
|
||||
bbox: { south: number; west: number; north: number; east: number },
|
||||
sel: OsmSelection,
|
||||
): string {
|
||||
const b = `${bbox.south},${bbox.west},${bbox.north},${bbox.east}`;
|
||||
const parts: string[] = [];
|
||||
if (sel.buildings) parts.push(`way["building"](${b});`);
|
||||
if (sel.roads) parts.push(`way["highway"](${b});`);
|
||||
if (sel.water) {
|
||||
parts.push(`way["natural"="water"](${b});`);
|
||||
parts.push(`way["waterway"](${b});`);
|
||||
parts.push(`relation["natural"="water"](${b});`);
|
||||
}
|
||||
if (sel.green) {
|
||||
parts.push(`way["leisure"="park"](${b});`);
|
||||
parts.push(`way["landuse"~"grass|forest|meadow|recreation_ground"](${b});`);
|
||||
parts.push(`way["natural"="wood"](${b});`);
|
||||
}
|
||||
return `[out:json][timeout:40];(${parts.join("")});out geom;`;
|
||||
}
|
||||
|
||||
/** Ordnet ein Element anhand seiner Tags einer Kategorie zu (oder null). */
|
||||
function categorize(
|
||||
tags: Record<string, string> | undefined,
|
||||
sel: OsmSelection,
|
||||
): GeoCategory | null {
|
||||
if (!tags) return null;
|
||||
if (sel.buildings && tags.building) return "building";
|
||||
if (sel.water && (tags.natural === "water" || tags.waterway)) return "water";
|
||||
if (
|
||||
sel.green &&
|
||||
(tags.leisure === "park" ||
|
||||
tags.natural === "wood" ||
|
||||
/grass|forest|meadow|recreation_ground/.test(tags.landuse ?? ""))
|
||||
)
|
||||
return "green";
|
||||
if (sel.roads && tags.highway) return "road";
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Geschlossene Kategorien (Flächen) vs. offene (Strassen). */
|
||||
function isClosedCategory(cat: GeoCategory): boolean {
|
||||
return cat !== "road";
|
||||
}
|
||||
|
||||
/**
|
||||
* OSM-Features für Zentrum + Radius (Meter) laden. `originIn` erlaubt es, exakt
|
||||
* dieselbe Origin wie ein vorheriger swisstopo-Import zu verwenden.
|
||||
*/
|
||||
export async function fetchOsm(
|
||||
center: LV95,
|
||||
radius: number,
|
||||
sel: OsmSelection,
|
||||
originIn?: GeoOrigin,
|
||||
): Promise<{ origin: GeoOrigin; features: GeoFeature[] }> {
|
||||
const origin = originIn ?? makeOrigin(center);
|
||||
if (!sel.buildings && !sel.roads && !sel.water && !sel.green) {
|
||||
return { origin, features: [] };
|
||||
}
|
||||
|
||||
const bbox = lv95BboxToWgs84(bboxAround(center, radius));
|
||||
const query = buildQuery(bbox, sel);
|
||||
|
||||
const res = await fetch(viaProxy(OVERPASS), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: "data=" + encodeURIComponent(query),
|
||||
});
|
||||
if (!res.ok) throw new Error(`OSM-Abruf fehlgeschlagen (${res.status})`);
|
||||
const data = (await res.json()) as { elements?: OverpassElement[] };
|
||||
|
||||
const features: GeoFeature[] = [];
|
||||
const push = (
|
||||
geometry: { lat: number; lon: number }[] | undefined,
|
||||
cat: GeoCategory,
|
||||
) => {
|
||||
if (!geometry || geometry.length < 2) return;
|
||||
const pts = geometry.map((g) => wgs84ToLocal(g.lat, g.lon, origin));
|
||||
const closed = isClosedCategory(cat);
|
||||
if (closed && pts.length < 3) return;
|
||||
features.push({ category: cat, pts, closed });
|
||||
};
|
||||
|
||||
for (const el of data.elements ?? []) {
|
||||
const cat = categorize(el.tags, sel);
|
||||
if (!cat) continue;
|
||||
if (el.type === "way") {
|
||||
push(el.geometry, cat);
|
||||
} else if (el.type === "relation") {
|
||||
// Multipolygon: outer-Ringe als einzelne geschlossene Linien übernehmen.
|
||||
for (const m of el.members ?? []) {
|
||||
if (m.role === "outer" || m.role === "") push(m.geometry, cat);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { origin, features };
|
||||
}
|
||||
Reference in New Issue
Block a user