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 => (
onChange(o.value)}
className={value === o.value ? 'btn-contained' : 'btn-outlined'}
style={{ padding: '4px 10px', fontSize: 10 }}
title={o.hint || ''}
>
{o.label}
))}
)
}
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 [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')
// 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 }) => {
if (Array.isArray(ebenen)) setEbenen(ebenen)
})
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 && !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 (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,
})
}
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' }}
/>
{searching ? '…' : 'Suchen'}
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
setGetBuild(e.target.checked)} />
Bestand-Gebäude (swissBUILDINGS3D, DWG)
{getBuild && (
)}
{getBuild && buildVersion === 'v3' && (
)}
setGetTerrain(e.target.checked)} />
Terrain (swissALTI3D → Mesh)
{getTerrain && (
)}
setGetOrtho(e.target.checked)} />
Orthofoto auf Terrain mappen (SWISSIMAGE 10cm)
setGetContours(e.target.checked)} />
Höhenlinien (2D, auf aktivem Geschoss)
setGetContourTin(e.target.checked)} />
TIN-Mesh aus Höhenlinien
setGetContourSchicht(e.target.checked)} />
Schichtenmodell aus Höhenlinien
{(getContours || getContourTin || getContourSchicht) && (
)}
Positionierung
setShift(v === 'origin')}
/>
setAutoZoom(e.target.checked)} />
Auto-Zoom auf importierte Objekte
setReplaceExisting(e.target.checked)} />
Bestehende swisstopo-Objekte vorher löschen
setClipToBbox(e.target.checked)} />
Auf Radius zuschneiden (langsam, optional)
{(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'}
send('CANCEL', {})}
disabled={running}>
{done ? 'Schliessen' : 'Abbrechen'}
{running ? 'Importiere…' : 'Importieren'}
)
}