import { useState, useEffect, useRef } from 'react' import Icon from './components/Icon' import { onMessage, notifyReady } from './lib/rhinoBridge' function send(type, payload = {}) { if (!window.RHINO_MODE) { console.log('[Swisstopo] →', type, payload); return } document.title = 'RHINOMSG::' + JSON.stringify({ type, payload }) } function Field({ label, hint, children }) { return (
{label}
{children}
{hint && ( {hint} )}
) } function SectionLabel({ children }) { return (
{children}
) } function Radio({ value, options, onChange }) { return (
{options.map(o => ( ))}
) } export default function SwisstopoApp() { const [ebenen, setEbenen] = useState([]) // Standort const [searchText, setSearchText] = useState('') const [center, setCenter] = useState(null) // {e, n, label} const [searching, setSearching] = useState(false) // Optionen const [radius, setRadius] = useState(100) const [getBuild, setGetBuild] = useState(true) const [buildVersion, setBuildVersion] = useState('v2') // v2 (stabil) / v3 (beta) const [buildVariant, setBuildVariant] = useState('separated') const [getTerrain, setGetTerrain] = useState(false) const [getOrtho, setGetOrtho] = useState(false) const [getContours, setGetContours] = useState(false) const [getContourTin,setGetContourTin]= useState(false) const [getContourSchicht, setGetContourSchicht] = useState(false) const [getContourPatch, setGetContourPatch] = useState(false) const [contourInt, setContourInt] = useState('2.0') // TLM3D deaktiviert: swisstopo liefert nur GDB/SHP/GPKG — kein DXF. // Rhino kann das nicht nativ importieren; OSM-Importer ist die Alternative // fuer Vektordaten (Strassen/Wasser/Gebaeude). const getTlm = false const tlmKinds = {} const [shift, setShift] = useState(true) const [autoZoom, setAutoZoom] = useState(true) const [replaceExisting, setReplaceExisting] = useState(true) const [clipToBbox, setClipToBbox] = useState(false) const [terrainRes, setTerrainRes] = useState('2.0') // Terrain als geschlossenes Volumen (mit Boden 10m unter tiefstem Punkt) // damit Section-Cuts gefuellte Querschnitte zeigen statt nur Linien. // Wirkt auf 3D-Mesh / TIN / Patch — nicht auf 2D-Hoehenlinien und Schichten. const [terrainVolume, setTerrainVolume] = useState(false) const [terrainVolumeDepth, setTerrainVolumeDepth] = useState('10') // Live-Log const [logs, setLogs] = useState([]) const [running, setRunning] = useState(false) const [done, setDone] = useState(false) const logRef = useRef(null) useEffect(() => { onMessage('SWISSTOPO_STATE', ({ ebenen, projectAddress }) => { if (Array.isArray(ebenen)) setEbenen(ebenen) // Projekt-Adresse aus Project-Settings als Vorschlag — nur belegen // wenn das Feld noch leer ist (User-Input nicht ueberschreiben). if (projectAddress) { setSearchText(prev => prev && prev.trim() ? prev : projectAddress) } }) onMessage('GEOCODE_RESULT', ({ result }) => { setSearching(false) if (result && result.e != null && result.n != null) { setCenter({ e: result.e, n: result.n, label: result.label || searchText }) } else { setCenter(null) addLog('Keine Adresse gefunden') } }) onMessage('SWISSTOPO_LOG', ({ msg }) => addLog(msg)) onMessage('IMPORT_DONE', ({ count }) => { setRunning(false) setDone(true) addLog(`✓ Fertig — ${count} Objekt(e) importiert`) }) notifyReady() const blockContext = (ev) => ev.preventDefault() document.addEventListener('contextmenu', blockContext) return () => document.removeEventListener('contextmenu', blockContext) // eslint-disable-next-line react-hooks/exhaustive-deps }, []) // Auto-Scroll Log useEffect(() => { if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight }, [logs]) const addLog = (m) => setLogs(l => [...l, m]) const handleSearch = () => { const t = searchText.trim() if (!t) return setSearching(true) setCenter(null) addLog(`Suche '${t}'...`) send('GEOCODE', { text: t }) } const handleManualCoords = (eRaw, nRaw) => { const e = parseFloat(eRaw), n = parseFloat(nRaw) if (e > 2000000 && n > 1000000) { setCenter({ e, n, label: `LV95 manuell` }) } else { setCenter(null) } } const handleImport = () => { if (!center) { addLog('Bitte zuerst einen Standort wählen'); return } if (!getBuild && !getTerrain && !getContours && !getContourTin && !getContourSchicht && !getContourPatch && !getTlm) { addLog('Mindestens eine Datenquelle wählen'); return } setLogs([]) setRunning(true) setDone(false) const kinds = [] if (getBuild) kinds.push('buildings') if (getTerrain) kinds.push('terrain') if (getOrtho && getTerrain) kinds.push('ortho') if (getContours) kinds.push('contours') if (getContourTin) kinds.push('contour_tin') if (getContourSchicht)kinds.push('contour_schicht') if (getContourPatch) kinds.push('contour_patch') if (getTlm) kinds.push('tlm') const tlmList = Object.entries(tlmKinds).filter(([, v]) => v).map(([k]) => k) send('RUN_IMPORT', { centerE: center.e, centerN: center.n, radius: Number(radius), kinds, shiftToOrigin: shift, autoZoom, replaceExisting, clipToBbox, terrainResolution: terrainRes, buildVersion, buildVariant, contourInterval: contourInt, tlmKinds: tlmList, terrainAsVolume: terrainVolume, terrainVolumeDepth: parseFloat(terrainVolumeDepth) || 10, }) } return (
Standort setSearchText(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') handleSearch() }} placeholder="Adresse oder Ortsname" style={{ flex: 1, fontSize: 11, padding: '5px 8px' }} /> handleManualCoords(e.target.value, center?.n || '')} style={{ width: 110, fontSize: 11, fontFamily: 'DM Mono, monospace', padding: '5px 8px' }} /> / handleManualCoords(center?.e || '', e.target.value)} style={{ width: 110, fontSize: 11, fontFamily: 'DM Mono, monospace', padding: '5px 8px' }} /> {center && (
{center.label}
E {Math.round(center.e)} · N {Math.round(center.n)}
)} Bereich Was holen {getBuild && ( )} {getBuild && buildVersion === 'v3' && ( )} {getTerrain && ( )} {(getContours || getContourTin || getContourSchicht || getContourPatch) && ( )} {(getTerrain || getContourTin || getContourPatch) && ( <> Nachbearbeitung {terrainVolume && ( setTerrainVolumeDepth(e.target.value)} style={{ width: 60, textAlign: 'right' }} /> m unter tiefstem Punkt )} )} Positionierung setShift(v === 'origin')} /> {(logs.length > 0 || running) && ( <> Status
{logs.map((m, i) =>
{m}
)} {running &&
Läuft…
}
)}
{center ? `Tiles werden im Projekt-Ordner neben der .3dm gecacht (Fallback: ~/Library/Caches/Dossier/swisstopo/ wenn ungespeichert)` : 'Wähle zuerst einen Standort'}
) }