Swisstopo + OSM Importer + Höhenlinien + Bulk-Op Performance
Swisstopo Iter 3:
- Ortho-Drape: TIN-Mesh aus Terrain-Grid mit per-vertex UVs + PictureFrame-Material
- Project-Cache: TIFs werden neben .3dm gespeichert (SMB-shareable)
- Layer-Restruktur: 80_swisstopo/{Terrain, Luftbild} Sub-Ebenen
- TIFs direkt (kein PNG-Downsampling) für volle Auflösung
- UV-Inset gegen weisse Streifen zwischen Kacheln
- Hoehenlinien (2D, swissALTI3D) auf aktives Geschoss OKFF projiziert
- TIN-Mesh + Schichtenmodell aus Contours (separate Optionen)
- TLM3D entfernt (swisstopo liefert nur GDB/SHP, kein DXF)
OSM Importer (neu):
- rhino/osm.py: Overpass-API-Client
- src/OsmApp.jsx: React-Dialog mit Adresse + Radius + 7 Kategorien
- Strassen/Gebäude/Wasser/Wasserläufe/Parks/Wald/Fusswege (Codes 7101-7107)
- ElementeApp: PillGroup "Importer" mit Swisstopo + OSM Buttons
Sub-Ebenen — rekursiv durch hierarchische Ebenen:
- Visibility-Toggle: slimEbene rekursiv (children bleiben erhalten)
- Settings-Dialog: _find_sublayer_by_code_recursive + _replace_in_tree
- Hatch Auto-Fill: refresh_layer_fills + _fill_signature + _ebene_fill_for_layer
alle rekursiv durch children
- EbenenSettingsApp: flattenEbenen-Helper
Bulk-Op Performance (Delete/Cut/etc.):
- _USER_BULK_CMDS + _BULK_ACTIVE_KEY Sticky-Flag
- CommandBegin: doc.Views.RedrawEnabled = False + Listener-Bail aktiv
- CommandEnd: RedrawEnabled restore + 1× Redraw + Selection-Refresh
- Bail-outs in dimensionen.on_idle/on_select, elemente._on_idle_selection,
gestaltung.on_idle_flush/on_delete
- Verhindert das sichtbare "Runterzählen" pro Element bei Bulk-Delete
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -580,6 +580,10 @@ def _install_listeners(bridge):
|
|||||||
return
|
return
|
||||||
|
|
||||||
def on_idle(s, e):
|
def on_idle(s, e):
|
||||||
|
# Waehrend Bulk-Ops (z.B. _Delete bei 6000 Objekten): nicht pollen.
|
||||||
|
# tick_idle iteriert alle Doc-Objekte, das ist Overhead bei jedem
|
||||||
|
# Tick zwischen den einzelnen Deletes. CommandEnd refresht.
|
||||||
|
if sc.sticky.get("_dossier_bulk_op_active"): return
|
||||||
b = sc.sticky.get("dimensionen_bridge")
|
b = sc.sticky.get("dimensionen_bridge")
|
||||||
if b is not None:
|
if b is not None:
|
||||||
try: b.tick_idle()
|
try: b.tick_idle()
|
||||||
@@ -588,6 +592,7 @@ def _install_listeners(bridge):
|
|||||||
def on_select(s, e):
|
def on_select(s, e):
|
||||||
# Swisstopo-Import feuert tausende Selection-Events → bail.
|
# Swisstopo-Import feuert tausende Selection-Events → bail.
|
||||||
if sc.sticky.get("dossier_swisstopo_busy"): return
|
if sc.sticky.get("dossier_swisstopo_busy"): return
|
||||||
|
if sc.sticky.get("_dossier_bulk_op_active"): return
|
||||||
b = sc.sticky.get("dimensionen_bridge")
|
b = sc.sticky.get("dimensionen_bridge")
|
||||||
if b is not None:
|
if b is not None:
|
||||||
try: b._send_state(force=True)
|
try: b._send_state(force=True)
|
||||||
|
|||||||
+565
-90
@@ -4661,6 +4661,7 @@ class ElementeBridge(panel_base.BaseBridge):
|
|||||||
elif t == "OPEN_SWISSTOPO": self._cmd_open_swisstopo(p)
|
elif t == "OPEN_SWISSTOPO": self._cmd_open_swisstopo(p)
|
||||||
elif t == "IMPORT_SWISSTOPO": self._cmd_import_swisstopo(p)
|
elif t == "IMPORT_SWISSTOPO": self._cmd_import_swisstopo(p)
|
||||||
elif t == "OPEN_SWISSTOPO_DIALOG": self._cmd_open_swisstopo_dialog(p)
|
elif t == "OPEN_SWISSTOPO_DIALOG": self._cmd_open_swisstopo_dialog(p)
|
||||||
|
elif t == "OPEN_OSM_DIALOG": self._cmd_open_osm_dialog(p)
|
||||||
elif t == "UPDATE_WALL": self._update_wall(p)
|
elif t == "UPDATE_WALL": self._update_wall(p)
|
||||||
elif t == "UPDATE_ELEMENT": self._update_wall(p) # gleiche Logik fuer alle
|
elif t == "UPDATE_ELEMENT": self._update_wall(p) # gleiche Logik fuer alle
|
||||||
elif t == "DELETE_WALL": self._delete_wall(p.get("id"))
|
elif t == "DELETE_WALL": self._delete_wall(p.get("id"))
|
||||||
@@ -6810,6 +6811,15 @@ class ElementeBridge(panel_base.BaseBridge):
|
|||||||
|
|
||||||
before_all = set(o.Id for o in d.Objects if o and not o.IsDeleted)
|
before_all = set(o.Id for o in d.Objects if o and not o.IsDeleted)
|
||||||
|
|
||||||
|
# Cache-Folder pro Projekt setzen (neben der .3dm-Datei).
|
||||||
|
# Damit reisen die Tiles mit dem Projekt — bei SMB-Sharing
|
||||||
|
# findet Rhino die TIFs auch von anderen Maschinen, sofern
|
||||||
|
# der Mount-Pfad identisch ist. Falls Doc unsaved: globaler
|
||||||
|
# Cache.
|
||||||
|
cache_dir = swisstopo.get_cache_dir_for_doc(d)
|
||||||
|
swisstopo.set_cache_dir(cache_dir)
|
||||||
|
self._push_log("Cache: {}".format(cache_dir))
|
||||||
|
|
||||||
# Listener-Suppression: elemente.py + gestaltung.py haben Add/
|
# Listener-Suppression: elemente.py + gestaltung.py haben Add/
|
||||||
# Replace-Listener die pro neu importiertem Objekt feuern. Bei
|
# Replace-Listener die pro neu importiertem Objekt feuern. Bei
|
||||||
# 5000+ DXF-Objekten erstickt das den Import. Sticky-Flag setzen,
|
# 5000+ DXF-Objekten erstickt das den Import. Sticky-Flag setzen,
|
||||||
@@ -6820,8 +6830,11 @@ class ElementeBridge(panel_base.BaseBridge):
|
|||||||
if "buildings" in kinds:
|
if "buildings" in kinds:
|
||||||
variant = (opts.get("buildVariant") or "separated").strip().lower()
|
variant = (opts.get("buildVariant") or "separated").strip().lower()
|
||||||
if variant not in ("separated", "solid"): variant = "separated"
|
if variant not in ("separated", "solid"): variant = "separated"
|
||||||
|
version = (opts.get("buildVersion") or "v2").strip().lower()
|
||||||
|
if version not in ("v2", "v3"): version = "v2"
|
||||||
paths = swisstopo.fetch_buildings_dwg(
|
paths = swisstopo.fetch_buildings_dwg(
|
||||||
bbox, progress=self._push_log, variant=variant)
|
bbox, progress=self._push_log,
|
||||||
|
variant=variant, version=version)
|
||||||
for idx, p in enumerate(paths):
|
for idx, p in enumerate(paths):
|
||||||
try: size_mb = os.path.getsize(p) / 1e6
|
try: size_mb = os.path.getsize(p) / 1e6
|
||||||
except Exception: size_mb = 0
|
except Exception: size_mb = 0
|
||||||
@@ -6952,16 +6965,18 @@ class ElementeBridge(panel_base.BaseBridge):
|
|||||||
new_obj_ids.extend(o.Id for o in kept)
|
new_obj_ids.extend(o.Id for o in kept)
|
||||||
|
|
||||||
# --- Terrain (XYZ → Mesh) ------------------------------
|
# --- Terrain (XYZ → Mesh) ------------------------------
|
||||||
if "terrain" in kinds:
|
# Terrain-Daten (XYZ + Grid) holen, sobald Mesh ODER
|
||||||
|
# Hoehenlinien gewuenscht sind — beide nutzen das Grid.
|
||||||
|
need_dem = any(k in kinds for k in
|
||||||
|
("terrain", "contours", "contour_tin", "contour_schicht"))
|
||||||
|
mesh_objects = []
|
||||||
|
merged_grid = None
|
||||||
|
if need_dem:
|
||||||
res = (opts.get("terrainResolution") or "2.0").strip()
|
res = (opts.get("terrainResolution") or "2.0").strip()
|
||||||
try: target_step = float(res)
|
try: target_step = float(res)
|
||||||
except Exception: target_step = 2.0
|
except Exception: target_step = 2.0
|
||||||
xyz_paths = swisstopo.fetch_terrain_xyz(
|
xyz_paths = swisstopo.fetch_terrain_xyz(
|
||||||
bbox, resolution=res, progress=self._push_log)
|
bbox, resolution=res, progress=self._push_log)
|
||||||
mesh_objects = []
|
|
||||||
# Erst ALLE Tiles in Grids parsen, dann mergen, dann
|
|
||||||
# EIN Mesh bauen — sonst gibt es einen 1m-Streifen
|
|
||||||
# ohne Faces zwischen benachbarten Tiles.
|
|
||||||
grids = []
|
grids = []
|
||||||
for p in xyz_paths:
|
for p in xyz_paths:
|
||||||
self._push_log("Parse {}...".format(os.path.basename(p)))
|
self._push_log("Parse {}...".format(os.path.basename(p)))
|
||||||
@@ -6980,20 +6995,137 @@ class ElementeBridge(panel_base.BaseBridge):
|
|||||||
if merged is None:
|
if merged is None:
|
||||||
self._push_log("Merge lieferte None")
|
self._push_log("Merge lieferte None")
|
||||||
else:
|
else:
|
||||||
|
merged_grid = merged
|
||||||
self._push_log("Merge: {} Tiles → {} Punkte ({}×{} Raster)".format(
|
self._push_log("Merge: {} Tiles → {} Punkte ({}×{} Raster)".format(
|
||||||
len(grids), len(merged["points"]),
|
len(grids), len(merged["points"]),
|
||||||
len(merged["es"]), len(merged["ns"])))
|
len(merged["es"]), len(merged["ns"])))
|
||||||
mesh = swisstopo.mesh_from_grid(
|
|
||||||
merged,
|
|
||||||
origin_shift=origin_shift,
|
|
||||||
unit_scale=m_to_unit)
|
|
||||||
self._push_log("→ Mesh: {} Vertices / {} Faces".format(
|
|
||||||
mesh.Vertices.Count, mesh.Faces.Count))
|
|
||||||
gid = d.Objects.AddMesh(mesh)
|
|
||||||
obj = d.Objects.Find(gid)
|
|
||||||
if obj: mesh_objects.append((obj, merged["bbox"]))
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
self._push_log("Mesh-Bau fehlgeschlagen: {}".format(ex))
|
self._push_log("Grid-Merge fehlgeschlagen: {}".format(ex))
|
||||||
|
|
||||||
|
# 3D-Mesh bauen wenn Terrain gewuenscht — unabhaengig vom
|
||||||
|
# Ortho. Wenn Ortho auch an ist: Drape-Mesh liegt ueber
|
||||||
|
# dem Plain-Mesh (User togglet im Layer-Panel was er
|
||||||
|
# sehen will).
|
||||||
|
if "terrain" in kinds and merged_grid is not None:
|
||||||
|
try:
|
||||||
|
mesh = swisstopo.mesh_from_grid(
|
||||||
|
merged_grid,
|
||||||
|
origin_shift=origin_shift,
|
||||||
|
unit_scale=m_to_unit)
|
||||||
|
self._push_log("→ Mesh: {} Vertices / {} Faces".format(
|
||||||
|
mesh.Vertices.Count, mesh.Faces.Count))
|
||||||
|
gid = d.Objects.AddMesh(mesh)
|
||||||
|
obj = d.Objects.Find(gid)
|
||||||
|
if obj: mesh_objects.append((obj, merged_grid["bbox"]))
|
||||||
|
except Exception as ex:
|
||||||
|
self._push_log("Mesh-Bau fehlgeschlagen: {}".format(ex))
|
||||||
|
|
||||||
|
# Contours sind die Grundlage fuer drei moegliche Outputs:
|
||||||
|
# 'contours' → flache 2D-Curves auf OKFF
|
||||||
|
# 'contour_tin' → TIN-Mesh aus Contour-Vertices
|
||||||
|
# 'contour_schicht' → Planare Flaechen pro Hoehe
|
||||||
|
# Wir generieren einmal die echten 3D-Curves und teilen
|
||||||
|
# sie auf die drei Outputs auf.
|
||||||
|
contour_kinds = ("contours", "contour_tin", "contour_schicht")
|
||||||
|
need_contours = any(k in kinds for k in contour_kinds) and merged_grid is not None
|
||||||
|
raw_contours = []
|
||||||
|
if need_contours:
|
||||||
|
try:
|
||||||
|
interval_c = float(opts.get("contourInterval") or 2.0)
|
||||||
|
except Exception: interval_c = 2.0
|
||||||
|
try:
|
||||||
|
self._push_log("Hoehenlinien generieren (Abstand {} m, real Z)...".format(interval_c))
|
||||||
|
raw_contours = swisstopo.generate_contour_curves(
|
||||||
|
merged_grid, origin_shift, m_to_unit,
|
||||||
|
interval=interval_c,
|
||||||
|
progress=self._push_log)
|
||||||
|
except Exception as ex:
|
||||||
|
self._push_log("Contour-Generation-Fehler: {}".format(ex))
|
||||||
|
raw_contours = []
|
||||||
|
|
||||||
|
# 2D-Hoehenlinien auf OKFF des aktiven Geschosses
|
||||||
|
if "contours" in kinds and raw_contours:
|
||||||
|
project_zero_doc = 0.0 if shift else project_zero_mum * m_to_unit
|
||||||
|
active_okff = 0.0
|
||||||
|
try:
|
||||||
|
z_raw = d.Strings.GetValue("dossier_zeichnungsebenen")
|
||||||
|
zlist = json.loads(z_raw) if z_raw else []
|
||||||
|
for z_ in zlist:
|
||||||
|
if isinstance(z_, dict) and z_.get("id") == z_id:
|
||||||
|
active_okff = float(z_.get("okff", 0) or 0)
|
||||||
|
break
|
||||||
|
except Exception: pass
|
||||||
|
flatten_z_doc = project_zero_doc + active_okff * m_to_unit
|
||||||
|
self._push_log("2D-Hoehenlinien auf OKFF Z={:.3f}...".format(flatten_z_doc))
|
||||||
|
contour_objs = []
|
||||||
|
for c in raw_contours:
|
||||||
|
# Wichtig: duplizieren, damit das Original (mit
|
||||||
|
# echtem Z) fuer TIN/Schichten erhalten bleibt.
|
||||||
|
try:
|
||||||
|
c_flat = c.DuplicateCurve()
|
||||||
|
bb_c = c_flat.GetBoundingBox(True)
|
||||||
|
z_mid = (bb_c.Min.Z + bb_c.Max.Z) * 0.5
|
||||||
|
dz = flatten_z_doc - z_mid
|
||||||
|
if abs(dz) > 1e-9:
|
||||||
|
c_flat.Translate(rg.Vector3d(0, 0, dz))
|
||||||
|
gid = d.Objects.AddCurve(c_flat)
|
||||||
|
if gid and gid != System.Guid.Empty:
|
||||||
|
ob = d.Objects.Find(gid)
|
||||||
|
if ob: contour_objs.append(ob)
|
||||||
|
except Exception: pass
|
||||||
|
if z_id and contour_objs:
|
||||||
|
self._move_to_sublayer(
|
||||||
|
d, contour_objs, z_id, "14",
|
||||||
|
tag="contour",
|
||||||
|
fallback_name="14_Höhenlinien",
|
||||||
|
fallback_color="#909050")
|
||||||
|
elif contour_objs:
|
||||||
|
self._tag_objects(d, contour_objs, "contour")
|
||||||
|
self._push_log("→ {} Hoehenlinien (2D) auf '14_Höhenlinien'".format(
|
||||||
|
len(contour_objs)))
|
||||||
|
|
||||||
|
# TIN-Mesh aus Hoehenlinien
|
||||||
|
if "contour_tin" in kinds and raw_contours:
|
||||||
|
try:
|
||||||
|
tin_obj = swisstopo.generate_mesh_from_contours(
|
||||||
|
d, raw_contours,
|
||||||
|
m_to_unit=m_to_unit,
|
||||||
|
progress=self._push_log)
|
||||||
|
if tin_obj:
|
||||||
|
# Tag + auf 80_swisstopo Parent
|
||||||
|
at = tin_obj.Attributes.Duplicate()
|
||||||
|
at.SetUserString("dossier_swisstopo_kind", "contour_tin")
|
||||||
|
d.Objects.ModifyAttributes(tin_obj, at, True)
|
||||||
|
if z_id:
|
||||||
|
self._move_to_sublayer(
|
||||||
|
d, [tin_obj], z_id, "80",
|
||||||
|
tag="contour_tin",
|
||||||
|
fallback_name="80_swisstopo",
|
||||||
|
fallback_color="#909090")
|
||||||
|
except Exception as ex:
|
||||||
|
self._push_log("TIN-Mesh-Fehler: {}".format(ex))
|
||||||
|
|
||||||
|
# Schichtenmodell (planare Flaechen pro Hoehe)
|
||||||
|
if "contour_schicht" in kinds and raw_contours:
|
||||||
|
try:
|
||||||
|
schicht_objs = swisstopo.generate_schichtenmodell(
|
||||||
|
d, raw_contours, progress=self._push_log)
|
||||||
|
for s in schicht_objs:
|
||||||
|
try:
|
||||||
|
at = s.Attributes.Duplicate()
|
||||||
|
at.SetUserString("dossier_swisstopo_kind", "contour_schicht")
|
||||||
|
d.Objects.ModifyAttributes(s, at, True)
|
||||||
|
except Exception: pass
|
||||||
|
if z_id and schicht_objs:
|
||||||
|
self._move_to_sublayer(
|
||||||
|
d, schicht_objs, z_id, "80",
|
||||||
|
tag="contour_schicht",
|
||||||
|
fallback_name="80_swisstopo",
|
||||||
|
fallback_color="#909090")
|
||||||
|
self._push_log("→ Schichtenmodell: {} Flaechen auf '80_swisstopo'".format(
|
||||||
|
len(schicht_objs)))
|
||||||
|
except Exception as ex:
|
||||||
|
self._push_log("Schichtenmodell-Fehler: {}".format(ex))
|
||||||
# Layer-Move auf aktive Geschoss/80_swisstopo Sublayer
|
# Layer-Move auf aktive Geschoss/80_swisstopo Sublayer
|
||||||
if z_id and mesh_objects:
|
if z_id and mesh_objects:
|
||||||
sub_name = _find_ebene_sublayer_name(
|
sub_name = _find_ebene_sublayer_name(
|
||||||
@@ -7008,47 +7140,29 @@ class ElementeBridge(panel_base.BaseBridge):
|
|||||||
elif mesh_objects:
|
elif mesh_objects:
|
||||||
objs = [m[0] for m in mesh_objects]
|
objs = [m[0] for m in mesh_objects]
|
||||||
self._tag_objects(d, objs, "terrain")
|
self._tag_objects(d, objs, "terrain")
|
||||||
if "ortho" in kinds and mesh_objects:
|
if "ortho" in kinds and merged_grid is not None:
|
||||||
self._push_log("Hole Orthofoto...")
|
self._push_log("Hole Orthofoto...")
|
||||||
ortho_paths = swisstopo.fetch_orthophoto(
|
ortho_paths = swisstopo.fetch_orthophoto(
|
||||||
bbox, resolution="2.0", progress=self._push_log)
|
bbox, resolution="2.0", progress=self._push_log)
|
||||||
if ortho_paths:
|
if ortho_paths:
|
||||||
# Max-Z des Terrains finden — Plane sitzt knapp darueber
|
# Pro Tile:
|
||||||
# damit sie in Top-View ueber dem Terrain liegt.
|
# - Drape-Mesh (Foto folgt Topo) auf '80T_Terrain'
|
||||||
terr_max_z = 0.0
|
# - flache PictureFrame (fuer 2D-Zeichnen) auf '80L_Luftbild'
|
||||||
for tobj, _ in mesh_objects:
|
self._push_log("→ {} Ortho-Tile(s) als Terrain (Drape) + Luftbild (flach)".format(
|
||||||
try:
|
len(ortho_paths)))
|
||||||
bb = tobj.Geometry.GetBoundingBox(True)
|
# Sub-Ebenen Terrain + Luftbild sicherstellen
|
||||||
if bb.IsValid and bb.Max.Z > terr_max_z:
|
sub_codes = {}
|
||||||
terr_max_z = bb.Max.Z
|
if z_id:
|
||||||
except Exception: pass
|
|
||||||
z_offset = max(0.001, terr_max_z * 1e-4) # winziges Epsilon
|
|
||||||
plane_z = terr_max_z + z_offset
|
|
||||||
self._push_log("→ {} Ortho-Tile(s), platziere Plane bei Z={:.3f}".format(
|
|
||||||
len(ortho_paths), plane_z))
|
|
||||||
# Tile-IDs vorab extrahieren + Sub-Ebenen unter
|
|
||||||
# 80_swisstopo registrieren BEVOR die Pictures
|
|
||||||
# erzeugt werden. Damit kann jede _-Picture direkt
|
|
||||||
# auf dem richtigen Sub-Layer landen statt nach-
|
|
||||||
# traeglich verschoben zu werden (das brach die
|
|
||||||
# Textur).
|
|
||||||
import re as _re_o
|
|
||||||
tile_id_per_path = {}
|
|
||||||
for op in ortho_paths:
|
|
||||||
m = _re_o.search(r"(\d{3,4}-\d{2,4})",
|
|
||||||
os.path.basename(op))
|
|
||||||
if m: tile_id_per_path[op] = m.group(1)
|
|
||||||
if z_id and tile_id_per_path:
|
|
||||||
_find_ebene_sublayer_name(
|
_find_ebene_sublayer_name(
|
||||||
d, ["swisstopo", "gelaende_topo"],
|
d, ["swisstopo", "gelaende_topo"],
|
||||||
"80", "swisstopo",
|
"80", "swisstopo",
|
||||||
default_color="#909090", default_lw=0.18)
|
default_color="#909090", default_lw=0.18)
|
||||||
self._ensure_ortho_tile_ebenen(
|
sub_codes = self._ensure_swisstopo_subebenen(d)
|
||||||
d, list(tile_id_per_path.values()))
|
# Target-Layer-Indices fuer Terrain + Luftbild
|
||||||
# Target-Sub-Layer-Index pro Tile holen
|
|
||||||
import layer_builder as _lb_o
|
import layer_builder as _lb_o
|
||||||
tile_layer_idx = {}
|
terrain_idx = -1
|
||||||
if z_id:
|
luftbild_idx = -1
|
||||||
|
if z_id and sub_codes:
|
||||||
parent_idx = _lb_o._find_top_by_id(d, z_id)
|
parent_idx = _lb_o._find_top_by_id(d, z_id)
|
||||||
if parent_idx >= 0:
|
if parent_idx >= 0:
|
||||||
parent_id_ = d.Layers[parent_idx].Id
|
parent_id_ = d.Layers[parent_idx].Id
|
||||||
@@ -7056,10 +7170,20 @@ class ElementeBridge(panel_base.BaseBridge):
|
|||||||
d, parent_id_, "80")
|
d, parent_id_, "80")
|
||||||
if base_idx >= 0:
|
if base_idx >= 0:
|
||||||
base_id_ = d.Layers[base_idx].Id
|
base_id_ = d.Layers[base_idx].Id
|
||||||
for op, tid in tile_id_per_path.items():
|
if sub_codes.get("terrain"):
|
||||||
idx = _lb_o._find_sublayer_by_code(
|
terrain_idx = _lb_o._find_sublayer_by_code(
|
||||||
d, base_id_, tid)
|
d, base_id_, sub_codes["terrain"])
|
||||||
if idx >= 0: tile_layer_idx[op] = idx
|
if sub_codes.get("luftbild"):
|
||||||
|
luftbild_idx = _lb_o._find_sublayer_by_code(
|
||||||
|
d, base_id_, sub_codes["luftbild"])
|
||||||
|
# Max-Z des Terrains fuer flache Luftbild-Plane
|
||||||
|
terr_max_z_doc = 0.0
|
||||||
|
if merged_grid:
|
||||||
|
try:
|
||||||
|
max_z_m = max(z for z in merged_grid["points"].values())
|
||||||
|
terr_max_z_doc = (max_z_m - origin_shift[2]) * m_to_unit
|
||||||
|
except Exception: pass
|
||||||
|
flat_z = terr_max_z_doc + max(0.001, terr_max_z_doc * 1e-4)
|
||||||
ortho_objs = []
|
ortho_objs = []
|
||||||
for ortho_path in ortho_paths:
|
for ortho_path in ortho_paths:
|
||||||
tile_bbox = _parse_swisstopo_tile_bbox(
|
tile_bbox = _parse_swisstopo_tile_bbox(
|
||||||
@@ -7068,24 +7192,41 @@ class ElementeBridge(panel_base.BaseBridge):
|
|||||||
self._push_log(" → Tile-bbox nicht ableitbar aus {}".format(
|
self._push_log(" → Tile-bbox nicht ableitbar aus {}".format(
|
||||||
os.path.basename(ortho_path)))
|
os.path.basename(ortho_path)))
|
||||||
continue
|
continue
|
||||||
tgt_idx = tile_layer_idx.get(ortho_path, -1)
|
# 1) Drape-Mesh auf '80T_Terrain'
|
||||||
try:
|
try:
|
||||||
obj = swisstopo.add_ortho_plane(
|
drape = swisstopo.add_ortho_draped_mesh(
|
||||||
d, ortho_path, tile_bbox,
|
d, ortho_path, tile_bbox, merged_grid,
|
||||||
origin_shift, m_to_unit, z_doc=plane_z,
|
origin_shift, m_to_unit,
|
||||||
target_layer_idx=tgt_idx)
|
z_lift=0.05,
|
||||||
if obj:
|
target_layer_idx=terrain_idx)
|
||||||
ortho_objs.append(obj)
|
if drape:
|
||||||
# Tag fuer Replace-Detection bei naechstem Import
|
ortho_objs.append(drape)
|
||||||
try:
|
try:
|
||||||
at = obj.Attributes.Duplicate()
|
at = drape.Attributes.Duplicate()
|
||||||
at.SetUserString(
|
at.SetUserString(
|
||||||
"dossier_swisstopo_kind", "ortho")
|
"dossier_swisstopo_kind", "ortho")
|
||||||
d.Objects.ModifyAttributes(obj, at, True)
|
d.Objects.ModifyAttributes(drape, at, True)
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
self._push_log("Ortho-Apply: {}".format(ex))
|
self._push_log("Drape-Apply: {}".format(ex))
|
||||||
self._push_log("→ {} Ortho-Plane(s) auf eigene Sub-Layer".format(
|
# 2) Flache Picture auf '80L_Luftbild'
|
||||||
|
try:
|
||||||
|
flat = swisstopo.add_ortho_plane(
|
||||||
|
d, ortho_path, tile_bbox,
|
||||||
|
origin_shift, m_to_unit,
|
||||||
|
z_doc=flat_z,
|
||||||
|
target_layer_idx=luftbild_idx)
|
||||||
|
if flat:
|
||||||
|
ortho_objs.append(flat)
|
||||||
|
try:
|
||||||
|
at = flat.Attributes.Duplicate()
|
||||||
|
at.SetUserString(
|
||||||
|
"dossier_swisstopo_kind", "ortho")
|
||||||
|
d.Objects.ModifyAttributes(flat, at, True)
|
||||||
|
except Exception: pass
|
||||||
|
except Exception as ex:
|
||||||
|
self._push_log("Flat-Apply: {}".format(ex))
|
||||||
|
self._push_log("→ {} Ortho-Objekte (Drape+Flat) auf eigene Sub-Ebenen".format(
|
||||||
len(ortho_objs)))
|
len(ortho_objs)))
|
||||||
# End-Diagnose mit BBox-Koords damit wir sehen
|
# End-Diagnose mit BBox-Koords damit wir sehen
|
||||||
# wo die Pictures tatsaechlich gelandet sind.
|
# wo die Pictures tatsaechlich gelandet sind.
|
||||||
@@ -7138,6 +7279,77 @@ class ElementeBridge(panel_base.BaseBridge):
|
|||||||
new_obj_ids.extend(o.Id for o in ortho_objs)
|
new_obj_ids.extend(o.Id for o in ortho_objs)
|
||||||
new_obj_ids.extend(o.Id for o, _ in mesh_objects)
|
new_obj_ids.extend(o.Id for o, _ in mesh_objects)
|
||||||
|
|
||||||
|
# --- TLM3D Vektor (Strassen/Wasser/Bahn/Vegetation) ---
|
||||||
|
if "tlm" in kinds:
|
||||||
|
tlm_kinds = opts.get("tlmKinds") or []
|
||||||
|
if tlm_kinds:
|
||||||
|
self._push_log("TLM3D Vektor holen ({} Kategorien)...".format(
|
||||||
|
len(tlm_kinds)))
|
||||||
|
try:
|
||||||
|
tlm_paths = swisstopo.fetch_tlm3d_vector(
|
||||||
|
bbox, tlm_kinds, progress=self._push_log)
|
||||||
|
except Exception as ex:
|
||||||
|
self._push_log("TLM Fetch-Fehler: {}".format(ex))
|
||||||
|
tlm_paths = {}
|
||||||
|
# Layer-Mapping: TLM-Kategorie → Dossier-Ebenen-Code
|
||||||
|
tlm_layer_map = {
|
||||||
|
"streets": "11", # 11_Strasse (Default-Ebene)
|
||||||
|
"waterways": "15", # 15_Gewässer (auto-add)
|
||||||
|
"railways": "16", # 16_Bahn (auto-add)
|
||||||
|
"landcover": "13", # 13_Bäume (Default-Ebene)
|
||||||
|
}
|
||||||
|
tlm_fallback_names = {
|
||||||
|
"11": "11_Strasse", "13": "13_Bäume",
|
||||||
|
"15": "15_Gewässer", "16": "16_Bahn",
|
||||||
|
}
|
||||||
|
for cat, paths_list in tlm_paths.items():
|
||||||
|
for tlm_p in paths_list:
|
||||||
|
self._push_log("Import TLM {}: {}".format(
|
||||||
|
cat, os.path.basename(tlm_p)))
|
||||||
|
before_tlm = set(o.Id for o in d.Objects
|
||||||
|
if o and not o.IsDeleted)
|
||||||
|
cmd = '_-Import "{}" _Enter'.format(
|
||||||
|
tlm_p.replace('"', '\\"'))
|
||||||
|
try: Rhino.RhinoApp.RunScript(cmd, False)
|
||||||
|
except Exception as ex:
|
||||||
|
self._push_log(" Import-Fail: {}".format(ex))
|
||||||
|
continue
|
||||||
|
new_tlm = [o for o in d.Objects
|
||||||
|
if o and not o.IsDeleted
|
||||||
|
and o.Id not in before_tlm]
|
||||||
|
self._push_log(" → {} Objekte".format(len(new_tlm)))
|
||||||
|
# Auto-Skala falls noetig (gleiche Logik wie Buildings)
|
||||||
|
if new_tlm and abs(eC) > 1.0:
|
||||||
|
try:
|
||||||
|
import math as _m
|
||||||
|
sx = sum(o.Geometry.GetBoundingBox(True).Center.X
|
||||||
|
for o in new_tlm[:30]) / min(30, len(new_tlm))
|
||||||
|
ratio = (eC * m_to_unit) / sx if sx else 1
|
||||||
|
snap = 10 ** round(_m.log10(abs(ratio)))
|
||||||
|
if abs(snap - 1.0) > 0.01:
|
||||||
|
self._push_log(" TLM Auto-Skala {}×".format(snap))
|
||||||
|
self._apply_xform_fast(d, new_tlm,
|
||||||
|
scale_factor=snap,
|
||||||
|
translate=(-origin_shift_doc[0],
|
||||||
|
-origin_shift_doc[1], 0))
|
||||||
|
elif shift:
|
||||||
|
self._apply_xform_fast(d, new_tlm,
|
||||||
|
translate=(-origin_shift_doc[0],
|
||||||
|
-origin_shift_doc[1], 0))
|
||||||
|
except Exception as ex:
|
||||||
|
self._push_log(" TLM Skala/Shift: {}".format(ex))
|
||||||
|
# Layer + Tag
|
||||||
|
code = tlm_layer_map.get(cat)
|
||||||
|
fallback = tlm_fallback_names.get(code)
|
||||||
|
if z_id and new_tlm and code:
|
||||||
|
self._move_to_sublayer(d, new_tlm, z_id,
|
||||||
|
code, tag="tlm_" + cat,
|
||||||
|
fallback_name=fallback,
|
||||||
|
fallback_color="#707080")
|
||||||
|
elif new_tlm:
|
||||||
|
self._tag_objects(d, new_tlm, "tlm_" + cat)
|
||||||
|
new_obj_ids.extend(o.Id for o in new_tlm)
|
||||||
|
|
||||||
self._push_log("Import fertig: {} neue Objekte".format(len(new_obj_ids)))
|
self._push_log("Import fertig: {} neue Objekte".format(len(new_obj_ids)))
|
||||||
|
|
||||||
# Auto-Zoom NOCH IM TRY-Block: sticky-Flag bleibt True
|
# Auto-Zoom NOCH IM TRY-Block: sticky-Flag bleibt True
|
||||||
@@ -7279,11 +7491,15 @@ class ElementeBridge(panel_base.BaseBridge):
|
|||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
self._push_log(" ortho-per-tile: {}".format(ex))
|
self._push_log(" ortho-per-tile: {}".format(ex))
|
||||||
|
|
||||||
def _ensure_ortho_tile_ebenen(self, doc, tile_ids):
|
def _ensure_swisstopo_subebenen(self, doc):
|
||||||
"""Registriert jeden Tile als Child unter '80_swisstopo' in
|
"""Stellt sicher dass 80_swisstopo zwei Children hat:
|
||||||
dossier_ebenen JSON, baut Layer einmal synchron, broadcastet
|
'Terrain' (Drape-Mesh — Foto folgt Topographie) und
|
||||||
an die UI. Duplikate werden uebersprungen."""
|
'Luftbild' (flache Picture ueber max-Z — fuer 2D-Zeichnen).
|
||||||
if not tile_ids: return
|
Liefert {'terrain': '80T', 'luftbild': '80L'}."""
|
||||||
|
CHILD_SPEC = [
|
||||||
|
("80T", "Terrain", "#909090", "terrain"),
|
||||||
|
("80L", "Luftbild", "#888888", "luftbild"),
|
||||||
|
]
|
||||||
raw = doc.Strings.GetValue("dossier_ebenen")
|
raw = doc.Strings.GetValue("dossier_ebenen")
|
||||||
try: ebenen = json.loads(raw) if raw else []
|
try: ebenen = json.loads(raw) if raw else []
|
||||||
except Exception: ebenen = []
|
except Exception: ebenen = []
|
||||||
@@ -7303,27 +7519,27 @@ class ElementeBridge(panel_base.BaseBridge):
|
|||||||
have = {c.get("code") for c in parent["children"]
|
have = {c.get("code") for c in parent["children"]
|
||||||
if isinstance(c, dict)}
|
if isinstance(c, dict)}
|
||||||
changed = False
|
changed = False
|
||||||
for tile_id in set(tile_ids):
|
for ccode, cname, ccol, _key in CHILD_SPEC:
|
||||||
if tile_id in have: continue
|
if ccode not in have:
|
||||||
parent["children"].append({
|
parent["children"].append({
|
||||||
"code": tile_id, "name": "Ortho",
|
"code": ccode, "name": cname, "color": ccol,
|
||||||
"color": "#909090", "lw": 0.13,
|
"lw": 0.13, "visible": True, "locked": False,
|
||||||
"visible": True, "locked": False,
|
})
|
||||||
})
|
changed = True
|
||||||
changed = True
|
if changed:
|
||||||
if not changed: return
|
try:
|
||||||
try:
|
doc.Strings.SetString("dossier_ebenen",
|
||||||
doc.Strings.SetString("dossier_ebenen",
|
json.dumps(ebenen, ensure_ascii=False))
|
||||||
json.dumps(ebenen, ensure_ascii=False))
|
import layer_builder
|
||||||
import layer_builder
|
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
|
||||||
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
|
zlist = json.loads(z_raw) if z_raw else []
|
||||||
zlist = json.loads(z_raw) if z_raw else []
|
if zlist:
|
||||||
if zlist:
|
layer_builder.build_layers(doc, zlist, ebenen)
|
||||||
layer_builder.build_layers(doc, zlist, ebenen)
|
import rhinopanel
|
||||||
import rhinopanel
|
rhinopanel._broadcast_state(doc)
|
||||||
rhinopanel._broadcast_state(doc)
|
except Exception as ex:
|
||||||
except Exception as ex:
|
self._push_log(" swisstopo-ebenen build: {}".format(ex))
|
||||||
self._push_log(" ortho-ebenen build: {}".format(ex))
|
return {key: ccode for ccode, _n, _col, key in CHILD_SPEC}
|
||||||
|
|
||||||
def _ensure_sub_sublayer(self, doc, parent_id, name,
|
def _ensure_sub_sublayer(self, doc, parent_id, name,
|
||||||
color_hex="#888888", lw=0.25):
|
color_hex="#888888", lw=0.25):
|
||||||
@@ -7557,6 +7773,218 @@ class ElementeBridge(panel_base.BaseBridge):
|
|||||||
size=(560, 620),
|
size=(560, 620),
|
||||||
bridge=b)
|
bridge=b)
|
||||||
|
|
||||||
|
def _cmd_open_osm_dialog(self, p):
|
||||||
|
"""Oeffnet das OSM-Importer-Satelliten-Fenster mit Overpass-API:
|
||||||
|
Strassen, Gebaeudeumrisse, Wasser, Gruenflaechen, Wege als 2D-Linien."""
|
||||||
|
outer = self
|
||||||
|
bridge_holder = {"form": None}
|
||||||
|
|
||||||
|
class _OsmBridge(panel_base.BaseBridge):
|
||||||
|
def __init__(self):
|
||||||
|
panel_base.BaseBridge.__init__(self, "osm")
|
||||||
|
def _push_log(self, msg):
|
||||||
|
try: self.send("OSM_LOG", {"msg": str(msg)})
|
||||||
|
except Exception: pass
|
||||||
|
def handle(self, data):
|
||||||
|
if not isinstance(data, dict): return
|
||||||
|
t = data.get("type", "")
|
||||||
|
pp = data.get("payload") or {}
|
||||||
|
if t == "READY":
|
||||||
|
pass # nothing to send initially
|
||||||
|
elif t == "GEOCODE":
|
||||||
|
import swisstopo
|
||||||
|
res = swisstopo.geocode(pp.get("text") or "")
|
||||||
|
self.send("GEOCODE_RESULT", {"result": res})
|
||||||
|
elif t == "RUN_OSM_IMPORT":
|
||||||
|
self._run_osm_import(pp)
|
||||||
|
elif t == "CANCEL":
|
||||||
|
try:
|
||||||
|
f = bridge_holder.get("form")
|
||||||
|
if f is not None: f.Close()
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
def _run_osm_import(self, opts):
|
||||||
|
d = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
if d is None:
|
||||||
|
self._push_log("Kein aktives Doc"); return
|
||||||
|
try:
|
||||||
|
import osm, swisstopo, layer_builder
|
||||||
|
except Exception as ex:
|
||||||
|
self._push_log("Module-Import-Fehler: {}".format(ex)); return
|
||||||
|
try:
|
||||||
|
eC = float(opts.get("centerE"))
|
||||||
|
nC = float(opts.get("centerN"))
|
||||||
|
r = float(opts.get("radius") or 200)
|
||||||
|
except Exception:
|
||||||
|
self._push_log("Center/Radius ungueltig"); return
|
||||||
|
categories = opts.get("categories") or []
|
||||||
|
if not categories:
|
||||||
|
self._push_log("Keine Kategorien gewaehlt"); return
|
||||||
|
shift = bool(opts.get("shiftToOrigin", True))
|
||||||
|
replace_existing = bool(opts.get("replaceExisting", True))
|
||||||
|
# Doc-Unit
|
||||||
|
try:
|
||||||
|
m_to_unit = Rhino.RhinoMath.UnitScale(
|
||||||
|
Rhino.UnitSystem.Meters, d.ModelUnitSystem)
|
||||||
|
except Exception:
|
||||||
|
m_to_unit = 1.0
|
||||||
|
# Projekt-Nullpunkt (z-Offset wie bei swisstopo)
|
||||||
|
try:
|
||||||
|
z_raw = d.Strings.GetValue("dossier_project_zero_mum")
|
||||||
|
project_zero_mum = float(z_raw) if z_raw else 0.0
|
||||||
|
except Exception:
|
||||||
|
project_zero_mum = 0.0
|
||||||
|
z_offset_m = project_zero_mum if shift else 0.0
|
||||||
|
# bbox in LV95-Metern + WGS84 fuer Overpass
|
||||||
|
bbox_lv95 = (eC - r, nC - r, eC + r, nC + r)
|
||||||
|
bbox_wgs = swisstopo.lv95_bbox_to_wgs84_bbox(*bbox_lv95)
|
||||||
|
self._push_log("Center LV95: E={:.1f} N={:.1f} Radius={}m".format(eC, nC, r))
|
||||||
|
self._push_log("BBox WGS84: {:.5f},{:.5f} – {:.5f},{:.5f}".format(*bbox_wgs))
|
||||||
|
origin_shift = (eC, nC, z_offset_m) if shift else (0, 0, 0)
|
||||||
|
z_id = d.Strings.GetValue("dossier_active_id")
|
||||||
|
|
||||||
|
# Listener-Suppression
|
||||||
|
sc.sticky["dossier_swisstopo_busy"] = True
|
||||||
|
try:
|
||||||
|
# Bestehende OSM-Objekte loeschen?
|
||||||
|
if replace_existing:
|
||||||
|
self._push_log("Loesche bestehende OSM-Objekte...")
|
||||||
|
removed = 0
|
||||||
|
for obj in list(d.Objects):
|
||||||
|
if obj is None or obj.IsDeleted: continue
|
||||||
|
try:
|
||||||
|
tag = obj.Attributes.GetUserString("dossier_osm_kind")
|
||||||
|
except Exception: tag = None
|
||||||
|
if tag:
|
||||||
|
d.Objects.Delete(obj.Id, True); removed += 1
|
||||||
|
self._push_log("→ {} alte OSM-Objekte geloescht".format(removed))
|
||||||
|
# Sub-Ebenen-Struktur unter '70_osm' sicherstellen
|
||||||
|
osm_sub_codes = self._ensure_osm_ebenen(d, categories)
|
||||||
|
# Layer-Indices ermitteln
|
||||||
|
cat_layer_idx = {}
|
||||||
|
if z_id:
|
||||||
|
parent_idx = layer_builder._find_top_by_id(d, z_id)
|
||||||
|
if parent_idx >= 0:
|
||||||
|
parent_id_ = d.Layers[parent_idx].Id
|
||||||
|
base_idx = layer_builder._find_sublayer_by_code(
|
||||||
|
d, parent_id_, "70")
|
||||||
|
if base_idx >= 0:
|
||||||
|
base_id_ = d.Layers[base_idx].Id
|
||||||
|
for cat, ccode in osm_sub_codes.items():
|
||||||
|
idx = layer_builder._find_sublayer_by_code(
|
||||||
|
d, base_id_, ccode)
|
||||||
|
if idx >= 0: cat_layer_idx[cat] = idx
|
||||||
|
# Import via osm-Modul
|
||||||
|
self._push_log("Hole OSM-Daten...")
|
||||||
|
created = osm.import_osm_to_doc(
|
||||||
|
d, bbox_wgs, categories,
|
||||||
|
shift_lv95=origin_shift,
|
||||||
|
m_to_unit=m_to_unit,
|
||||||
|
z_doc=0.0,
|
||||||
|
progress=self._push_log)
|
||||||
|
# Layer-Move + Tag pro Objekt
|
||||||
|
new_obj_ids = []
|
||||||
|
moved_by_cat = {}
|
||||||
|
for item in created:
|
||||||
|
cat = item["category"]
|
||||||
|
obj = item["obj"]
|
||||||
|
tgt_idx = cat_layer_idx.get(cat, -1)
|
||||||
|
try:
|
||||||
|
at = obj.Attributes.Duplicate()
|
||||||
|
if tgt_idx >= 0: at.LayerIndex = tgt_idx
|
||||||
|
at.SetUserString("dossier_osm_kind", cat)
|
||||||
|
d.Objects.ModifyAttributes(obj, at, True)
|
||||||
|
new_obj_ids.append(obj.Id)
|
||||||
|
moved_by_cat[cat] = moved_by_cat.get(cat, 0) + 1
|
||||||
|
except Exception: pass
|
||||||
|
for cat, n in moved_by_cat.items():
|
||||||
|
if cat in cat_layer_idx:
|
||||||
|
self._push_log(" → {} {} auf '{}'".format(
|
||||||
|
n, cat, d.Layers[cat_layer_idx[cat]].FullPath))
|
||||||
|
else:
|
||||||
|
self._push_log(" → {} {} (Layer fallback)".format(n, cat))
|
||||||
|
self._push_log("Import fertig: {} OSM-Objekte".format(
|
||||||
|
len(new_obj_ids)))
|
||||||
|
# Auto-Zoom
|
||||||
|
if opts.get("autoZoom") and new_obj_ids:
|
||||||
|
try:
|
||||||
|
combined = rg.BoundingBox.Empty
|
||||||
|
for oid in new_obj_ids:
|
||||||
|
ob = d.Objects.Find(oid)
|
||||||
|
if ob is None: continue
|
||||||
|
bb = ob.Geometry.GetBoundingBox(True)
|
||||||
|
if bb.IsValid: combined.Union(bb)
|
||||||
|
if combined.IsValid:
|
||||||
|
view = d.Views.ActiveView
|
||||||
|
if view is not None:
|
||||||
|
view.ActiveViewport.ZoomBoundingBox(combined)
|
||||||
|
except Exception as ex:
|
||||||
|
self._push_log("Auto-Zoom: {}".format(ex))
|
||||||
|
try: d.Views.Redraw()
|
||||||
|
except Exception: pass
|
||||||
|
self.send("IMPORT_DONE", {"count": len(new_obj_ids)})
|
||||||
|
finally:
|
||||||
|
sc.sticky["dossier_swisstopo_busy"] = False
|
||||||
|
|
||||||
|
def _ensure_osm_ebenen(self, doc, categories):
|
||||||
|
"""Stellt sicher dass '70_osm' Parent + Children fuer jede
|
||||||
|
gewuenschte Kategorie in dossier_ebenen existieren. Liefert
|
||||||
|
{category_key: code} Map."""
|
||||||
|
import osm
|
||||||
|
raw = doc.Strings.GetValue("dossier_ebenen")
|
||||||
|
try: ebenen = json.loads(raw) if raw else []
|
||||||
|
except Exception: ebenen = []
|
||||||
|
if not isinstance(ebenen, list): ebenen = []
|
||||||
|
parent = next((e for e in ebenen if isinstance(e, dict)
|
||||||
|
and e.get("code") == "70"), None)
|
||||||
|
if parent is None:
|
||||||
|
parent = {
|
||||||
|
"code": "70", "name": "osm",
|
||||||
|
"color": "#707080", "lw": 0.13,
|
||||||
|
"visible": True, "locked": False,
|
||||||
|
"children": [],
|
||||||
|
}
|
||||||
|
ebenen.append(parent)
|
||||||
|
if not isinstance(parent.get("children"), list):
|
||||||
|
parent["children"] = []
|
||||||
|
have = {c.get("code") for c in parent["children"]
|
||||||
|
if isinstance(c, dict)}
|
||||||
|
code_map = {}
|
||||||
|
changed = False
|
||||||
|
for cat_key in categories:
|
||||||
|
spec = osm.CATEGORIES.get(cat_key)
|
||||||
|
if not spec: continue
|
||||||
|
code = spec["code"]
|
||||||
|
code_map[cat_key] = code
|
||||||
|
if code in have: continue
|
||||||
|
parent["children"].append({
|
||||||
|
"code": code, "name": spec["name"],
|
||||||
|
"color": spec["color"], "lw": 0.13,
|
||||||
|
"visible": True, "locked": False,
|
||||||
|
})
|
||||||
|
changed = True
|
||||||
|
if changed:
|
||||||
|
try:
|
||||||
|
doc.Strings.SetString("dossier_ebenen",
|
||||||
|
json.dumps(ebenen, ensure_ascii=False))
|
||||||
|
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
|
||||||
|
zlist = json.loads(z_raw) if z_raw else []
|
||||||
|
if zlist:
|
||||||
|
import layer_builder
|
||||||
|
layer_builder.build_layers(doc, zlist, ebenen)
|
||||||
|
import rhinopanel
|
||||||
|
rhinopanel._broadcast_state(doc)
|
||||||
|
except Exception as ex:
|
||||||
|
self._push_log("osm-ebenen build: {}".format(ex))
|
||||||
|
return code_map
|
||||||
|
|
||||||
|
b = _OsmBridge()
|
||||||
|
bridge_holder["form"] = panel_base.open_satellite_window(
|
||||||
|
"osm",
|
||||||
|
title="OSM Importer",
|
||||||
|
size=(520, 620),
|
||||||
|
bridge=b)
|
||||||
|
|
||||||
def _update_wall(self, p):
|
def _update_wall(self, p):
|
||||||
"""Properties eines Elements aendern (Wand/Decke/Dach/Oeffnung).
|
"""Properties eines Elements aendern (Wand/Decke/Dach/Oeffnung).
|
||||||
Volumen wird anschliessend regeneriert."""
|
Volumen wird anschliessend regeneriert."""
|
||||||
@@ -8547,6 +8975,9 @@ def _on_object_deleted(sender, e):
|
|||||||
"""
|
"""
|
||||||
# Waehrend Swisstopo-Import: keine DOSSIER-Metas vorhanden, nur Overhead
|
# Waehrend Swisstopo-Import: keine DOSSIER-Metas vorhanden, nur Overhead
|
||||||
if sc.sticky.get("dossier_swisstopo_busy"): return
|
if sc.sticky.get("dossier_swisstopo_busy"): return
|
||||||
|
# Bulk-Delete (z.B. SelAll + Delete bei 6000 OSM-Curves): pro-Event-
|
||||||
|
# Arbeit waere reiner Overhead. CommandEnd refresht einmalig.
|
||||||
|
if sc.sticky.get(_BULK_ACTIVE_KEY): return
|
||||||
# Waehrend Move/Rotate/Mirror/Scale: CommandEnd-Pfad uebernimmt das
|
# Waehrend Move/Rotate/Mirror/Scale: CommandEnd-Pfad uebernimmt das
|
||||||
# Re-Sync. Sonst queued der Delete-Event ueberfluessige Regen-Calls die
|
# Re-Sync. Sonst queued der Delete-Event ueberfluessige Regen-Calls die
|
||||||
# den Pure-Translate-Skip wieder zunichtemachen.
|
# den Pure-Translate-Skip wieder zunichtemachen.
|
||||||
@@ -8794,6 +9225,9 @@ def _on_idle_selection(sender, e):
|
|||||||
Replace-Event) ausgefuehrt. So vermeiden wir Volume-Flicker waehrend
|
Replace-Event) ausgefuehrt. So vermeiden wir Volume-Flicker waehrend
|
||||||
fortlaufenden Gumball-/Move-Operationen — der finale Regen rendert
|
fortlaufenden Gumball-/Move-Operationen — der finale Regen rendert
|
||||||
nach Drag-Ende, bis dahin uebernimmt Rhinos Transform die Geometrie."""
|
nach Drag-Ende, bis dahin uebernimmt Rhinos Transform die Geometrie."""
|
||||||
|
# Waehrend Bulk-Op (z.B. _Delete bei 6000 OSM-Curves): nicht pollen.
|
||||||
|
# Wuerde sonst pro Idle-Tick alle Objekte iterieren = Quasi-Stall.
|
||||||
|
if sc.sticky.get(_BULK_ACTIVE_KEY): return
|
||||||
b = sc.sticky.get("elemente_bridge")
|
b = sc.sticky.get("elemente_bridge")
|
||||||
if b is None: return
|
if b is None: return
|
||||||
doc = Rhino.RhinoDoc.ActiveDoc
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
@@ -8990,6 +9424,16 @@ _USER_TRANSFORM_CMDS = frozenset((
|
|||||||
"Drag", "Gumball", "Orient", "Orient3Pt", "RemapCPlane", "Transform",
|
"Drag", "Gumball", "Orient", "Orient3Pt", "RemapCPlane", "Transform",
|
||||||
))
|
))
|
||||||
|
|
||||||
|
# Bulk-Operations: User selektiert N Objekte + ausfuehrt die Operation
|
||||||
|
# einmal. Wir suspenden Redraws + Listener-Arbeit damit das nicht
|
||||||
|
# pro-Object visuell durchrieselt. Beispiel: SelAll + Delete bei 6000
|
||||||
|
# Curves → ohne Suspend dauert das ewig + man sieht jedes Element
|
||||||
|
# einzeln verschwinden.
|
||||||
|
_USER_BULK_CMDS = frozenset((
|
||||||
|
"Delete", "DeleteSelected", "DeleteSubObject", "Cut",
|
||||||
|
))
|
||||||
|
_BULK_ACTIVE_KEY = "_dossier_bulk_op_active"
|
||||||
|
|
||||||
# Undo/Redo: Rhino restored Objekte aus dem Undo-Stack → feuert Add/Delete-
|
# Undo/Redo: Rhino restored Objekte aus dem Undo-Stack → feuert Add/Delete-
|
||||||
# Events fuer ALLE betroffenen Objekte. Unsere Handler wuerden fuer jedes
|
# Events fuer ALLE betroffenen Objekte. Unsere Handler wuerden fuer jedes
|
||||||
# einen Regen queuen → Storm. Wir suppressen die Handler komplett; Undo hat
|
# einen Regen queuen → Storm. Wir suppressen die Handler komplett; Undo hat
|
||||||
@@ -9076,6 +9520,21 @@ def _on_command_begin(sender, e):
|
|||||||
if name in _USER_UNDO_CMDS:
|
if name in _USER_UNDO_CMDS:
|
||||||
sc.sticky[_UNDO_ACTIVE_KEY] = name
|
sc.sticky[_UNDO_ACTIVE_KEY] = name
|
||||||
return
|
return
|
||||||
|
# Bulk-Ops (z.B. _Delete mit 6000 Selektion): RedrawEnabled aus +
|
||||||
|
# Listener bail-out — am Ende einmal redrawn.
|
||||||
|
if name in _USER_BULK_CMDS:
|
||||||
|
sc.sticky[_BULK_ACTIVE_KEY] = name
|
||||||
|
print("[ELEMENTE] Bulk-Op start: '{}' — Listener bail aktiv".format(name))
|
||||||
|
try:
|
||||||
|
sc.sticky["_dossier_bulk_redraw_prev"] = bool(doc.Views.RedrawEnabled)
|
||||||
|
doc.Views.RedrawEnabled = False
|
||||||
|
except Exception: pass
|
||||||
|
return
|
||||||
|
# Diagnose: andere Commands sehen wir hier vorbeiziehen — wenn _Delete
|
||||||
|
# einen anderen Namen hat als 'Delete', sehen wir's und koennen den
|
||||||
|
# frozenset anpassen.
|
||||||
|
if name and "delete" in name.lower():
|
||||||
|
print("[ELEMENTE] CmdBegin '{}' (nicht im Bulk-Set — anpassen?)".format(name))
|
||||||
if name not in _USER_TRANSFORM_CMDS: return
|
if name not in _USER_TRANSFORM_CMDS: return
|
||||||
sc.sticky[_UT_SNAPSHOT_KEY] = _snapshot_source_positions(doc)
|
sc.sticky[_UT_SNAPSHOT_KEY] = _snapshot_source_positions(doc)
|
||||||
sc.sticky[_UT_ACTIVE_KEY] = name
|
sc.sticky[_UT_ACTIVE_KEY] = name
|
||||||
@@ -9095,6 +9554,22 @@ def _on_command_begin(sender, e):
|
|||||||
|
|
||||||
|
|
||||||
def _on_command_end(sender, e):
|
def _on_command_end(sender, e):
|
||||||
|
# Bulk-Op fertig: RedrawEnabled zurueck + EINMAL redrawn + selection
|
||||||
|
# refresh ans Gestaltung-Panel.
|
||||||
|
if sc.sticky.get(_BULK_ACTIVE_KEY):
|
||||||
|
sc.sticky[_BULK_ACTIVE_KEY] = None
|
||||||
|
try:
|
||||||
|
prev = sc.sticky.pop("_dossier_bulk_redraw_prev", True)
|
||||||
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
if doc is not None:
|
||||||
|
doc.Views.RedrawEnabled = prev
|
||||||
|
doc.Views.Redraw()
|
||||||
|
except Exception: pass
|
||||||
|
gb = sc.sticky.get("gestaltung_bridge")
|
||||||
|
if gb is not None:
|
||||||
|
try: gb._send_selection()
|
||||||
|
except Exception: pass
|
||||||
|
return
|
||||||
# Undo/Redo abschliessen: nur Flag clearen, kein Regen + ein Selection-
|
# Undo/Redo abschliessen: nur Flag clearen, kein Regen + ein Selection-
|
||||||
# Refresh fuers Gestaltung-Panel (Listener waren waehrend Undo aus).
|
# Refresh fuers Gestaltung-Panel (Listener waren waehrend Undo aus).
|
||||||
if sc.sticky.get(_UNDO_ACTIVE_KEY):
|
if sc.sticky.get(_UNDO_ACTIVE_KEY):
|
||||||
|
|||||||
+62
-23
@@ -302,9 +302,20 @@ def _ebene_fill_for_layer(doc, layer):
|
|||||||
print("[GESTALTUNG] _ebene_fill_for_layer: json-Fehler:", ex)
|
print("[GESTALTUNG] _ebene_fill_for_layer: json-Fehler:", ex)
|
||||||
return None
|
return None
|
||||||
if not isinstance(ebenen, list): return None
|
if not isinstance(ebenen, list): return None
|
||||||
for e in ebenen:
|
# Rekursiv durch Tree — Sub-Ebenen sind in children verschachtelt
|
||||||
if not isinstance(e, dict): continue
|
def _find_by_code(lst, target):
|
||||||
if e.get("code") != code: continue
|
for e in lst:
|
||||||
|
if not isinstance(e, dict): continue
|
||||||
|
if e.get("code") == target: return e
|
||||||
|
kids = e.get("children")
|
||||||
|
if isinstance(kids, list) and kids:
|
||||||
|
hit = _find_by_code(kids, target)
|
||||||
|
if hit is not None: return hit
|
||||||
|
return None
|
||||||
|
found = _find_by_code(ebenen, code)
|
||||||
|
if found is None: return None
|
||||||
|
e = found
|
||||||
|
if True:
|
||||||
f = e.get("fill")
|
f = e.get("fill")
|
||||||
if not isinstance(f, dict):
|
if not isinstance(f, dict):
|
||||||
print("[GESTALTUNG] _ebene_fill_for_layer: Ebene code={} hat KEIN fill-Feld".format(code))
|
print("[GESTALTUNG] _ebene_fill_for_layer: Ebene code={} hat KEIN fill-Feld".format(code))
|
||||||
@@ -471,19 +482,26 @@ def refresh_layer_fills(doc):
|
|||||||
if not isinstance(ebenen, list):
|
if not isinstance(ebenen, list):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
# Code -> fill-dict fuer schnellen Lookup
|
# Code -> fill-dict fuer schnellen Lookup. Rekursiv durch Children, damit
|
||||||
|
# Sub-Ebenen-Schraffuren auch wirken (sonst landen Polygone auf z.B.
|
||||||
|
# 70_osm/7102_Gebaeudeumrisse nie in der Auto-Fill-Logik).
|
||||||
|
def _walk_fills(lst, out):
|
||||||
|
for e in lst:
|
||||||
|
if not isinstance(e, dict): continue
|
||||||
|
f = e.get("fill")
|
||||||
|
if isinstance(f, dict) and f.get("pattern") not in (None, "None"):
|
||||||
|
out[e.get("code")] = {
|
||||||
|
"pattern": f.get("pattern"),
|
||||||
|
"source": f.get("source", "layer"),
|
||||||
|
"color": f.get("color"),
|
||||||
|
"scale": float(f.get("scale", 1.0)) if f.get("scale") is not None else 1.0,
|
||||||
|
"rotation": float(f.get("rotation", 0.0)) if f.get("rotation") is not None else 0.0,
|
||||||
|
}
|
||||||
|
kids = e.get("children")
|
||||||
|
if isinstance(kids, list) and kids:
|
||||||
|
_walk_fills(kids, out)
|
||||||
fill_by_code = {}
|
fill_by_code = {}
|
||||||
for e in ebenen:
|
_walk_fills(ebenen, fill_by_code)
|
||||||
if not isinstance(e, dict): continue
|
|
||||||
f = e.get("fill")
|
|
||||||
if isinstance(f, dict) and f.get("pattern") not in (None, "None"):
|
|
||||||
fill_by_code[e.get("code")] = {
|
|
||||||
"pattern": f.get("pattern"),
|
|
||||||
"source": f.get("source", "layer"),
|
|
||||||
"color": f.get("color"),
|
|
||||||
"scale": float(f.get("scale", 1.0)) if f.get("scale") is not None else 1.0,
|
|
||||||
"rotation": float(f.get("rotation", 0.0)) if f.get("rotation") is not None else 0.0,
|
|
||||||
}
|
|
||||||
if not fill_by_code:
|
if not fill_by_code:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
@@ -1367,23 +1385,38 @@ def _install_selection_listener(bridge):
|
|||||||
if sc.sticky.get(flag):
|
if sc.sticky.get(flag):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Selection-Refresh wird via Idle-Event debounced:
|
||||||
|
# Rhino feuert pro Object-Select/Deselect einzeln. Bei mass-Delete von
|
||||||
|
# 327 Objekten = 327 refresh-Calls → 327 IPC-Sends in den WebView →
|
||||||
|
# UI haengt + Command-History wird mit '[GESTALTUNG] sel: n=N'
|
||||||
|
# zugemuellt. Wir setzen nur ein Dirty-Flag und feuern EINMAL beim
|
||||||
|
# naechsten Idle-Tick.
|
||||||
def refresh(*args):
|
def refresh(*args):
|
||||||
# Waehrend Move/Rotate/Mirror/Scale schweigen — Rhino oszilliert die
|
|
||||||
# Selection pro transformiertem Object mehrfach (deselect→delete→add→
|
|
||||||
# reselect). Bei 7 Objekten sind das ~100 IPC-Sends in den WebView,
|
|
||||||
# was sich als „Regen" anfuehlt. elemente._on_command_end refresht
|
|
||||||
# nach dem Command einmalig.
|
|
||||||
# Waehrend Swisstopo-Import: Rhino selektiert jedes neu importierte
|
|
||||||
# Objekt → 5000 selection-changes → 5000 send-Calls in den WebView →
|
|
||||||
# erstickt den UI-Thread. Sticky-Flag => bail.
|
|
||||||
if sc.sticky.get("dossier_swisstopo_busy"): return
|
if sc.sticky.get("dossier_swisstopo_busy"): return
|
||||||
if sc.sticky.get("_dossier_user_transform_active"): return
|
if sc.sticky.get("_dossier_user_transform_active"): return
|
||||||
if sc.sticky.get("_dossier_undo_active"): return
|
if sc.sticky.get("_dossier_undo_active"): return
|
||||||
|
sc.sticky["_gestaltung_selection_dirty"] = True
|
||||||
|
|
||||||
|
def on_idle_flush(sender, args):
|
||||||
|
if not sc.sticky.get("_gestaltung_selection_dirty"): return
|
||||||
|
if sc.sticky.get("dossier_swisstopo_busy"): return
|
||||||
|
if sc.sticky.get("_dossier_user_transform_active"): return
|
||||||
|
if sc.sticky.get("_dossier_undo_active"): return
|
||||||
|
if sc.sticky.get("_dossier_bulk_op_active"): return
|
||||||
|
sc.sticky["_gestaltung_selection_dirty"] = False
|
||||||
b = sc.sticky.get("gestaltung_bridge")
|
b = sc.sticky.get("gestaltung_bridge")
|
||||||
if b is not None:
|
if b is not None:
|
||||||
try: b._send_selection()
|
try: b._send_selection()
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
|
|
||||||
|
# Idle-Hook nur einmal aufhaengen (sticky guard)
|
||||||
|
if not sc.sticky.get("_gestaltung_idle_attached"):
|
||||||
|
try:
|
||||||
|
Rhino.RhinoApp.Idle += on_idle_flush
|
||||||
|
sc.sticky["_gestaltung_idle_attached"] = True
|
||||||
|
except Exception as ex:
|
||||||
|
print("[GESTALTUNG] Idle-Hook fail:", ex)
|
||||||
|
|
||||||
def on_replace(sender, args):
|
def on_replace(sender, args):
|
||||||
"""Sync Curve↔Hatch bei Move/Replace:
|
"""Sync Curve↔Hatch bei Move/Replace:
|
||||||
- Curve hat _FILL_KEY (= hatch_id) → Hatch via Hatch.Create neu auf die
|
- Curve hat _FILL_KEY (= hatch_id) → Hatch via Hatch.Create neu auf die
|
||||||
@@ -1484,6 +1517,12 @@ def _install_selection_listener(bridge):
|
|||||||
Curve aufraeumen damit beim naechsten Toggle keine Geister-Referenz steht."""
|
Curve aufraeumen damit beim naechsten Toggle keine Geister-Referenz steht."""
|
||||||
if sc.sticky.get("_dossier_undo_active"): return
|
if sc.sticky.get("_dossier_undo_active"): return
|
||||||
if sc.sticky.get("_elemente_regen_busy"): return
|
if sc.sticky.get("_elemente_regen_busy"): return
|
||||||
|
# Bulk-Delete (SelAll + Delete): pro-Object Hatch-Sync ueberspringen
|
||||||
|
# — bei 6000 Objekten waere das massive Overhead. Hatch-Verweise
|
||||||
|
# wuerden zwar nicht aufgeraeumt aber das ist tolerierbar
|
||||||
|
# (Sticky-Cache laeuft auch ohne Cleanup ab, alte Eintraege bleiben
|
||||||
|
# nur unsichtbar liegen).
|
||||||
|
if sc.sticky.get("_dossier_bulk_op_active"): return
|
||||||
obj = args.TheObject
|
obj = args.TheObject
|
||||||
if obj is None or obj.Id in _processing:
|
if obj is None or obj.Id in _processing:
|
||||||
return
|
return
|
||||||
|
|||||||
+21
-2
@@ -653,14 +653,33 @@ def cleanup_default_layers(doc):
|
|||||||
print("[EBENEN] Default-Layer entfernt: {}".format(", ".join(deleted)))
|
print("[EBENEN] Default-Layer entfernt: {}".format(", ".join(deleted)))
|
||||||
|
|
||||||
|
|
||||||
|
def _find_sublayer_by_code_recursive(doc, parent_id, code):
|
||||||
|
"""Sucht einen Sub-Layer mit `code` unter parent_id — auch tief
|
||||||
|
verschachtelt (Sub-Sub-Layer mit gleichem Code-Prefix). Liefert
|
||||||
|
layer_index oder -1."""
|
||||||
|
prefix = code + "_"
|
||||||
|
direct = []
|
||||||
|
for i, layer in enumerate(doc.Layers):
|
||||||
|
if layer is None or layer.IsDeleted: continue
|
||||||
|
if layer.ParentLayerId == parent_id:
|
||||||
|
if layer.Name.startswith(prefix): return i
|
||||||
|
direct.append(layer.Id)
|
||||||
|
for child_id in direct:
|
||||||
|
idx = _find_sublayer_by_code_recursive(doc, child_id, code)
|
||||||
|
if idx >= 0: return idx
|
||||||
|
return -1
|
||||||
|
|
||||||
|
|
||||||
def set_active_sublayer(doc, zeichnungsebene_id, code):
|
def set_active_sublayer(doc, zeichnungsebene_id, code):
|
||||||
"""Macht den Sublayer 'code' unter Zeichnungsebene 'zeichnungsebene_id' aktiv."""
|
"""Macht den Sublayer 'code' unter Zeichnungsebene 'zeichnungsebene_id'
|
||||||
|
aktiv. Sucht rekursiv durch verschachtelte Sub-Layer (z.B. 70_osm/
|
||||||
|
7101_Strassen liegt zwei Ebenen tief)."""
|
||||||
parent_idx = _find_top_by_id(doc, zeichnungsebene_id)
|
parent_idx = _find_top_by_id(doc, zeichnungsebene_id)
|
||||||
if parent_idx < 0:
|
if parent_idx < 0:
|
||||||
print("[EBENEN] Parent-Layer fuer Zeichnungsebene {} nicht gefunden".format(zeichnungsebene_id))
|
print("[EBENEN] Parent-Layer fuer Zeichnungsebene {} nicht gefunden".format(zeichnungsebene_id))
|
||||||
return
|
return
|
||||||
parent_id = doc.Layers[parent_idx].Id
|
parent_id = doc.Layers[parent_idx].Id
|
||||||
sub_idx = _find_sublayer_by_code(doc, parent_id, code)
|
sub_idx = _find_sublayer_by_code_recursive(doc, parent_id, code)
|
||||||
if sub_idx >= 0:
|
if sub_idx >= 0:
|
||||||
doc.Layers.SetCurrentLayerIndex(sub_idx, True)
|
doc.Layers.SetCurrentLayerIndex(sub_idx, True)
|
||||||
else:
|
else:
|
||||||
|
|||||||
+189
@@ -0,0 +1,189 @@
|
|||||||
|
#! python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
OSM-Importer fuer Dossier — holt OpenStreetMap-Daten via Overpass-API als
|
||||||
|
Polylinien (Strassen, Gebaeudeumrisse, Wasser, Gruenflaechen, Wege).
|
||||||
|
|
||||||
|
Pipeline:
|
||||||
|
Adresse → bbox (LV95) → bbox (WGS84) → Overpass-Query →
|
||||||
|
JSON-Response → OSM-Ways → Polylinien (in Doc-Units) → Rhino-Layer
|
||||||
|
|
||||||
|
Koord-Konversion WGS84↔LV95 nutzt swisstopo.wgs84_to_lv95 (LV95 ist die
|
||||||
|
gemeinsame Basis mit dem swisstopo-Importer).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import urllib.request
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
import Rhino
|
||||||
|
import Rhino.Geometry as rg
|
||||||
|
|
||||||
|
import swisstopo # fuer wgs84_to_lv95
|
||||||
|
|
||||||
|
|
||||||
|
OVERPASS_URL = "https://overpass-api.de/api/interpreter"
|
||||||
|
|
||||||
|
|
||||||
|
# --- Kategorien ------------------------------------------------------------
|
||||||
|
# Jede Kategorie liefert (Overpass-Selektor, Layer-Code, Layer-Name, Color).
|
||||||
|
# Codes 7100-7199 reserviert fuer OSM-Sub-Ebenen unter '70_osm'.
|
||||||
|
CATEGORIES = {
|
||||||
|
"streets": {
|
||||||
|
"selector": '[highway~"^(motorway|trunk|primary|secondary|tertiary|residential|unclassified|service|living_street|pedestrian)$"]',
|
||||||
|
"code": "7101", "name": "Strassen", "color": "#a89070",
|
||||||
|
},
|
||||||
|
"buildings": {
|
||||||
|
"selector": '[building]',
|
||||||
|
"code": "7102", "name": "Gebaeudeumrisse", "color": "#888888",
|
||||||
|
"include_relations": True,
|
||||||
|
},
|
||||||
|
"water": {
|
||||||
|
"selector": '[natural=water]',
|
||||||
|
"code": "7103", "name": "Wasser", "color": "#4080a0",
|
||||||
|
"include_relations": True,
|
||||||
|
},
|
||||||
|
"waterways": {
|
||||||
|
"selector": '[waterway~"^(river|stream|canal)$"]',
|
||||||
|
"code": "7104", "name": "Wasserlaeufe", "color": "#4080a0",
|
||||||
|
},
|
||||||
|
"parks": {
|
||||||
|
"selector": '[leisure~"^(park|garden)$"]',
|
||||||
|
"code": "7105", "name": "Parks", "color": "#60a070",
|
||||||
|
"include_relations": True,
|
||||||
|
},
|
||||||
|
"forest": {
|
||||||
|
"selector": '[landuse~"^(forest|grass|meadow)$"]',
|
||||||
|
"code": "7106", "name": "Wald_Gruen", "color": "#406050",
|
||||||
|
"include_relations": True,
|
||||||
|
},
|
||||||
|
"footpaths": {
|
||||||
|
"selector": '[highway~"^(footway|path|track|cycleway)$"]',
|
||||||
|
"code": "7107", "name": "Wege", "color": "#806040",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_overpass_query(bbox_wgs, categories):
|
||||||
|
"""Baut die Overpass-QL-Query fuer bbox + ausgewaehlte Kategorien.
|
||||||
|
bbox_wgs: (min_lon, min_lat, max_lon, max_lat) — WGS84."""
|
||||||
|
south = bbox_wgs[1]; west = bbox_wgs[0]
|
||||||
|
north = bbox_wgs[3]; east = bbox_wgs[2]
|
||||||
|
bbox_str = "{},{},{},{}".format(south, west, north, east)
|
||||||
|
parts = []
|
||||||
|
for cat in categories:
|
||||||
|
spec = CATEGORIES.get(cat)
|
||||||
|
if not spec: continue
|
||||||
|
parts.append('way{}({});'.format(spec["selector"], bbox_str))
|
||||||
|
if spec.get("include_relations"):
|
||||||
|
parts.append('relation{}({});'.format(spec["selector"], bbox_str))
|
||||||
|
body = ''.join(parts)
|
||||||
|
return '[out:json][timeout:60];({});out body;>;out skel qt;'.format(body)
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_overpass(bbox_wgs, categories, progress=None):
|
||||||
|
"""Schickt Overpass-Query, liefert JSON-Dict oder None."""
|
||||||
|
q = build_overpass_query(bbox_wgs, categories)
|
||||||
|
if progress: progress("Overpass-Query ({} Kategorien)...".format(len(categories)))
|
||||||
|
data = urllib.parse.urlencode({"data": q}).encode("utf-8")
|
||||||
|
req = urllib.request.Request(OVERPASS_URL, data=data, method="POST",
|
||||||
|
headers={"User-Agent": "Dossier/OSM-Importer"})
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=180) as resp:
|
||||||
|
text = resp.read().decode("utf-8", errors="ignore")
|
||||||
|
out = json.loads(text)
|
||||||
|
if progress: progress("Antwort: {} Elemente".format(len(out.get("elements", []))))
|
||||||
|
return out
|
||||||
|
except Exception as ex:
|
||||||
|
if progress: progress("Overpass fail: {}".format(ex))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_osm_elements(osm_json):
|
||||||
|
"""Zerlegt OSM-JSON in {nodes: {id: (lon, lat)}, ways: [{id, nodes, tags}]}."""
|
||||||
|
if not osm_json: return None
|
||||||
|
nodes = {}
|
||||||
|
ways = []
|
||||||
|
for el in osm_json.get("elements", []):
|
||||||
|
t = el.get("type")
|
||||||
|
if t == "node":
|
||||||
|
nodes[el["id"]] = (el["lon"], el["lat"])
|
||||||
|
elif t == "way":
|
||||||
|
ways.append({
|
||||||
|
"id": el["id"],
|
||||||
|
"nodes": el.get("nodes", []),
|
||||||
|
"tags": el.get("tags") or {},
|
||||||
|
})
|
||||||
|
return {"nodes": nodes, "ways": ways}
|
||||||
|
|
||||||
|
|
||||||
|
def classify_way(tags):
|
||||||
|
"""Mappt Way-Tags auf eine Kategorie-Key (oder None falls uninteressant)."""
|
||||||
|
if not tags: return None
|
||||||
|
hw = tags.get("highway")
|
||||||
|
if hw in ("motorway","trunk","primary","secondary","tertiary",
|
||||||
|
"residential","unclassified","service","living_street","pedestrian"):
|
||||||
|
return "streets"
|
||||||
|
if hw in ("footway","path","track","cycleway"): return "footpaths"
|
||||||
|
if tags.get("building"): return "buildings"
|
||||||
|
if tags.get("natural") == "water": return "water"
|
||||||
|
ww = tags.get("waterway")
|
||||||
|
if ww in ("river","stream","canal"): return "waterways"
|
||||||
|
if tags.get("leisure") in ("park","garden"): return "parks"
|
||||||
|
if tags.get("landuse") in ("forest","grass","meadow"): return "forest"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def way_to_polyline(way_node_ids, nodes, shift_lv95, m_to_unit, z=0.0):
|
||||||
|
"""OSM-Way → Rhino.Polyline in Doc-Units. shift_lv95 = (sx, sy, sz) Origin-
|
||||||
|
Shift in LV95-Metern (gleicher Pipeline wie swisstopo)."""
|
||||||
|
pts = []
|
||||||
|
sx, sy, sz = shift_lv95
|
||||||
|
for nid in way_node_ids:
|
||||||
|
node = nodes.get(nid)
|
||||||
|
if node is None: continue
|
||||||
|
lon, lat = node
|
||||||
|
e, n = swisstopo.wgs84_to_lv95(lon, lat)
|
||||||
|
x = (e - sx) * m_to_unit
|
||||||
|
y = (n - sy) * m_to_unit
|
||||||
|
pts.append(rg.Point3d(x, y, z))
|
||||||
|
if len(pts) < 2: return None
|
||||||
|
poly = rg.Polyline(pts)
|
||||||
|
return poly
|
||||||
|
|
||||||
|
|
||||||
|
def import_osm_to_doc(doc, bbox_wgs, categories, shift_lv95, m_to_unit,
|
||||||
|
z_doc=0.0, progress=None):
|
||||||
|
"""End-to-end-Import: Overpass-Query + Polylinien-Erzeugung. Liefert
|
||||||
|
Liste von dicts: [{category, obj_id, way_tags}, ...] — Aufrufer macht
|
||||||
|
Layer-Move + Tag selbst."""
|
||||||
|
osm_json = fetch_overpass(bbox_wgs, categories, progress=progress)
|
||||||
|
if osm_json is None: return []
|
||||||
|
parsed = parse_osm_elements(osm_json)
|
||||||
|
if not parsed: return []
|
||||||
|
nodes = parsed["nodes"]
|
||||||
|
ways = parsed["ways"]
|
||||||
|
if progress: progress("Parse {} Ways...".format(len(ways)))
|
||||||
|
created = []
|
||||||
|
for way in ways:
|
||||||
|
cat = classify_way(way["tags"])
|
||||||
|
if cat is None or cat not in categories: continue
|
||||||
|
poly = way_to_polyline(way["nodes"], nodes, shift_lv95,
|
||||||
|
m_to_unit, z=z_doc)
|
||||||
|
if poly is None or poly.Count < 2: continue
|
||||||
|
# Wenn Polyline geschlossen ist (erster == letzter Punkt) → als Curve
|
||||||
|
# mit Schluss-Edge, sonst offene Polyline.
|
||||||
|
curve = poly.ToNurbsCurve()
|
||||||
|
if curve is None: continue
|
||||||
|
gid = doc.Objects.AddCurve(curve)
|
||||||
|
if gid is None: continue
|
||||||
|
obj = doc.Objects.Find(gid)
|
||||||
|
if obj is None: continue
|
||||||
|
created.append({
|
||||||
|
"category": cat,
|
||||||
|
"obj": obj,
|
||||||
|
"tags": way["tags"],
|
||||||
|
})
|
||||||
|
if progress: progress("→ {} OSM-Linien erzeugt".format(len(created)))
|
||||||
|
return created
|
||||||
+40
-25
@@ -585,12 +585,24 @@ class EbenenBridge(panel_base.BaseBridge):
|
|||||||
try: e_list = json.loads(e_raw)
|
try: e_list = json.loads(e_raw)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[EBENEN] save_ebene JSON:", ex); return
|
print("[EBENEN] save_ebene JSON:", ex); return
|
||||||
replaced = False
|
# Rekursive Suche + Replace durch den Tree — Sub-Ebenen
|
||||||
for i, e in enumerate(e_list):
|
# (children) liegen verschachtelt, nicht in der Top-Level-Liste.
|
||||||
if isinstance(e, dict) and e.get("code") == orig_code:
|
def _replace_in_tree(lst, target_code, new_data):
|
||||||
e_list[i] = updated
|
for i, e in enumerate(lst):
|
||||||
replaced = True
|
if not isinstance(e, dict): continue
|
||||||
break
|
if e.get("code") == target_code:
|
||||||
|
kids = e.get("children")
|
||||||
|
merged = dict(new_data)
|
||||||
|
if isinstance(kids, list) and "children" not in merged:
|
||||||
|
merged["children"] = kids
|
||||||
|
lst[i] = merged
|
||||||
|
return True
|
||||||
|
kids = e.get("children")
|
||||||
|
if isinstance(kids, list):
|
||||||
|
if _replace_in_tree(kids, target_code, new_data):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
replaced = _replace_in_tree(e_list, orig_code, updated)
|
||||||
if not replaced:
|
if not replaced:
|
||||||
print("[EBENEN] save_ebene: code {} nicht gefunden".format(orig_code))
|
print("[EBENEN] save_ebene: code {} nicht gefunden".format(orig_code))
|
||||||
return
|
return
|
||||||
@@ -639,25 +651,28 @@ class EbenenBridge(panel_base.BaseBridge):
|
|||||||
def _fill_signature(e_list):
|
def _fill_signature(e_list):
|
||||||
out = {}
|
out = {}
|
||||||
if not isinstance(e_list, list): return out
|
if not isinstance(e_list, list): return out
|
||||||
for e in e_list:
|
def _walk(lst):
|
||||||
if not isinstance(e, dict): continue
|
for e in lst:
|
||||||
f = e.get("fill")
|
if not isinstance(e, dict): continue
|
||||||
if not isinstance(f, dict): continue
|
f = e.get("fill")
|
||||||
if f.get("pattern") in (None, "None"): continue
|
if isinstance(f, dict) and f.get("pattern") not in (None, "None"):
|
||||||
# lw kann None sein -> als Sentinel ein eindeutiger Wert
|
lw_raw = f.get("lw")
|
||||||
lw_raw = f.get("lw")
|
try:
|
||||||
try:
|
lw_sig = round(float(lw_raw), 6) if lw_raw is not None else None
|
||||||
lw_sig = round(float(lw_raw), 6) if lw_raw is not None else None
|
except Exception:
|
||||||
except Exception:
|
lw_sig = None
|
||||||
lw_sig = None
|
out[e.get("code")] = (
|
||||||
out[e.get("code")] = (
|
f.get("pattern"),
|
||||||
f.get("pattern"),
|
f.get("source", "layer"),
|
||||||
f.get("source", "layer"),
|
(f.get("color") or "").lower(),
|
||||||
(f.get("color") or "").lower(),
|
round(float(f.get("scale") or 1.0), 6),
|
||||||
round(float(f.get("scale") or 1.0), 6),
|
round(float(f.get("rotation") or 0.0), 6),
|
||||||
round(float(f.get("rotation") or 0.0), 6),
|
lw_sig,
|
||||||
lw_sig,
|
)
|
||||||
)
|
kids = e.get("children")
|
||||||
|
if isinstance(kids, list) and kids:
|
||||||
|
_walk(kids)
|
||||||
|
_walk(e_list)
|
||||||
return out
|
return out
|
||||||
old_e_raw = doc.Strings.GetValue("dossier_ebenen")
|
old_e_raw = doc.Strings.GetValue("dossier_ebenen")
|
||||||
old_sig = {}
|
old_sig = {}
|
||||||
|
|||||||
+351
-18
@@ -20,11 +20,37 @@ import Rhino
|
|||||||
import Rhino.Geometry as rg
|
import Rhino.Geometry as rg
|
||||||
import System
|
import System
|
||||||
|
|
||||||
CACHE_DIR = os.path.expanduser("~/Library/Caches/Dossier/swisstopo")
|
DEFAULT_CACHE_DIR = os.path.expanduser("~/Library/Caches/Dossier/swisstopo")
|
||||||
|
CACHE_DIR = DEFAULT_CACHE_DIR
|
||||||
STAC_BASE = "https://data.geo.admin.ch/api/stac/v1"
|
STAC_BASE = "https://data.geo.admin.ch/api/stac/v1"
|
||||||
SEARCH_API = "https://api3.geo.admin.ch/rest/services/api/SearchServer"
|
SEARCH_API = "https://api3.geo.admin.ch/rest/services/api/SearchServer"
|
||||||
|
|
||||||
|
|
||||||
|
def get_cache_dir_for_doc(doc):
|
||||||
|
"""Cache-Pfad fuer ein Doc. Wenn das Doc auf Disk liegt: Subfolder neben
|
||||||
|
der .3dm-Datei (`<dir>/<basename>_swisstopo/`). Damit reisen die Files
|
||||||
|
mit dem Projekt — kann via SMB von anderen Maschinen geoeffnet werden
|
||||||
|
solange der Mount-Pfad identisch ist. Falls Doc nicht gespeichert:
|
||||||
|
globaler Fallback-Cache."""
|
||||||
|
try:
|
||||||
|
p = doc.Path if doc else None
|
||||||
|
if p and os.path.isfile(p):
|
||||||
|
doc_dir = os.path.dirname(p)
|
||||||
|
doc_base = os.path.splitext(os.path.basename(p))[0]
|
||||||
|
return os.path.join(doc_dir, doc_base + "_swisstopo")
|
||||||
|
except Exception: pass
|
||||||
|
return DEFAULT_CACHE_DIR
|
||||||
|
|
||||||
|
|
||||||
|
def set_cache_dir(path):
|
||||||
|
"""Stellt das aktive Cache-Verzeichnis. Alle nachfolgenden Downloads
|
||||||
|
landen dort. Aufrufer-Verantwortung: vor jedem Import den richtigen
|
||||||
|
Cache setzen (per-Doc oder global)."""
|
||||||
|
global CACHE_DIR
|
||||||
|
CACHE_DIR = path
|
||||||
|
_ensure_cache()
|
||||||
|
|
||||||
|
|
||||||
def _ensure_cache():
|
def _ensure_cache():
|
||||||
if not os.path.isdir(CACHE_DIR):
|
if not os.path.isdir(CACHE_DIR):
|
||||||
try: os.makedirs(CACHE_DIR)
|
try: os.makedirs(CACHE_DIR)
|
||||||
@@ -416,24 +442,86 @@ def _fetch_buildings_from_collection(collection_id, bbox_wgs, variant,
|
|||||||
return paths
|
return paths
|
||||||
|
|
||||||
|
|
||||||
def fetch_buildings_dwg(bbox_lv95, progress=None, variant="separated"):
|
def fetch_buildings_dwg(bbox_lv95, progress=None, variant="separated",
|
||||||
"""Holt swissBUILDINGS3D Tile-CAD-Files. Versucht erst v3.0 (separated/
|
version="v2"):
|
||||||
solid Varianten), faellt automatisch auf v2.0 zurueck wenn v3.0 in der
|
"""Holt swissBUILDINGS3D Tile-CAD-Files.
|
||||||
Region keine brauchbaren Files liefert (typisch in Staedten — die 3.0-
|
|
||||||
Tiles sind dort >700 MB pro Stueck und werden vom Size-Limit geblockt)."""
|
version='v2': stabile 2.0-Variante (1km-Tiles, keine Solid/Separated-
|
||||||
|
Aufteilung — alle Kategorien auf eigenen DXF-Layern).
|
||||||
|
version='v3': Beta 3.0-Variante mit Solid/Separated-Wahl. In Staedten
|
||||||
|
oft >700 MB pro Tile → auto-fallback auf v2 wenn v3
|
||||||
|
nichts brauchbares liefert."""
|
||||||
bbox_wgs = lv95_bbox_to_wgs84_bbox(*bbox_lv95)
|
bbox_wgs = lv95_bbox_to_wgs84_bbox(*bbox_lv95)
|
||||||
paths = _fetch_buildings_from_collection(
|
if version == "v3":
|
||||||
_BUILDINGS_COLLECTION_V3, bbox_wgs, variant, progress=progress)
|
paths = _fetch_buildings_from_collection(
|
||||||
if not paths:
|
_BUILDINGS_COLLECTION_V3, bbox_wgs, variant, progress=progress)
|
||||||
if progress: progress("v3.0 lieferte keine Tiles — fallback auf v2.0 (1km-Tiles)...")
|
if not paths:
|
||||||
# v2.0 hat keine variant-Marker im Filename, ist immer "separated"-
|
if progress: progress("v3.0 lieferte keine Tiles — fallback auf v2.0...")
|
||||||
# artig (Kategorien auf eigenen DXF-Layern innerhalb einer DWG).
|
paths = _fetch_buildings_from_collection(
|
||||||
|
_BUILDINGS_COLLECTION_V2, bbox_wgs, variant, progress=progress)
|
||||||
|
else:
|
||||||
paths = _fetch_buildings_from_collection(
|
paths = _fetch_buildings_from_collection(
|
||||||
_BUILDINGS_COLLECTION_V2, bbox_wgs, variant, progress=progress)
|
_BUILDINGS_COLLECTION_V2, bbox_wgs, variant, progress=progress)
|
||||||
if progress: progress("{} CAD-Datei(en) bereit".format(len(paths)))
|
if progress: progress("{} CAD-Datei(en) bereit".format(len(paths)))
|
||||||
return paths
|
return paths
|
||||||
|
|
||||||
|
|
||||||
|
# --- TLM3D Vektor (Strassen / Gewaesser / Bahn / Vegetation) ----------------
|
||||||
|
# swisstopo bietet TLM3D unter mehreren Collection-IDs an (genaue Namen
|
||||||
|
# variieren). Wir probieren defensiv mehrere Kandidaten und nehmen DXF/DWG
|
||||||
|
# wenn verfuegbar (alles andere — GPKG/SHP — koennen wir nicht parsen).
|
||||||
|
|
||||||
|
# Echte swisstopo TLM-Collections (verifiziert via STAC API):
|
||||||
|
# ch.swisstopo.swisstlm3d — voller TLM3D Layer (~ganze CH)
|
||||||
|
# ch.swisstopo.swisstlmregio — kleinere Auflösung 1:200000
|
||||||
|
# ch.swisstopo.swissboundaries3d — Verwaltungsgrenzen
|
||||||
|
# ch.swisstopo.swiss-map-vector25 — 1:25000 Vektor
|
||||||
|
# Achtung: ALLE liefern nur GDB/SHP/GPKG/XTF — KEIN DXF/DWG. Direkter Rhino-
|
||||||
|
# Import funktioniert nicht ohne Shapefile-/GPKG-Parser.
|
||||||
|
_TLM_COLLECTIONS = [
|
||||||
|
"ch.swisstopo.swisstlm3d",
|
||||||
|
"ch.swisstopo.swisstlmregio",
|
||||||
|
"ch.swisstopo.swissboundaries3d",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_tlm3d_vector(bbox_lv95, kinds, progress=None):
|
||||||
|
"""Versucht swissTLM3D-Daten als DXF/DWG zu holen. swisstopo liefert
|
||||||
|
aktuell NUR GDB/SHP/GPKG-Formate — kein DXF. Diese Funktion findet
|
||||||
|
daher in den meisten Faellen keine importierbaren Files; sie loggt
|
||||||
|
aber sauber, was verfuegbar waere, falls wir spaeter einen
|
||||||
|
Shapefile-Parser einbauen."""
|
||||||
|
bbox_wgs = lv95_bbox_to_wgs84_bbox(*bbox_lv95)
|
||||||
|
out = {}
|
||||||
|
if progress:
|
||||||
|
progress("TLM3D-Import: swisstopo bietet aktuell KEINE DXF-Assets")
|
||||||
|
progress(" (nur GDB/SHP/GPKG — Rhino kann diese nicht nativ lesen)")
|
||||||
|
progress(" Verfuegbare Collections (zur Info):")
|
||||||
|
for coll in _TLM_COLLECTIONS:
|
||||||
|
try:
|
||||||
|
items = stac_query(coll, bbox_wgs,
|
||||||
|
asset_extensions=None) # alle Assets
|
||||||
|
except Exception as ex:
|
||||||
|
if progress: progress(" {}: HTTP-fail ({})".format(coll, ex))
|
||||||
|
continue
|
||||||
|
if not items:
|
||||||
|
if progress: progress(" {}: keine Items in der Region".format(coll))
|
||||||
|
continue
|
||||||
|
sample = items[0]
|
||||||
|
formats = set()
|
||||||
|
for k, a in (sample.get("assets") or {}).items():
|
||||||
|
href = (a.get("href") or "").lower()
|
||||||
|
for ext in (".gdb.zip", ".shp.zip", ".gpkg.zip", ".gpkg",
|
||||||
|
".xtf.zip", ".dxf", ".dwg"):
|
||||||
|
if href.endswith(ext): formats.add(ext.lstrip("."))
|
||||||
|
if progress: progress(" {}: {} Items, Formate: {}".format(
|
||||||
|
coll, len(items), ", ".join(sorted(formats)) or "?"))
|
||||||
|
if progress:
|
||||||
|
progress("→ TLM3D-Direct-Import nicht moeglich. Nutze OSM-Importer "
|
||||||
|
"fuer Vector-Daten (Strassen/Wasser/Gebaeude).")
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
# --- Terrain: swissALTI3D via XYZ ASCII -------------------------------------
|
# --- Terrain: swissALTI3D via XYZ ASCII -------------------------------------
|
||||||
|
|
||||||
def fetch_terrain_xyz(bbox_lv95, resolution="2.0", progress=None):
|
def fetch_terrain_xyz(bbox_lv95, resolution="2.0", progress=None):
|
||||||
@@ -640,6 +728,151 @@ def mesh_from_grid(grid, origin_shift=(0, 0, 0), unit_scale=1.0):
|
|||||||
return mesh
|
return mesh
|
||||||
|
|
||||||
|
|
||||||
|
def generate_mesh_from_contours(doc, contour_curves, sample_step_m=2.0,
|
||||||
|
m_to_unit=1.0, progress=None):
|
||||||
|
"""Baut ein TIN-Mesh aus Hoehenlinien-Curves. Jede Curve hat ihre echte
|
||||||
|
Z-Hoehe — wir sampeln Vertices entlang der Curves und triangulieren
|
||||||
|
sie via Rhinos _-MeshPatch / _-Delaunay Command. Resultat: Topographie-
|
||||||
|
Mesh basierend auf den diskreten Hoehenlinien-Stufen.
|
||||||
|
|
||||||
|
Liefert RhinoObject (Mesh) oder None."""
|
||||||
|
import System
|
||||||
|
if not contour_curves: return None
|
||||||
|
pts = []
|
||||||
|
for c in contour_curves:
|
||||||
|
if c is None: continue
|
||||||
|
# Polyline-Vertices wenn moeglich (exakt), sonst entlang Curve sampeln
|
||||||
|
ok, poly = c.TryGetPolyline()
|
||||||
|
if ok and poly is not None:
|
||||||
|
for pt in poly: pts.append(rg.Point3d(pt))
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
L = c.GetLength()
|
||||||
|
n = max(2, int(L / (sample_step_m * m_to_unit)))
|
||||||
|
params = c.DivideByCount(n, True)
|
||||||
|
if params:
|
||||||
|
for t in params: pts.append(c.PointAt(t))
|
||||||
|
except Exception: pass
|
||||||
|
if len(pts) < 3:
|
||||||
|
if progress: progress("Contour-Mesh: zu wenig Vertices ({})".format(len(pts)))
|
||||||
|
return None
|
||||||
|
if progress: progress("Contour-Mesh: trianguliere {} Vertices...".format(len(pts)))
|
||||||
|
# Temp-Points erzeugen + selektieren
|
||||||
|
temp_pids = []
|
||||||
|
try:
|
||||||
|
for p in pts:
|
||||||
|
pid = doc.Objects.AddPoint(p)
|
||||||
|
if pid and pid != System.Guid.Empty:
|
||||||
|
temp_pids.append(pid)
|
||||||
|
if not temp_pids:
|
||||||
|
if progress: progress("Contour-Mesh: keine Temp-Points")
|
||||||
|
return None
|
||||||
|
doc.Objects.UnselectAll()
|
||||||
|
for pid in temp_pids: doc.Objects.Select(pid)
|
||||||
|
before = set(o.Id for o in doc.Objects
|
||||||
|
if o and not o.IsDeleted
|
||||||
|
and isinstance(o.Geometry, rg.Mesh))
|
||||||
|
# Mehrere Commands probieren (Mac Rhino 8 vs neuere Versionen)
|
||||||
|
cmd_tried = None
|
||||||
|
for cmd in ['_-MeshPatch _Enter _Enter',
|
||||||
|
'_-Delaunay _Enter',
|
||||||
|
'_-DelaunayMesh _Enter',
|
||||||
|
'_-MeshFromPoints _Enter']:
|
||||||
|
try:
|
||||||
|
Rhino.RhinoApp.RunScript(cmd, False)
|
||||||
|
except Exception: continue
|
||||||
|
cmd_tried = cmd
|
||||||
|
new_mesh = next((o for o in doc.Objects
|
||||||
|
if o and not o.IsDeleted
|
||||||
|
and isinstance(o.Geometry, rg.Mesh)
|
||||||
|
and o.Id not in before), None)
|
||||||
|
if new_mesh:
|
||||||
|
if progress: progress("→ Contour-Mesh via '{}'".format(cmd.split()[0]))
|
||||||
|
return new_mesh
|
||||||
|
if progress:
|
||||||
|
progress("Contour-Mesh: kein Command lieferte ein Mesh "
|
||||||
|
"(zuletzt: {})".format(cmd_tried))
|
||||||
|
return None
|
||||||
|
finally:
|
||||||
|
# Temp-Points wieder weg
|
||||||
|
doc.Objects.UnselectAll()
|
||||||
|
for pid in temp_pids:
|
||||||
|
try: doc.Objects.Delete(pid, True)
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
|
||||||
|
def generate_schichtenmodell(doc, contour_curves, progress=None):
|
||||||
|
"""Schichtenmodell: jede geschlossene Hoehenlinie wird zu einer planaren
|
||||||
|
Flaeche auf ihrer Z-Hoehe. Stacked Discs — der architektonische
|
||||||
|
'Pappmodell'-Look. Offene Konturen (typ. am bbox-Rand) werden
|
||||||
|
uebersprungen.
|
||||||
|
|
||||||
|
Liefert Liste von erzeugten RhinoObjects."""
|
||||||
|
import System
|
||||||
|
if not contour_curves: return []
|
||||||
|
created = []
|
||||||
|
tol = doc.ModelAbsoluteTolerance
|
||||||
|
n_open = 0
|
||||||
|
for c in contour_curves:
|
||||||
|
if c is None: continue
|
||||||
|
try:
|
||||||
|
if not c.IsClosed:
|
||||||
|
n_open += 1
|
||||||
|
continue
|
||||||
|
breps = rg.Brep.CreatePlanarBreps(c, tol)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if not breps: continue
|
||||||
|
for brep in breps:
|
||||||
|
gid = doc.Objects.AddBrep(brep)
|
||||||
|
if gid and gid != System.Guid.Empty:
|
||||||
|
obj = doc.Objects.Find(gid)
|
||||||
|
if obj: created.append(obj)
|
||||||
|
if progress:
|
||||||
|
progress("→ {} Schichten-Flaechen ({} offene Konturen skipped)".format(
|
||||||
|
len(created), n_open))
|
||||||
|
return created
|
||||||
|
|
||||||
|
|
||||||
|
def generate_contour_curves(grid, shift_lv95, m_to_unit, interval=2.0,
|
||||||
|
progress=None):
|
||||||
|
"""Generiert Hoehenlinien (Contour-Curves) aus dem Terrain-Grid via
|
||||||
|
Mesh.CreateContourCurves.
|
||||||
|
|
||||||
|
interval: Hoehenabstand in REALEN METERN (1.0/2.0/5.0 typisch).
|
||||||
|
Liefert Liste von rg.Curve-Objekten in Doc-Units. Caller macht
|
||||||
|
doc.Objects.AddCurve + Layer-Move."""
|
||||||
|
if not grid or not grid.get("points"): return []
|
||||||
|
# Temp-Mesh aus Grid (gleicher Pipeline wie mesh_from_grid)
|
||||||
|
mesh = mesh_from_grid(grid, origin_shift=shift_lv95, unit_scale=m_to_unit)
|
||||||
|
if mesh.Vertices.Count < 3: return []
|
||||||
|
bb = mesh.GetBoundingBox(True)
|
||||||
|
z_min_doc = bb.Min.Z
|
||||||
|
z_max_doc = bb.Max.Z
|
||||||
|
interval_doc = interval * m_to_unit
|
||||||
|
if interval_doc <= 0: return []
|
||||||
|
if progress:
|
||||||
|
z_min_m = z_min_doc / m_to_unit + shift_lv95[2]
|
||||||
|
z_max_m = z_max_doc / m_to_unit + shift_lv95[2]
|
||||||
|
progress("Hoehenlinien: Z {:.1f}–{:.1f} m.ü.M, Abstand {} m".format(
|
||||||
|
z_min_m, z_max_m, interval))
|
||||||
|
try:
|
||||||
|
curves = rg.Mesh.CreateContourCurves(
|
||||||
|
mesh,
|
||||||
|
rg.Point3d(0, 0, z_min_doc),
|
||||||
|
rg.Point3d(0, 0, z_max_doc),
|
||||||
|
interval_doc)
|
||||||
|
except Exception as ex:
|
||||||
|
if progress: progress("Contour fail: {}".format(ex))
|
||||||
|
return []
|
||||||
|
if not curves:
|
||||||
|
if progress: progress("Keine Hoehenlinien erzeugt")
|
||||||
|
return []
|
||||||
|
out = list(curves)
|
||||||
|
if progress: progress("→ {} Hoehenlinien-Kurven".format(len(out)))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
# --- Orthofoto: SWISSIMAGE 10cm via GeoTIFF --------------------------------
|
# --- Orthofoto: SWISSIMAGE 10cm via GeoTIFF --------------------------------
|
||||||
|
|
||||||
def fetch_orthophoto(bbox_lv95, resolution="2.0", progress=None):
|
def fetch_orthophoto(bbox_lv95, resolution="2.0", progress=None):
|
||||||
@@ -737,6 +970,108 @@ def _geotiff_to_png(tif_path, max_dim=2048):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def add_ortho_draped_mesh(doc, ortho_path, tile_bbox_lv95, terrain_grid,
|
||||||
|
shift_lv95, m_to_unit, z_lift=0.05,
|
||||||
|
target_layer_idx=-1):
|
||||||
|
"""Erzeugt ein Mesh, das der Topographie folgt — textured mit dem Ortho-
|
||||||
|
Foto. Statt einer flachen Plane: Per-Tile-Sub-Mesh aus dem Terrain-Grid
|
||||||
|
mit Per-Vertex-UV (0..1 ueber die Tile-Breite). Material kommt von einem
|
||||||
|
temporaeren PictureFrame (das ist der einzige Weg auf Mac Rhino 8 die
|
||||||
|
embedded Bitmap in Cycles zur Anzeige zu bringen) — der PictureFrame
|
||||||
|
wird hinterher geloescht, nur das Drape-Mesh bleibt.
|
||||||
|
|
||||||
|
terrain_grid: dict aus merge_grids() — wir extrahieren daraus die Punkte
|
||||||
|
innerhalb der Tile-bbox.
|
||||||
|
z_lift: kleiner Z-Offset (in doc-units) gegen Z-Fighting mit dem
|
||||||
|
darunterliegenden Terrain-Mesh."""
|
||||||
|
if not (ortho_path and os.path.isfile(ortho_path)): return None
|
||||||
|
# TIF direkt verwenden — Rhino's _Picture liest GeoTIFF nativ ueber
|
||||||
|
# NSImage (Mac) und behaelt 10cm-Aufloesung (10000×10000 px statt 2k PNG).
|
||||||
|
e_min, n_min, e_max, n_max = tile_bbox_lv95
|
||||||
|
sx, sy, sz = shift_lv95
|
||||||
|
# Terrain-Punkte innerhalb des Tiles aus dem Merged-Grid extrahieren
|
||||||
|
es = sorted(e for e in terrain_grid["es"]
|
||||||
|
if e_min - 0.01 <= e <= e_max + 0.01)
|
||||||
|
ns = sorted(n for n in terrain_grid["ns"]
|
||||||
|
if n_min - 0.01 <= n <= n_max + 0.01)
|
||||||
|
if len(es) < 2 or len(ns) < 2:
|
||||||
|
print("[SWISSTOPO] drape: zu wenig Terrain-Punkte fuer Tile")
|
||||||
|
return None
|
||||||
|
pts = terrain_grid["points"]
|
||||||
|
span_e = e_max - e_min
|
||||||
|
span_n = n_max - n_min
|
||||||
|
# Half-Pixel-Inset: bei 10000×10000 px Tiles wuerde ein Sample exakt an
|
||||||
|
# u=0 oder u=1 auf der Pixel-Grenze landen; mit clamp-to-border kann das
|
||||||
|
# weisse Linien an den Tile-Boundaries erzeugen. Wir verschieben UV
|
||||||
|
# minimal nach innen.
|
||||||
|
UV_INSET = 0.5 / 10000.0 # halbe Pixel-Breite im UV-Raum
|
||||||
|
mesh = rg.Mesh()
|
||||||
|
idx_for = {}
|
||||||
|
for j, ny in enumerate(ns):
|
||||||
|
for i, ex in enumerate(es):
|
||||||
|
z = pts.get((ex, ny))
|
||||||
|
if z is None: continue
|
||||||
|
v_idx = mesh.Vertices.Add(
|
||||||
|
(ex - sx) * m_to_unit,
|
||||||
|
(ny - sy) * m_to_unit,
|
||||||
|
(z - sz) * m_to_unit + z_lift)
|
||||||
|
u = UV_INSET + (ex - e_min) / span_e * (1.0 - 2 * UV_INSET)
|
||||||
|
v = UV_INSET + (ny - n_min) / span_n * (1.0 - 2 * UV_INSET)
|
||||||
|
mesh.TextureCoordinates.Add(u, v)
|
||||||
|
idx_for[(i, j)] = v_idx
|
||||||
|
n_faces = 0
|
||||||
|
for j in range(len(ns) - 1):
|
||||||
|
for i in range(len(es) - 1):
|
||||||
|
a = idx_for.get((i, j))
|
||||||
|
b = idx_for.get((i+1, j))
|
||||||
|
c = idx_for.get((i+1, j+1))
|
||||||
|
d = idx_for.get((i, j+1))
|
||||||
|
if a is None or b is None or c is None or d is None: continue
|
||||||
|
mesh.Faces.AddFace(a, b, c, d)
|
||||||
|
n_faces += 1
|
||||||
|
if n_faces == 0:
|
||||||
|
print("[SWISSTOPO] drape: keine Faces erzeugt")
|
||||||
|
return None
|
||||||
|
mesh.Normals.ComputeNormals()
|
||||||
|
mesh.Compact()
|
||||||
|
# Temp-PictureFrame off-screen erzeugen — ergibt working RenderMaterial
|
||||||
|
# mit Bitmap-Texture, das wir auf das Mesh uebertragen.
|
||||||
|
# embedBitmap=False: Pfad-Referenz statt 70MB-TIF-Embedding ins .3dm.
|
||||||
|
# Cache ist persistent (~/Library/Caches), Pfad bleibt gueltig.
|
||||||
|
pf_plane = rg.Plane(rg.Point3d(-1e6, -1e6, -1e6),
|
||||||
|
rg.Vector3d.XAxis, rg.Vector3d.YAxis)
|
||||||
|
try:
|
||||||
|
pf_gid = doc.Objects.AddPictureFrame(
|
||||||
|
pf_plane, ortho_path, False, 1.0, 1.0, True, False)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SWISSTOPO] drape: PictureFrame-create fail:", ex)
|
||||||
|
return None
|
||||||
|
if not pf_gid or pf_gid == System.Guid.Empty:
|
||||||
|
print("[SWISSTOPO] drape: PictureFrame Empty-GUID")
|
||||||
|
return None
|
||||||
|
pf_obj = doc.Objects.Find(pf_gid)
|
||||||
|
pf_mat_idx = pf_obj.Attributes.MaterialIndex
|
||||||
|
# Mesh ins Doc + Material vom PictureFrame uebernehmen
|
||||||
|
mesh_gid = doc.Objects.AddMesh(mesh)
|
||||||
|
mesh_obj = doc.Objects.Find(mesh_gid)
|
||||||
|
if mesh_obj is None:
|
||||||
|
try: doc.Objects.Delete(pf_gid, True)
|
||||||
|
except Exception: pass
|
||||||
|
return None
|
||||||
|
attrs = mesh_obj.Attributes.Duplicate()
|
||||||
|
attrs.MaterialSource = Rhino.DocObjects.ObjectMaterialSource.MaterialFromObject
|
||||||
|
attrs.MaterialIndex = pf_mat_idx
|
||||||
|
if target_layer_idx >= 0:
|
||||||
|
attrs.LayerIndex = target_layer_idx
|
||||||
|
doc.Objects.ModifyAttributes(mesh_obj, attrs, True)
|
||||||
|
# Temp-PictureFrame loeschen — das Mesh hat jetzt das Material
|
||||||
|
try: doc.Objects.Delete(pf_gid, True)
|
||||||
|
except Exception: pass
|
||||||
|
print("[SWISSTOPO] drape mesh: {}x{} grid, {} faces, mat={}".format(
|
||||||
|
len(es), len(ns), n_faces, pf_mat_idx))
|
||||||
|
return mesh_obj
|
||||||
|
|
||||||
|
|
||||||
def add_ortho_plane(doc, ortho_path, tile_bbox_lv95, shift_lv95, m_to_unit,
|
def add_ortho_plane(doc, ortho_path, tile_bbox_lv95, shift_lv95, m_to_unit,
|
||||||
z_doc=0.0, target_layer_idx=-1):
|
z_doc=0.0, target_layer_idx=-1):
|
||||||
"""Erzeugt eine planare Brep-Flaeche mit dem SWISSIMAGE-Foto als Material,
|
"""Erzeugt eine planare Brep-Flaeche mit dem SWISSIMAGE-Foto als Material,
|
||||||
@@ -749,11 +1084,8 @@ def add_ortho_plane(doc, ortho_path, tile_bbox_lv95, shift_lv95, m_to_unit,
|
|||||||
|
|
||||||
Liefert den RhinoObject der erzeugten Plane (oder None)."""
|
Liefert den RhinoObject der erzeugten Plane (oder None)."""
|
||||||
if not (ortho_path and os.path.isfile(ortho_path)): return None
|
if not (ortho_path and os.path.isfile(ortho_path)): return None
|
||||||
# GeoTIFF → PNG damit Rhino's Material-Bitmap es als Diffuse nehmen kann
|
# TIF direkt — Rhino's Picture-Pfad liest GeoTIFF nativ (NSImage auf Mac).
|
||||||
if ortho_path.lower().endswith((".tif", ".tiff")):
|
# Behaelt die volle 10cm-Aufloesung statt auf 2k PNG runter zu skalieren.
|
||||||
png = _geotiff_to_png(ortho_path)
|
|
||||||
if not png: return None
|
|
||||||
ortho_path = png
|
|
||||||
# bbox in Doc-Units (nach Shift + Scale)
|
# bbox in Doc-Units (nach Shift + Scale)
|
||||||
e_min, n_min, e_max, n_max = tile_bbox_lv95
|
e_min, n_min, e_max, n_max = tile_bbox_lv95
|
||||||
sx, sy, sz = shift_lv95
|
sx, sy, sz = shift_lv95
|
||||||
@@ -785,7 +1117,8 @@ def add_ortho_plane(doc, ortho_path, tile_bbox_lv95, shift_lv95, m_to_unit,
|
|||||||
True, # selfIllumination=True — Textur unabhaengig von
|
True, # selfIllumination=True — Textur unabhaengig von
|
||||||
# Lighting sichtbar (sonst evtl. dunkel in modes
|
# Lighting sichtbar (sonst evtl. dunkel in modes
|
||||||
# ohne Lichtquellen)
|
# ohne Lichtquellen)
|
||||||
True) # embedBitmap=True (Pfad-Probleme umgehen)
|
False) # embedBitmap=False — Pfad-Referenz (Cache bleibt
|
||||||
|
# persistent, kein 70MB-Embedding pro Tile)
|
||||||
if gid == System.Guid.Empty:
|
if gid == System.Guid.Empty:
|
||||||
print("[SWISSTOPO] AddPictureFrame: Empty-GUID")
|
print("[SWISSTOPO] AddPictureFrame: Empty-GUID")
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -29,14 +29,45 @@ export default function EbenenSettingsApp() {
|
|||||||
return () => document.removeEventListener('contextmenu', blockContext)
|
return () => document.removeEventListener('contextmenu', blockContext)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const sortedEbenen = [...ebenen].sort((a, b) => {
|
// Flach durch Tree iterieren — Sub-Ebenen sind verschachtelt in children
|
||||||
const ca = parseInt(a.code, 10), cb = parseInt(b.code, 10)
|
const flattenEbenen = (list, depth = 0) => {
|
||||||
if (!isNaN(ca) && !isNaN(cb)) return ca - cb
|
const out = []
|
||||||
return (a.code || '').localeCompare(b.code || '')
|
for (const e of list) {
|
||||||
})
|
if (!e || typeof e !== 'object') continue
|
||||||
|
out.push({ ...e, _depth: depth })
|
||||||
|
if (Array.isArray(e.children) && e.children.length) {
|
||||||
|
out.push(...flattenEbenen(e.children, depth + 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
const flatEbenen = flattenEbenen(ebenen)
|
||||||
|
|
||||||
const currentEbene = ebenen.find(e => e.code === selectedCode)
|
// Sort: nur Top-Level (depth=0) numerisch sortieren — Children stehen
|
||||||
|| ebenen.find(e => e.code === originalCode)
|
// direkt hinter ihrem Parent. Beim Picker zeigt das die Hierarchie.
|
||||||
|
const sortedEbenen = (() => {
|
||||||
|
const tops = flatEbenen.filter(e => e._depth === 0)
|
||||||
|
tops.sort((a, b) => {
|
||||||
|
const ca = parseInt(a.code, 10), cb = parseInt(b.code, 10)
|
||||||
|
if (!isNaN(ca) && !isNaN(cb)) return ca - cb
|
||||||
|
return (a.code || '').localeCompare(b.code || '')
|
||||||
|
})
|
||||||
|
const out = []
|
||||||
|
for (const top of tops) {
|
||||||
|
out.push(top)
|
||||||
|
// Children direkt anhaengen, ebenfalls per Code sortiert
|
||||||
|
const kids = flatEbenen.filter(e =>
|
||||||
|
e._depth === 1 &&
|
||||||
|
flatEbenen.find(p => p._depth === 0 && Array.isArray(p.children)
|
||||||
|
&& p.children.some(c => c.code === e.code))?.code === top.code)
|
||||||
|
kids.sort((a, b) => (a.code || '').localeCompare(b.code || ''))
|
||||||
|
out.push(...kids)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
})()
|
||||||
|
|
||||||
|
const currentEbene = flatEbenen.find(e => e.code === selectedCode)
|
||||||
|
|| flatEbenen.find(e => e.code === originalCode)
|
||||||
|| initial.ebene
|
|| initial.ebene
|
||||||
|| null
|
|| null
|
||||||
|
|
||||||
|
|||||||
+7
-4
@@ -6,7 +6,7 @@ import {
|
|||||||
createFenster, createTuer, createAussparung, createTreppe,
|
createFenster, createTuer, createAussparung, createTreppe,
|
||||||
createStuetze, createTraeger, createRaum,
|
createStuetze, createTraeger, createRaum,
|
||||||
exportRaeume,
|
exportRaeume,
|
||||||
openSwisstopo, openSwisstopoDialog,
|
openSwisstopo, openSwisstopoDialog, openOsmDialog,
|
||||||
updateElement, deleteElement, regenerateAllElements,
|
updateElement, deleteElement, regenerateAllElements,
|
||||||
} from './lib/rhinoBridge'
|
} from './lib/rhinoBridge'
|
||||||
|
|
||||||
@@ -478,10 +478,13 @@ function NeuesElementSection({ noGeschoss, activeName }) {
|
|||||||
onClick={() => createRaum({})} />
|
onClick={() => createRaum({})} />
|
||||||
</PillGroup>
|
</PillGroup>
|
||||||
|
|
||||||
<PillGroup label="Swisstopo">
|
<PillGroup label="Importer">
|
||||||
<PillButton icon="download" label="Importer…"
|
<PillButton icon="download" label="Swisstopo"
|
||||||
hint="Vollautomatischer Import via swisstopo STAC-API: Adresse suchen, Radius wählen, Gebäude + Terrain + Luftbild holen — Tiles werden gecacht"
|
hint="Vollautomatischer Import via swisstopo STAC-API: Adresse suchen, Radius wählen, Gebäude + Terrain + Luftbild holen"
|
||||||
onClick={() => openSwisstopoDialog()} />
|
onClick={() => openSwisstopoDialog()} />
|
||||||
|
<PillButton icon="public" label="OSM"
|
||||||
|
hint="OpenStreetMap-Daten via Overpass-API als 2D-Linien: Strassen, Gebäudeumrisse, Wasser, Grünflächen, Wege"
|
||||||
|
onClick={() => openOsmDialog()} />
|
||||||
<PillButton icon="map" label="Karte"
|
<PillButton icon="map" label="Karte"
|
||||||
hint="Öffnet map.geo.admin.ch im Browser zur visuellen Inspektion"
|
hint="Öffnet map.geo.admin.ch im Browser zur visuellen Inspektion"
|
||||||
onClick={() => openSwisstopo('both')} />
|
onClick={() => openSwisstopo('both')} />
|
||||||
|
|||||||
+309
@@ -0,0 +1,309 @@
|
|||||||
|
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('[OSM] →', type, payload); return }
|
||||||
|
document.title = 'RHINOMSG::' + JSON.stringify({ type, payload })
|
||||||
|
}
|
||||||
|
|
||||||
|
function Field({ label, hint, children }) {
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, padding: '6px 0' }}>
|
||||||
|
{label && <span className="label-xs">{label}</span>}
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>{children}</div>
|
||||||
|
{hint && (
|
||||||
|
<span style={{ fontSize: 9, color: 'var(--text-muted)', lineHeight: 1.4 }}>{hint}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SectionLabel({ children }) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
fontSize: 9, color: 'var(--text-muted)', fontWeight: 600,
|
||||||
|
letterSpacing: 0.5, textTransform: 'uppercase',
|
||||||
|
padding: '10px 0 4px',
|
||||||
|
borderTop: '1px solid var(--border-light)',
|
||||||
|
marginTop: 8,
|
||||||
|
}}>{children}</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Radio({ value, options, onChange }) {
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||||
|
{options.map(o => (
|
||||||
|
<button key={o.value}
|
||||||
|
onClick={() => onChange(o.value)}
|
||||||
|
className={value === o.value ? 'btn-contained' : 'btn-outlined'}
|
||||||
|
style={{ padding: '4px 10px', fontSize: 10 }}
|
||||||
|
>
|
||||||
|
{o.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute', inset: 0,
|
||||||
|
background: 'var(--bg-dialog)',
|
||||||
|
display: 'flex', flexDirection: 'column',
|
||||||
|
fontFamily: 'var(--font)', color: 'var(--text-primary)',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}>
|
||||||
|
<div style={{ flex: 1, overflowY: 'auto', padding: '10px 16px' }}>
|
||||||
|
|
||||||
|
<SectionLabel>Standort</SectionLabel>
|
||||||
|
|
||||||
|
<Field label="ADRESSE / ORT" hint='z.B. "Bahnhofstrasse 1, Zürich"'>
|
||||||
|
<input
|
||||||
|
value={searchText}
|
||||||
|
onChange={(e) => setSearchText(e.target.value)}
|
||||||
|
onKeyDown={(e) => { if (e.key === 'Enter') handleSearch() }}
|
||||||
|
placeholder="Adresse oder Ortsname"
|
||||||
|
style={{ flex: 1, fontSize: 11, padding: '5px 8px' }}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="btn-outlined"
|
||||||
|
onClick={handleSearch}
|
||||||
|
disabled={searching || !searchText.trim()}
|
||||||
|
style={{ padding: '4px 10px', fontSize: 11 }}
|
||||||
|
>
|
||||||
|
{searching ? '…' : 'Suchen'}
|
||||||
|
</button>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="ODER LV95-KOORDS (E / N)"
|
||||||
|
hint="Falls aus Swisstopo-Import übernommen">
|
||||||
|
<input placeholder="E"
|
||||||
|
onChange={(e) => handleManualCoords(e.target.value, center?.n || '')}
|
||||||
|
style={{ width: 110, fontSize: 11, fontFamily: 'DM Mono, monospace', padding: '5px 8px' }} />
|
||||||
|
<span style={{ color: 'var(--text-muted)' }}>/</span>
|
||||||
|
<input placeholder="N"
|
||||||
|
onChange={(e) => handleManualCoords(center?.e || '', e.target.value)}
|
||||||
|
style={{ width: 110, fontSize: 11, fontFamily: 'DM Mono, monospace', padding: '5px 8px' }} />
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
{center && (
|
||||||
|
<div style={{
|
||||||
|
display: 'flex', alignItems: 'center', gap: 8,
|
||||||
|
padding: '8px 10px',
|
||||||
|
background: 'var(--accent-dim)',
|
||||||
|
border: '1px solid var(--accent-border)',
|
||||||
|
borderRadius: 'var(--r)',
|
||||||
|
marginTop: 4,
|
||||||
|
}}>
|
||||||
|
<Icon name="location_on" size={14} style={{ color: 'var(--accent)' }} />
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<div style={{ fontSize: 11, fontWeight: 500, overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{center.label}</div>
|
||||||
|
<div style={{ fontSize: 9, fontFamily: 'DM Mono, monospace',
|
||||||
|
color: 'var(--text-muted)' }}>
|
||||||
|
E {Math.round(center.e)} · N {Math.round(center.n)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<SectionLabel>Bereich</SectionLabel>
|
||||||
|
<Field label="RADIUS">
|
||||||
|
<Radio value={radius}
|
||||||
|
options={[
|
||||||
|
{ value: 100, label: '100 m' },
|
||||||
|
{ value: 200, label: '200 m' },
|
||||||
|
{ value: 500, label: '500 m' },
|
||||||
|
{ value: 1000, label: '1 km' },
|
||||||
|
]}
|
||||||
|
onChange={setRadius} />
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<SectionLabel>Kategorien</SectionLabel>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
|
{CATEGORIES.map(cat => (
|
||||||
|
<label key={cat.key}
|
||||||
|
style={{ display: 'flex', alignItems: 'center', gap: 8,
|
||||||
|
fontSize: 11, cursor: 'pointer', padding: '3px 0' }}
|
||||||
|
title={cat.hint}>
|
||||||
|
<input type="checkbox" checked={!!selected[cat.key]}
|
||||||
|
onChange={() => toggleCat(cat.key)} />
|
||||||
|
<Icon name={cat.icon} size={13} />
|
||||||
|
<span>{cat.label}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SectionLabel>Positionierung</SectionLabel>
|
||||||
|
<Field label="ORIGIN"
|
||||||
|
hint="LV95-Koords sind im Mio-Bereich. Auf 0/0/0 verschiebt zum aktiven Standort.">
|
||||||
|
<Radio value={shift ? 'origin' : 'lv95'}
|
||||||
|
options={[
|
||||||
|
{ value: 'origin', label: 'Auf Welt-Origin verschieben' },
|
||||||
|
{ value: 'lv95', label: 'Original LV95 lassen' },
|
||||||
|
]}
|
||||||
|
onChange={(v) => setShift(v === 'origin')} />
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="">
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: 6,
|
||||||
|
fontSize: 11, cursor: 'pointer' }}>
|
||||||
|
<input type="checkbox" checked={autoZoom}
|
||||||
|
onChange={(e) => setAutoZoom(e.target.checked)} />
|
||||||
|
Auto-Zoom auf importierte Objekte
|
||||||
|
</label>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label=""
|
||||||
|
hint="Bestehende OSM-Objekte (Tag dossier_osm_kind) werden vorher gelöscht.">
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: 6,
|
||||||
|
fontSize: 11, cursor: 'pointer' }}>
|
||||||
|
<input type="checkbox" checked={replaceExisting}
|
||||||
|
onChange={(e) => setReplaceExisting(e.target.checked)} />
|
||||||
|
Bestehende OSM-Objekte vorher löschen
|
||||||
|
</label>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<SectionLabel>Status</SectionLabel>
|
||||||
|
<div ref={logRef} style={{
|
||||||
|
height: 140, overflowY: 'auto',
|
||||||
|
padding: 8, fontSize: 10,
|
||||||
|
fontFamily: 'DM Mono, monospace',
|
||||||
|
background: 'var(--bg-base)',
|
||||||
|
border: '1px solid var(--border-light)',
|
||||||
|
borderRadius: 'var(--r)',
|
||||||
|
color: 'var(--text-secondary)',
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
}}>
|
||||||
|
{logs.length === 0
|
||||||
|
? <span style={{ color: 'var(--text-muted)' }}>Bereit</span>
|
||||||
|
: logs.map((l, i) => <div key={i}>{l}</div>)}
|
||||||
|
{running && <div style={{ color: 'var(--accent)' }}>Läuft…</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{
|
||||||
|
display: 'flex', alignItems: 'center', gap: 6,
|
||||||
|
padding: '8px 12px',
|
||||||
|
borderTop: '1px solid var(--border)',
|
||||||
|
background: 'var(--bg-section)',
|
||||||
|
}}>
|
||||||
|
<div style={{ fontSize: 9, color: 'var(--text-muted)', flex: 1 }}>
|
||||||
|
Quelle: Overpass-API · © OpenStreetMap-Mitwirkende (ODbL)
|
||||||
|
</div>
|
||||||
|
<button className="btn-text" onClick={() => send('CANCEL')}>Abbrechen</button>
|
||||||
|
<button className="btn-contained"
|
||||||
|
onClick={handleImport}
|
||||||
|
disabled={running || !center}>
|
||||||
|
<Icon name="download" size={12} />
|
||||||
|
{running ? 'Lädt…' : 'Importieren'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+77
-5
@@ -57,9 +57,19 @@ export default function SwisstopoApp() {
|
|||||||
// Optionen
|
// Optionen
|
||||||
const [radius, setRadius] = useState(100)
|
const [radius, setRadius] = useState(100)
|
||||||
const [getBuild, setGetBuild] = useState(true)
|
const [getBuild, setGetBuild] = useState(true)
|
||||||
|
const [buildVersion, setBuildVersion] = useState('v2') // v2 (stabil) / v3 (beta)
|
||||||
const [buildVariant, setBuildVariant] = useState('separated')
|
const [buildVariant, setBuildVariant] = useState('separated')
|
||||||
const [getTerrain, setGetTerrain] = useState(false)
|
const [getTerrain, setGetTerrain] = useState(false)
|
||||||
const [getOrtho, setGetOrtho] = 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 [shift, setShift] = useState(true)
|
||||||
const [autoZoom, setAutoZoom] = useState(true)
|
const [autoZoom, setAutoZoom] = useState(true)
|
||||||
const [replaceExisting, setReplaceExisting] = useState(true)
|
const [replaceExisting, setReplaceExisting] = useState(true)
|
||||||
@@ -124,14 +134,21 @@ export default function SwisstopoApp() {
|
|||||||
|
|
||||||
const handleImport = () => {
|
const handleImport = () => {
|
||||||
if (!center) { addLog('Bitte zuerst einen Standort wählen'); return }
|
if (!center) { addLog('Bitte zuerst einen Standort wählen'); return }
|
||||||
if (!getBuild && !getTerrain) { addLog('Mindestens Gebäude oder Terrain auswählen'); return }
|
if (!getBuild && !getTerrain && !getContours && !getContourTin && !getContourSchicht && !getTlm) {
|
||||||
|
addLog('Mindestens eine Datenquelle wählen'); return
|
||||||
|
}
|
||||||
setLogs([])
|
setLogs([])
|
||||||
setRunning(true)
|
setRunning(true)
|
||||||
setDone(false)
|
setDone(false)
|
||||||
const kinds = []
|
const kinds = []
|
||||||
if (getBuild) kinds.push('buildings')
|
if (getBuild) kinds.push('buildings')
|
||||||
if (getTerrain) kinds.push('terrain')
|
if (getTerrain) kinds.push('terrain')
|
||||||
if (getOrtho && getTerrain) kinds.push('ortho')
|
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', {
|
send('RUN_IMPORT', {
|
||||||
centerE: center.e,
|
centerE: center.e,
|
||||||
centerN: center.n,
|
centerN: center.n,
|
||||||
@@ -142,7 +159,10 @@ export default function SwisstopoApp() {
|
|||||||
replaceExisting,
|
replaceExisting,
|
||||||
clipToBbox,
|
clipToBbox,
|
||||||
terrainResolution: terrainRes,
|
terrainResolution: terrainRes,
|
||||||
|
buildVersion,
|
||||||
buildVariant,
|
buildVariant,
|
||||||
|
contourInterval: contourInt,
|
||||||
|
tlmKinds: tlmList,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,10 +257,23 @@ export default function SwisstopoApp() {
|
|||||||
<label style={{ display: 'flex', alignItems: 'center', gap: 6,
|
<label style={{ display: 'flex', alignItems: 'center', gap: 6,
|
||||||
fontSize: 11, cursor: 'pointer' }}>
|
fontSize: 11, cursor: 'pointer' }}>
|
||||||
<input type="checkbox" checked={getBuild} onChange={(e) => setGetBuild(e.target.checked)} />
|
<input type="checkbox" checked={getBuild} onChange={(e) => setGetBuild(e.target.checked)} />
|
||||||
<Icon name="location_city" size={13} /> Bestand-Gebäude (swissBUILDINGS3D 3.0, DWG)
|
<Icon name="location_city" size={13} /> Bestand-Gebäude (swissBUILDINGS3D, DWG)
|
||||||
</label>
|
</label>
|
||||||
</Field>
|
</Field>
|
||||||
{getBuild && (
|
{getBuild && (
|
||||||
|
<Field label="VERSION"
|
||||||
|
hint="2.0 = stabil, kein Solid/Separated-Split (alle Kategorien auf eigenen DXF-Layern innerhalb einer DWG). 3.0 = neuer, Beta — kann manchmal Probleme mit Variant-Erkennung haben.">
|
||||||
|
<Radio
|
||||||
|
value={buildVersion}
|
||||||
|
options={[
|
||||||
|
{ value: 'v2', label: '2.0 (stabil)' },
|
||||||
|
{ value: 'v3', label: '3.0 (beta)' },
|
||||||
|
]}
|
||||||
|
onChange={setBuildVersion}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
{getBuild && buildVersion === 'v3' && (
|
||||||
<Field label="GEBÄUDE-VARIANTE"
|
<Field label="GEBÄUDE-VARIANTE"
|
||||||
hint="Solid: ein geschlossenes Solid pro Gebäude (klein, schnell). Separated: Dach/Fassade/Wand als separate Objekte (mehr Detail, ermoeglicht z.B. Dach auszublenden).">
|
hint="Solid: ein geschlossenes Solid pro Gebäude (klein, schnell). Separated: Dach/Fassade/Wand als separate Objekte (mehr Detail, ermoeglicht z.B. Dach auszublenden).">
|
||||||
<Radio
|
<Radio
|
||||||
@@ -284,6 +317,45 @@ export default function SwisstopoApp() {
|
|||||||
</label>
|
</label>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
|
<Field label=""
|
||||||
|
hint="2D-Höhenlinien aus dem swissALTI3D-DEM. Werden flach auf die OKFF-Ebene des aktiven Geschosses gelegt — direkt zeichnungstauglich. Unabhängig vom 3D-Mesh.">
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: 6,
|
||||||
|
fontSize: 11, cursor: 'pointer' }}>
|
||||||
|
<input type="checkbox" checked={getContours}
|
||||||
|
onChange={(e) => setGetContours(e.target.checked)} />
|
||||||
|
<Icon name="terrain" size={13} /> Höhenlinien (2D, auf aktivem Geschoss)
|
||||||
|
</label>
|
||||||
|
</Field>
|
||||||
|
<Field label=""
|
||||||
|
hint="3D-TIN-Mesh aus den Vertices der Höhenlinien — Delaunay-trianguliert. Stilisierter Topo-Look mit weniger Polygonen als das DEM-Mesh.">
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: 6,
|
||||||
|
fontSize: 11, cursor: 'pointer' }}>
|
||||||
|
<input type="checkbox" checked={getContourTin}
|
||||||
|
onChange={(e) => setGetContourTin(e.target.checked)} />
|
||||||
|
<Icon name="lan" size={13} /> TIN-Mesh aus Höhenlinien
|
||||||
|
</label>
|
||||||
|
</Field>
|
||||||
|
<Field label=""
|
||||||
|
hint="Schichtenmodell: jede geschlossene Höhenlinie wird zur planaren Fläche auf ihrer Z-Höhe — der architektonische Pappmodell-Look.">
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: 6,
|
||||||
|
fontSize: 11, cursor: 'pointer' }}>
|
||||||
|
<input type="checkbox" checked={getContourSchicht}
|
||||||
|
onChange={(e) => setGetContourSchicht(e.target.checked)} />
|
||||||
|
<Icon name="stacks" size={13} /> Schichtenmodell aus Höhenlinien
|
||||||
|
</label>
|
||||||
|
</Field>
|
||||||
|
{(getContours || getContourTin || getContourSchicht) && (
|
||||||
|
<Field label="HÖHEN-ABSTAND">
|
||||||
|
<Radio value={contourInt}
|
||||||
|
options={[
|
||||||
|
{ value: '1.0', label: '1 m (fein)' },
|
||||||
|
{ value: '2.0', label: '2 m (Standard)' },
|
||||||
|
{ value: '5.0', label: '5 m (grob)' },
|
||||||
|
]}
|
||||||
|
onChange={setContourInt} />
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
|
||||||
<SectionLabel>Positionierung</SectionLabel>
|
<SectionLabel>Positionierung</SectionLabel>
|
||||||
|
|
||||||
<Field label="ORIGIN"
|
<Field label="ORIGIN"
|
||||||
@@ -352,7 +424,7 @@ export default function SwisstopoApp() {
|
|||||||
background: 'var(--bg-section)',
|
background: 'var(--bg-section)',
|
||||||
}}>
|
}}>
|
||||||
<div style={{ flex: 1, fontSize: 10, color: 'var(--text-muted)' }}>
|
<div style={{ flex: 1, fontSize: 10, color: 'var(--text-muted)' }}>
|
||||||
{center ? `Tiles werden gecacht in ~/Library/Caches/Dossier/swisstopo/` : 'Wähle zuerst einen Standort'}
|
{center ? `Tiles werden im Projekt-Ordner neben der .3dm gecacht (Fallback: ~/Library/Caches/Dossier/swisstopo/ wenn ungespeichert)` : 'Wähle zuerst einen Standort'}
|
||||||
</div>
|
</div>
|
||||||
<button className="btn-text" onClick={() => send('CANCEL', {})}
|
<button className="btn-text" onClick={() => send('CANCEL', {})}
|
||||||
disabled={running}>
|
disabled={running}>
|
||||||
|
|||||||
@@ -261,6 +261,7 @@ export function exportRaeume() { send('EXPORT_RAEUME', {}) }
|
|||||||
export function openSwisstopo(mode) { send('OPEN_SWISSTOPO', { mode: mode || 'both' }) }
|
export function openSwisstopo(mode) { send('OPEN_SWISSTOPO', { mode: mode || 'both' }) }
|
||||||
export function importSwisstopo(kind) { send('IMPORT_SWISSTOPO', { kind: kind || 'buildings' }) }
|
export function importSwisstopo(kind) { send('IMPORT_SWISSTOPO', { kind: kind || 'buildings' }) }
|
||||||
export function openSwisstopoDialog() { send('OPEN_SWISSTOPO_DIALOG', {}) }
|
export function openSwisstopoDialog() { send('OPEN_SWISSTOPO_DIALOG', {}) }
|
||||||
|
export function openOsmDialog() { send('OPEN_OSM_DIALOG', {}) }
|
||||||
export function updateElement(id, patch) { send('UPDATE_ELEMENT', { id, ...(patch || {}) }) }
|
export function updateElement(id, patch) { send('UPDATE_ELEMENT', { id, ...(patch || {}) }) }
|
||||||
export function deleteElement(id) { send('DELETE_ELEMENT', { id }) }
|
export function deleteElement(id) { send('DELETE_ELEMENT', { id }) }
|
||||||
// Backwards-Compat-Aliases
|
// Backwards-Compat-Aliases
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import LayerCombinationsApp from './LayerCombinationsApp.jsx'
|
|||||||
import AusschnittSettingsApp from './AusschnittSettingsApp.jsx'
|
import AusschnittSettingsApp from './AusschnittSettingsApp.jsx'
|
||||||
import LayoutDialogApp from './LayoutDialogApp.jsx'
|
import LayoutDialogApp from './LayoutDialogApp.jsx'
|
||||||
import SwisstopoApp from './SwisstopoApp.jsx'
|
import SwisstopoApp from './SwisstopoApp.jsx'
|
||||||
|
import OsmApp from './OsmApp.jsx'
|
||||||
import GestaltungApp from './GestaltungApp.jsx'
|
import GestaltungApp from './GestaltungApp.jsx'
|
||||||
import AusschnitteApp from './AusschnitteApp.jsx'
|
import AusschnitteApp from './AusschnitteApp.jsx'
|
||||||
import MassstabApp from './MassstabApp.jsx'
|
import MassstabApp from './MassstabApp.jsx'
|
||||||
@@ -38,6 +39,7 @@ const RootApp = mode === 'gestaltung' ? GestaltungApp
|
|||||||
: mode === 'ausschnitt_settings' ? AusschnittSettingsApp
|
: mode === 'ausschnitt_settings' ? AusschnittSettingsApp
|
||||||
: mode === 'layout_dialog' ? LayoutDialogApp
|
: mode === 'layout_dialog' ? LayoutDialogApp
|
||||||
: mode === 'swisstopo' ? SwisstopoApp
|
: mode === 'swisstopo' ? SwisstopoApp
|
||||||
|
: mode === 'osm' ? OsmApp
|
||||||
: App
|
: App
|
||||||
|
|
||||||
window.onerror = function (msg, src, line, col, err) {
|
window.onerror = function (msg, src, line, col, err) {
|
||||||
|
|||||||
Reference in New Issue
Block a user