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