// SPDX-License-Identifier: AGPL-3.0-or-later // Copyright (C) 2026 Karim Gabriele Varano import { useState, useEffect, useRef } from 'react' import Icon from './components/Icon' import { BarToggle, BAR_H } from './components/BarControls' import { onMessage, notifyReady } from './lib/rhinoBridge' function send(type, payload = {}) { if (!window.RHINO_MODE) { console.log('[OSM] →', type, payload); return } document.title = 'RHINOMSG::' + JSON.stringify({ type, payload }) } const pillInput = { height: BAR_H, background: 'var(--bg-input)', border: '1px solid var(--border)', borderRadius: 999, color: 'var(--text-primary)', fontSize: 11, fontFamily: 'var(--font)', padding: '0 10px', outline: 'none', boxSizing: 'border-box', } function Field({ label, hint, children }) { return (
{label && {label}}
{children}
{hint && ( {hint} )}
) } function SectionLabel({ children }) { return (
{children}
) } function Radio({ value, options, onChange }) { return (
{options.map(o => ( onChange(o.value)} /> ))}
) } // OSM-Kategorien — Keys matchen das Backend (osm.py CATEGORIES). const CATEGORIES = [ { key: 'streets', label: 'Strassen', icon: 'route', hint: 'Autobahn/Hauptstrasse/Quartierstrasse → Polylinien' }, { key: 'buildings', label: 'Gebäudeumrisse', icon: 'apartment', hint: 'building=* Umrisse als geschlossene Polylinien' }, { key: 'water', label: 'Wasser (Flächen)', icon: 'water', hint: 'natural=water (Seen, Teiche)' }, { key: 'waterways', label: 'Wasserläufe', icon: 'waves', hint: 'waterway=river/stream/canal' }, { key: 'parks', label: 'Parks', icon: 'park', hint: 'leisure=park/garden' }, { key: 'forest', label: 'Wald & Grün', icon: 'forest', hint: 'landuse=forest/grass/meadow' }, { key: 'footpaths', label: 'Fuss-/Radwege', icon: 'directions_walk', hint: 'highway=footway/path/track/cycleway' }, ] export default function OsmApp() { // Standort const [searchText, setSearchText] = useState('') const [center, setCenter] = useState(null) const [searching, setSearching] = useState(false) // Optionen const [radius, setRadius] = useState(200) const [selected, setSelected] = useState({ streets: true, buildings: true, waterways: true, parks: true, forest: true, water: false, footpaths: false, }) const [shift, setShift] = useState(true) const [autoZoom, setAutoZoom] = useState(true) const [replaceExisting, setReplaceExisting] = useState(true) // Live-Log const [logs, setLogs] = useState([]) const [running, setRunning] = useState(false) const [done, setDone] = useState(false) const logRef = useRef(null) useEffect(() => { 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('OSM_LOG', ({ msg }) => addLog(msg)) onMessage('IMPORT_DONE', ({ count }) => { setRunning(false); setDone(true) addLog(`✓ Fertig — ${count} OSM-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 }, []) 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 } const cats = Object.entries(selected).filter(([, v]) => v).map(([k]) => k) if (cats.length === 0) { addLog('Mindestens eine Kategorie auswählen'); return } setLogs([]); setRunning(true); setDone(false) send('RUN_OSM_IMPORT', { centerE: center.e, centerN: center.n, radius: Number(radius), categories: cats, shiftToOrigin: shift, autoZoom, replaceExisting, }) } const toggleCat = (key) => { setSelected(s => ({ ...s, [key]: !s[key] })) } return (
Standort setSearchText(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') handleSearch() }} placeholder="Adresse oder Ortsname" style={{ ...pillInput, flex: 1 }} /> handleManualCoords(e.target.value, center?.n || '')} style={{ ...pillInput, width: 110, fontFamily: 'DM Mono, monospace' }} /> / handleManualCoords(center?.e || '', e.target.value)} style={{ ...pillInput, width: 110, fontFamily: 'DM Mono, monospace' }} /> {center && (
{center.label}
E {Math.round(center.e)} · N {Math.round(center.n)}
)} Bereich Kategorien
{CATEGORIES.map(cat => ( ))}
Positionierung setShift(v === 'origin')} /> Status
{logs.length === 0 ? Bereit : logs.map((l, i) =>
{l}
)} {running &&
Läuft…
}
Quelle: Overpass-API · © OpenStreetMap-Mitwirkende (ODbL)
send('CANCEL')} />
) }