// Standort-Import-Dialog (modal). Sucht einen Ort/Adresse (geo.admin // SearchServer), lässt einen Radius wählen und importiert Gebäude-Grundrisse // (swisstopo) sowie OSM-Features (Gebäude/Strassen/Wasser/Grün) als Kontext- // Geometrie ins Projekt. // // Muster wie ExportPdfDialog/ImportDialog: abgedunkeltes Overlay, Klick auf den // Hintergrund + Esc schließen. Der eigentliche Netz-Abruf läuft hier (io/*); das // Ergebnis wird über `onImport(objs)` nach oben gereicht (SitePanel legt es via // Host in `project.context` ab). Bezeichner englisch, UI-Text/Kommentare deutsch // (CONVENTIONS.md). import { useEffect, useRef, useState } from "react"; import { t } from "../i18n"; import { geocode, fetchBuildings, fetchTerrain } from "../io/swissTopo"; import type { GeocodeCandidate } from "../io/swissTopo"; import { fetchOsm } from "../io/osm"; import type { OsmSelection } from "../io/osm"; import { featuresToContextObjects } from "../io/geoContext"; import type { GeoFeature } from "../io/geoContext"; import { terrainMeshFromGrid } from "../model/terrain"; import type { GeoOrigin } from "../io/lv95"; import type { ContextObject } from "../model/types"; export interface ContextImportDialogProps { /** Übernimmt die fertigen Kontext-Objekte (nach erfolgreichem Import). */ onImport: (objs: ContextObject[]) => void; onClose: () => void; } type Sources = { swissBuildings: boolean; swissTerrain: boolean; osmBuildings: boolean; osmRoads: boolean; osmWater: boolean; osmGreen: boolean; }; const DEFAULT_SOURCES: Sources = { swissBuildings: true, swissTerrain: true, osmBuildings: false, osmRoads: true, osmWater: true, osmGreen: true, }; export function ContextImportDialog({ onImport, onClose, }: ContextImportDialogProps) { useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [onClose]); const [query, setQuery] = useState(""); const [candidates, setCandidates] = useState([]); const [selected, setSelected] = useState(null); const [radius, setRadius] = useState(200); const [sources, setSources] = useState(DEFAULT_SOURCES); const [searching, setSearching] = useState(false); const [importing, setImporting] = useState(false); const [status, setStatus] = useState(null); const [error, setError] = useState(null); const searchSeq = useRef(0); const runSearch = async () => { const q = query.trim(); if (!q) return; setError(null); setSearching(true); const seq = ++searchSeq.current; try { const res = await geocode(q, 8); if (seq !== searchSeq.current) return; // veraltet setCandidates(res); setSelected(res[0] ?? null); if (res.length === 0) setStatus(t("ctxImport.noResults")); else setStatus(null); } catch { if (seq === searchSeq.current) setError(t("ctxImport.searchError")); } finally { if (seq === searchSeq.current) setSearching(false); } }; const toggle = (key: keyof Sources) => setSources((s) => ({ ...s, [key]: !s[key] })); const anySource = sources.swissBuildings || sources.swissTerrain || sources.osmBuildings || sources.osmRoads || sources.osmWater || sources.osmGreen; const runImport = async () => { if (!selected || !anySource) return; setError(null); setImporting(true); setStatus(t("ctxImport.working")); try { const center = selected.lv95; let origin: GeoOrigin | undefined; const all: GeoFeature[] = []; if (sources.swissBuildings) { const r = await fetchBuildings(center, radius, origin); origin = r.origin; all.push(...r.features); } const osmSel: OsmSelection = { buildings: sources.osmBuildings, roads: sources.osmRoads, water: sources.osmWater, green: sources.osmGreen, }; if ( osmSel.buildings || osmSel.roads || osmSel.water || osmSel.green ) { const r = await fetchOsm(center, radius, osmSel, origin); origin = r.origin; all.push(...r.features); } // Gelände-Höhenraster (swissALTI3D via geo.admin-Profil) → Terrain-Mesh. let terrain: ContextObject | null = null; if (sources.swissTerrain) { const r = await fetchTerrain(center, radius, origin); origin = r.origin; if (r.grid) { const mesh = terrainMeshFromGrid(r.grid); if (mesh.positions.length >= 9) terrain = mesh; } } if (all.length === 0 && !terrain) { setError(t("ctxImport.empty")); setStatus(null); return; } const objs = featuresToContextObjects(all, selected.label); if (terrain) objs.push(terrain); onImport(objs); setStatus( t("ctxImport.imported", { count: all.length, sets: objs.length }), ); onClose(); } catch { setError(t("ctxImport.importError")); setStatus(null); } finally { setImporting(false); } }; return (
e.stopPropagation()} >
{t("ctxImport.title")}
{/* Ortssuche */}
{t("ctxImport.location")}
setQuery(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); void runSearch(); } }} aria-label={t("ctxImport.location")} />
{candidates.length > 0 && (
    {candidates.map((c, i) => (
  • setSelected(c)} title={c.label} > {c.label}
  • ))}
)}
{/* Radius */}
{t("ctxImport.radius")}
setRadius( Math.max(20, Math.min(2000, Number(e.target.value) || 0)), ) } aria-label={t("ctxImport.radius")} /> m
{/* Quellen */}
{t("ctxImport.sources")}
{status &&
{status}
} {error &&
{error}
}
); }