Kostenlose Digitale Geländemodelle aus ganz NRW für BricsCAD und AutoCAD (Allgemeines)
Auf dem Geodatenserver der Bezirksregierung Köln hat es mehrere interessante Änderungen gegeben.
Zum einen sind die zu Gemeinden paketierten xyz-Höhendaten nun schon wieder in einem anderen Downloadverzeichnis zu finden (aktuell ist dieser Link gültig). Diese Daten wurden bisher von meinem Python-Programm zur Erstellung von Digitalen Geländemodellen beliebiger Grundstücke in NRW verwendet, das nun eigentlich erneut für diese URL anzupassen gewesen wäre.
Das geschieht aber nicht, denn zusätzlich zu den mehrere Gigabyte großen ZIP-Dateien gibt es nun alle 4-km²-Kacheln mit jeweils 4 Millionen zentimetergenauen NRW-Höhenwerten einzeln als nur noch knapp 20 MB kleine GNU-zip-Dateien. Das langwierige Herunterladen und Auspacken der riesigen ZIP-Archive entfällt endlich und das Programm lädt nun nur noch einzelne kleine Dateien herunter, falls es diese nicht ohnehin schon in einem früheren Programmlauf auf dem Rechner gespeichert hat.
Die dritte Änderung betrifft die Lizenz der Daten. Diese sind nun ohne jede Einschränkung nutzbar. Das Lizenzmodell heißt „Datenlizenz Deutschland - Zero - Version 2.0“
Die Halden Hoppenbruch und Hoheward als TIN-Oberflächen in BricsCAD V20
Um das kostenlose Programm „Geländemodell.py“ auszuführen, benötigen Sie lediglich eine aktuelle Python-Version. Das Programm fragt Sie zuerst nach dem Verzeichnis für die herunterzuladenden Geländedaten und anschließend nach dem Basisnamen der anzulegenden Dateien. Angenommen, Sie möchten ein 3D-Modell der Halde Hoheward in Herten erstellen, so bietet sich als Basisname natürlich so etwas wie „Hoheward“ an. Als nächstes sind zwei Geokoordinaten einzugeben, die Sie zum Beispiel durch zweimaliges Anklicken eines Punktes in einer Google-Maps-Karte erhalten. Bei der Halde Hoheward sind das vielleicht die beiden Punkte „51.553891, 7.149416“ und „51.578594, 7.178255“.
Weitere Benutzereingaben sind gar nicht notwendig. Das Programm lädt die fehlenden Geländkacheln vom Geodatenserver herunter und baut daraus eine Anzahl verschiedener 3D-Dateien zusammen. Während die Dateien geschrieben werden, zeigt das Python-Programm einen Höhenplan des ausgewählten Geländes an.
Als Ausgabedateien erzeugt das Programm:
- eine PNG-Grafik mit dem Matplotlib-Höhendiagramm aus der Abbildung oben
- eine unkomprimierte xyz-Datei der Geodaten im UTM-Koordinatensystem für GIS-Anwendungen
- eine BricsCAD-BIM-Geländedatei im kommagetrennten TXT-Format, bei der anstelle der UTM-Koordinaten relative Meterangaben zur Südwestecke des Geländes verwendet werden und die in Sekundenschnelle mittels des BIM-Befehls „TIN I“ geladen werden kann (siehe Abbildung oben)
- eine DXF-Datei mit farbigen 3D-Flächen, die sich in BricsCAD und AutoCAD zu einem Volumenkörper heften lässt
- eine Stereolithographiedatei (STL) für den 3D-Druck im ASCII-Format und
- eine STL-Datei für den 3D-Druck im Binärformat.
Download des Python-Programms:
Digitales Geländemodell V16.zip
#!/usr/bin/env python3 import os import sys import webbrowser import struct from tkinter import Tk from tkinter.filedialog import askdirectory from math import sqrt, sin, cos, tan, radians, degrees, floor import urllib.request import gzip print(""" Dieses Python3-Skript erstellt Höhenmodelle für CAD und 3D-Druck. Mit zwei Dezimalgradkoordinaten geben Sie die Eckpunkte einer rechteckigen Geländeoberfläche an, für die dieses Programm mehrere 3D-Dateien erzeugt: - ein Höhenmodell im STL-Format für 3D-Drucker, - eine DXF-Datei mit 3D-Flächen, die in Regionen umgewandelt und zu einem Volumenmodell geheftet werden können, - eine auf die von Ihnen gewählte Auswahl und Auflösung reduzierte XYZ-Datei, - eine importfähige DGM-Datei im TXT-Format für BricsCADs SITE-Befehl, sowie - eine Höhenkarte im PNG-Format, falls das Python-Modul Matplotlib installiert ist. """) # Das aus Drei- und Vierecken zusammengesetzte Oberflächenmodell in der # DXF-Datei lässt sich durch Anwenden der beiden Befehle "REGION" und # "DMHEFTEN" (BricsCAD) bzw. "FLÄCHEFORM" (AutoCAD) in ein Volumenmodell # umwandeln. # Das Programm wurde für die öffentlichen Höhendaten für digitale # Geländemodelle in NRW geschrieben, lässt sich aber mit geringen Anpassungen # auch für andere DGM-Daten nutzen. # Autor: Dipl.-Ing. Martin Vogel, Hochschule Bochum, 2017–2020 # Lizenz: Namensnennung - Weitergabe unter gleichen Bedingungen 3.0 Deutschland # (CC BY-SA 3.0 DE) https://creativecommons.org/licenses/by-sa/3.0/de/ # Version 16.0 vom 29. Mai 2020 # Die Download-URL der DGM-Dateien wurde schon wieder geändert. # # Die alten, zu Bezirken zusammengefassten ZIP-Dateien sind nun in # https://www.opengeodata.nrw.de # /produkte/geobasis/hm/dgm1_xyz/dgm1_xyz_paketiert/ # # Diese Riesenpakete werden vom Programm aber gar nicht mehr verwendet, # denn es gibt jetzt ein neues Verzeichnis mit einzelnen, # Gnuzip-komprimierten Dateien, die nur noch um die 20 MB groß sind und # viel besser geeignet sind, unter der URL # https://www.opengeodata.nrw.de # /produkte/geobasis/hm/dgm1_xyz/dgm1_xyz/ # Das Programm liest diese Dateien nun direkt, ohne sie erst aufwendig # auspacken und zwischenspeichern zu müssen. # Version 15.3 vom 22. Januar 2020 # Die Download-URL der DGM-Dateien wurde geändert. # alt: https://www.opengeodata.nrw.de/produkte/geobasis/dgm/dgm1/ # neu: https://www.opengeodata.nrw.de/produkte/geobasis/hm/dgm1_xyz/ # Version 15.2 vom 23. Juni 2019 # Die Quadrate der Geländeoberfläche werden nun so in zwei Dreiecke # aufgeteilt, dass die Diagonale möglichst kurz ist. Die Oberfläche wirkt # dadurch, besonders bei geringen Auflösungen, deutlich realistischer. # Version 15.1 vom 22. Juni 2019 # 3D-Flächen der DXF-Datei mit höhenabhängiger Indexfarbe. # Layer für alle Entities ist nun "Gelaendemodell.py". # Version 15 vom 21. Juni 2019 # Erzeugung von CAD-Scriptdateien aus dem Programm entfernt. # Seit der DXF-Unterstützung waren die SCR-Scripte eigentlich nur noch # eine überflüssige Spielerei. # Version 14.3 vom 21. Juni 2019 # In der DXF-Datei werden jetzt alle Entities explizit Layer "0" zugeordnet. # AutoCAD wirft sonst einen Importfehler aus. # Version 14.2 vom 12. Juni 2019 # Vor dem Herunterladen einer ZIP-Datei wird geprüft, ob sie nicht schon # existiert. # Es werden nur noch die gerade benötigten XYZ-Dateien aus der ZIP-Datei # ausgepackt. # Export des Matplotlib-Höhendiagramms als PNG statt PDF, weil der Export # von PDF- und SVG-Vektordaten hoher Auflösung extrem langsam ist. # Download jetzt mit Fortschrittsanzeige. # Version 14.1 vom 12. Juni 2019 # ZIP-Dateien mit Umlauten im Namen werden jetzt auch unter Microsoft Windows # heruntergeladen. # Version 14 vom 9. Juni 2019 # Fehlende Kacheln werden nun automatisch nachgeladen. # Eine Fortschrittsanzeige fehlt allerdings noch. # Version 13.1 vom 6. Juni 2019 # Bugfix, weil der Verzeichnisauswahldialog auf manchen Mac-OS-Geräten # nicht geschlossen wurde. # Alle alten C-Formatstrings durch f-Strings ersetzt. # Version 13 vom 10. Mai 2019 # XYZ-Daten als TXT-Datei für Bricscad-BIM-Geländeoberflächen. # Version 12 vom 3. Juni 2018 # Anzeige und PDF-Export eines Höhendiagramms mittels Matplotlib. # Version 11 vom 18. Juni 2017 # Zu fehlenden Kacheln wird nun angegeben, in welchen Ortsdateien diese # zu finden sind. Dazu mussten alle 396 Dateien einmal heruntergeladen und # diese 269 Gigabyte ZIP-Dateien katalogisiert werden. -> Gelaendekatalog.csv # Version 10 vom 15. Juni 2017 # UTM-Koordinatenumrechnung jetzt ohne externes Modul und nach vereinfachtem # Algorithmus. # Version 9 vom 13. Juni 2017 # DXF-Export der Dreiecks- und Vierecksflächen aus Version 8. # Version 8 vom 11. Juni 2017 # Oberflächenmodell für CAD-Skript aus einzelnen Dreiecksflächen, wie in der # STL-Datei, Seitenflächen und Boden jedoch aus Vierecken. # Version 7 vom 9. Juni 2017 # Jetzt mit 3D-Netz, viel schneller als Volumenkörper im CAD-Programm. # Leider kann BricsCAD aus dieser Oberfläche keinen Volumenkörper heften. # Version 6 vom 21. März 2017 # Jetzt mit Log-Datei zur Garantie der Wiederholbarkeit. # Version 5 vom 19. März 2017 # Jetzt mit variabler Höhe der Unterseite. Es gibt in NRW tatsächlich Flächen, # die weit unterhalb des Meeresspiegels liegen. (Tagebau Etzweiler: -298,90m!) # Version 4 vom 18. März 2017 # Jetzt mit binärem STL-Export. Die Dateien sind nur noch halb so groß # und werden viel schneller gelesen als ASCII-STL-Dateien. # Version 3 vom 17. März 2017 # Einige mögliche Fehleingaben werden jetzt freundlich abgefangen. # Erweiterung um CAD-Skript für Dreiecksprismen, um den Minecraft-Effekt # loszuwerden. # Version 2 vom 16. März 2017 # Jetzt mit Eingabe der Höhenauflösung. # Außerdem werden nicht mehr alle gefundenen Punkte im Dictionary # abgelegt, sondern nur noch die später auch benötigten. # Ideen für Erweiterungen: # - Grafische Oberfläche (mit Kontrolle der Kachelabdeckung) einbauen. # - In die DXF-Datei noch Werte für Zoom, Blickwinkel, Geokoordinaten # usw. eintragen. def sysexit(): input("\nProgramm wird beendet. [Enter]") sys.exit() def log(s, sichtbar=True): "Bildschirmmeldung mit Protokollierung" with open(name+".log","a") as logfile: logfile.write(f"{s}\n") if sichtbar: print(f"\n{s}") def utm(Bg,Lg): "Umrechnung von Breitengrad und Längengrad in UTM-Koordinaten" # Quelle: http://www.ottmarlabonde.de/L1/UTMBeispielRechnung.htm # Literatur: A. Schödlbauer, # Rechenformeln und Rechenbeispiele zur Landesvermessung, # Teil 2, Herbert WichmannVerlag Karlsruhe L = radians(Lg) B = radians(Bg) tB = tan(B) cB = cos(B) # Halbachsen des WGS84-Ellipsoids: # a = 6378137.0 # b = 6356752.314 # Radiusreduzierung mH = 0.9996 # c = a**2/b c = 6399593.626005325 # eq = (a**2-b**2)/b**2 eq = 0.006739496819936062 # E0 = c*(1-3/4*eq+45/64*eq**2-175/256*eq**3+11025/16384*eq**4) E0 = 6367449.145759811 # E2 = c*(-3/8*eq+15/32*eq**2-525/1024*eq**3+2205/4096*eq**4) E2 = -16038.508797800609 # E4 = c*(15/256*eq**2-105/1024*eq**3+2205/16384*eq**4) E4 = 16.83262765753934 # E6 = c*(-35/3072*eq**3+315/12288*eq**4) E6 = -0.021980907677118407 LL = E0*B + E2*sin(2*B) + E4*sin(4*B) + E6*sin(6*B) x0 = mH*LL LhL = 3 + 6*floor(degrees(L)/6) Zone = 30 + (3+LhL)/6 DL = L - radians(LhL) etaq = eq*cB**2 Nq = c/sqrt(1+etaq) x2 = mH/2*Nq*tB*cB**2 x4 = mH/24*Nq*tB*cB**4*(5-tB**2+9*etaq) x6 = mH/720*Nq*tB*cB**6*(61-58*tB**2+tB**4) N = x0 + x2*DL**2 + x4*DL**4 + x6*DL**6 y1 = mH*Nq*cB y3 = mH/6*Nq*cB**3*(1-tB**2+etaq) y5 = mH/120*Nq*cB**5*(5-18*tB**2+tB**4+etaq*(14-58*tB**2)) E = y1*DL + y3*DL**3 + y5*DL**5 + 500000 return N, E, Zone # Schritt 1: Wie soll das Kind heißen? print("\nGeben Sie einen Basisnamen für die erzeugten Dateien an!\n" "Existierende Dateien mit diesem Namen und den Endungen .dxf, \n" ".xyz, .png, .log, .txt und .stl werden ohne Warnung überschrieben.\n") name = input("Dateiname ohne Endung: ") log("Basisname: "+name, sichtbar=False) # Schritt 2: Wo sind die Geodaten? print("\nDas Programm benötigt Geodaten vom Webserver www.opengeodata.nrw.de." "\n\n" "Bitte wählen Sie den lokalen Ordner für diese Dateien aus.") # Für den Dialog müssen wir Tk laden. Fenster = Tk() # Wir brauchen das Tk-Hauptfenster hier nicht. Fenster.withdraw() # Die unter Windows und Linux überflüssigen Update-Aufrufe sollen einen Bug # unter Mac OS umgehen, durch den der Dialog bis zum Ende des Programms auf dem # Bildschirm bleibt. # https://stackoverflow.com/questions/21866537/what-could-cause-an-open-file- # dialog-window-in-tkinter-python-to-be-really-slow Fenster.update() # Dialogfenster zur Ordnerwahl ordner = askdirectory(title="Bitte den gewünschten Ordner doppelklicken") Fenster.update() if not ordner: print("\nKein Ordner ausgewählt.") sysexit() log(f"Gewählter Ordner: {ordner}") # Schritt 3: Welches Gebiet wollen wir modellieren? print("\nGeben Sie zwei gegenüberliegende Eckpunkte des zu modellierenden\n" "Rechtecks an! Das Eingabeformat ist (Breite, Länge) in Dezimalgrad,\n" "also beispielsweise 51.335757,7.479087 – Sie können die Koordinaten\n" "direkt aus Google Maps oder Ihrem GPS-Gerät übernehmen.\n") try: lat1, lon1 = eval(input("Erstes Eckpunktkoordinatenpaar: ")) lat2, lon2 = eval(input("Gegenüberliegendes Koordinatenpaar: ")) except: print("Es wurden keine zwei mit einem Komma getrennte Zahlen erkannt.") sysexit() # Die Eingabe oben war beliebig, wir brauchen aber gleich die Südwestecke # unten links und die Nordostecke oben rechts: ul_lat, or_lat = sorted((lat1, lat2)) ul_lon, or_lon = sorted((lon1, lon2)) log(f"Geokoordinaten: {ul_lat},{ul_lon} {or_lat},{or_lon}", sichtbar=False) # Umrechnung der Dezimalgradkoordinaten ins UTM-System; aus Längen- und # Breitengrad werden ein Nordwert, ein Ostwert und eine Zonennummer. ul_n, e, zn = map(int, utm(ul_lat, ul_lon)) # Die Zonennummer ist in den Dateien, die unter opengeodata.nrw.de # heruntergeladen werden können, dem Ostwert vorangestellt. ul_e = int(f"{zn}{e}") # Das gleiche noch einmal für die Nordostecke unseres Rechtecks. or_n, e, zn = map(int, utm(or_lat, or_lon)) or_e = int(f"{zn}{e}") log("\nLage und Größe des ausgewählten Rechtecks:") log(f"UTM-Koordinaten: {ul_e},{ul_n} {or_e},{or_n} in Zone {zn}") log(f"Ausdehnung Ost-West: {or_e-ul_e} m") log(f"Ausdehnung Nord-Süd: {or_n-ul_n} m") log(f"Fläche: {(or_e-ul_e)*(or_n-ul_n)} m²") # Schritt 4: Liste der zu öffnenden XYZ-Dateien zusammenbauen. # Bei den NRW-Höhendaten kann man am Dateinamen erkennen, ob gesuchte # Punkte in ihnen enthalten sein könnten. # Der Dateiname gibt die untere linke Ecke einer 2000x2000-m²-Kachel an. # Beispiel für einen Dateinamen: dgm1_32368_5700_2_nw.xyz # Hier sind die Ostwerte 32368000 bis 32369999 und die Nordwerte # 5700000 bis n5701999 enthalten. xyz_Liste = [] e_min = 2 * (ul_e // 2000) e_max = 2 * (or_e // 2000) n_min = 2 * (ul_n // 2000) n_max = 2 * (or_n // 2000) for e in range(e_min,e_max+1,2): for n in range(n_min,n_max+1,2): xyz_Liste.append(f"dgm1_{e}_{n}_2_nw.xyz.gz") # Sind alle Dateien vorhanden? print("\nUntersuche Vollständigkeit der Höhendaten…") # Liste fehlender Geländekacheln fehlende_xyz = [] # Für alle benötigten Kacheln: for xyz_name in xyz_Liste: # Ist diese Kachel im angegebenen Ordner vorhanden? if not os.path.isfile(os.path.join(ordner,xyz_name)): # Nein? Name an Liste fehlender Kacheln anhängen fehlende_xyz.append(xyz_name) # Sind Dateien herunterzuladen? if fehlende_xyz: print("\nGeländekacheln werden heruntergeladen:\n") for xyz_name in fehlende_xyz: # Downloadadresse der gz-Datei: url = ("https://www.opengeodata.nrw.de/" "produkte/geobasis/hm/dgm1_xyz/dgm1_xyz/" f"{xyz_name}") # Datei wird heruntergeladen. print(xyz_name,"…") dateiname = os.path.join(ordner,xyz_name) try: with urllib.request.urlopen(url) as response, \ open(dateiname, 'wb') as datei: datei.write(response.read()) except urllib.error.URLError: print("*** FEHLER ***\n") print("Der Downloadversuch ist gescheitert. Bitte laden Sie\n", f"{url}\n" "mit einem Webbrowser herunter und verschieben Sie die\n" "Datei in den Ordner\n", f"{ordner}.\n") print("Das Programm versucht nun, die Datei von einem " "Webbrowser\n" "herunterladen zu lassen.\n" "Starten Sie es nach dem Download erneut!\n") webbrowser.open(url) sysexit() print("… alle benötigten Dateien sind vorhanden.") # Schritt 5: Modelldetails festlegen print("\nDie horizontale Auflösung der Daten beträgt einen Meter, was bei\n" "größeren Flächen zu extremen Dateigrößen und Verarbeitungszeiten der\n" "erzeugten Modelle führen kann. Tipp: beginnen Sie mit ungefähr 1000\n" "Punkten auf der gesamten Fläche und erhöhen Sie die Auflösung in\n" "weiteren Durchläufen schrittweise.\n") # Vorschlag für die horizontale Auflösung ermitteln; es sollten etwa 1000 # Punkte in der gesamten Fläche liegen. kl0 = max(1, int(((or_e - ul_e) * (or_n - ul_n) / 1000) ** 0.5)) p0 = ((or_e - ul_e) // kl0) * ((or_n - ul_n) // kl0) print(f"Bei {kl0} m Auflösung würden Sie beispielsweise {p0} Punkte " "erhalten.\n") try: kl = int(input("Geben Sie einen ganzzahligen Wert ein [m]: ")) except: kl = 1 print("\nDie vertikale Auflösung der Daten beträgt einen Zentimeter. Das ist\n" "normalerweise in Ordnung. Für einen Höhenschichteneffekt wie bei\n" "architektonischen Geländemodellen kann dieser Wert auch auf 100 oder\n" "mehr geändert werden, je nach gewünschter Effektstärke.\n" "Nebeneffekt: Das CAD-Volumenmodell benötigt bei größeren Werten\n" "deutlich weniger Speicherplatz.\n") try: kh = int(input("Geben Sie einen ganzzahligen Wert ein [cm]: ")) except: kh = 1 # Höhe und Breite als Vielfaches der gewählten Auflösung berechnen xmax = or_e - (or_e - ul_e) % kl ymax = or_n - (or_n - ul_n) % kl log(f"Horizontale Auflösung [m]: {kl}") log(f"Vertikale Auflösung [cm]: {kh}") log(f"\nAusdehnung Ost-West: {xmax-ul_e} m") log(f"Ausdehnung Nord-Süd: {ymax-ul_n} m") fqm = (xmax - ul_e) * (ymax - ul_n) log(f"Fläche: {fqm} m² bzw. {fqm/1e6:.3f} km²") # Schritt 6: Verarbeitung der Eingabedateien. # Alle gefundenen Höhenwerte werden zunächst in ein Dictionary # geschrieben, aus dem sie für die einzelnen Dateien wieder # ausgelesen werden. D = {} # Die Höhe der Unterseite ist nicht null, sondern orientiert sich # am tatsächlichen Gelände. minh = float("inf") maxh = -float("inf") # Schleife über alle zu verwendenden Eingabedateien for dateiname in xyz_Liste: with gzip.open(os.path.join(ordner, dateiname)) as dgm: log(f"Verarbeite Kachel {dateiname}") for zeile in dgm: # Beispiel für eine Zeile aus Bochum: # 32372000.00 5706000.00 61.32 try: x,y,h = zeile.split() x = int(float(x)) y = int(float(y)) h = float(h) except ValueError: print("Abbruch, falsches Format:",zeile) break # Koordinaten der Zeile im gesuchten Rechteck? if ul_e <= x <= or_e and ul_n <= y <= or_n: # Liegt der Punkt im ausgedünnten Horizontalraster? if not (x - ul_e) % kl and not (y - ul_n) % kl: # Punkt wird bei gewählter Auflösung verwendet if kh != 1: # Höhenwerte werden gerundet h = round(round(h*100/kh)*kh/100, 2) D[x,y] = h minh = min(h, minh) maxh = max(h, maxh) log(f"Größte gefundene Höhe: {maxh:.2f} Meter") log(f"Kleinste gefundene Höhe: {minh:.2f} Meter") # "echten" minh-Wert für später merken minhs = minh # Unterkante des Modells absenken: minh = 10 * floor(minh / 10) - 10 log(f"Setze Unterkante auf {minh:.2f} Meter.") log(f"Neue Modellhöhe: {maxh-minh:.2f} Meter") ############################################################## # Die Verarbeitung der Eingabedateien ist nun abgeschlossen. # ############################################################## # Schritt 7: Erzeugen der Ausgabedateien. ########################## # Matplotlib-Höhendiagramm # Das Programm soll auch ohne Matplotlib durchlaufen. try: import matplotlib.pyplot as plt except ModuleNotFoundError: print("\nEin Höhendiagramm kann nicht angezeigt werden,\n" " weil Matplotlib nicht installiert ist.\n") plt = None # Wenn der Matplotlib-Import erfolgreich war: if plt: # Diagramm anzeigen print("\nHöhendiagramm wird erzeugt.") # Keine Interaktion, Diagramm nur anzeigen … plt.rcParams['toolbar'] = 'None' # Einheitlicher Maßstab für x und y plt.axis('equal') # Für das Diagramm wird die untere linke Ecke auf (0,0) # gesetzt. Wer kann schon was mit UTM-Koordinaten anfangen? # Liste der x-Werte xi = list(range(0, or_e - ul_e + 1, kl)) # Liste der y-Werte yi = list(range(0, or_n - ul_n + 1, kl)) # Matrix der Höhenwerte für alle x-y-Paare zi = [[D[(x + ul_e, y + ul_n)] for x in xi] for y in yi] # Anzahl der Höhenlinien: etwa 10 (7 bis 14) nh = (maxh - minhs) * 100 while nh > 70: nh = int(nh / 10) if nh > 35: nh = int(nh / 5) if nh > 14: nh = int(nh / 2) # print("Zeichne",nh,"Höhenlinien.") # Höhenlinien plt.contour(xi, yi, zi, nh, linewidths=1, colors="k") # farbige Oberfläche plt.pcolormesh(xi, yi, zi, cmap = plt.get_cmap('terrain')) # Legende plt.colorbar() # Überschrift plt.title(f"Ausschnittgröße {xmax-ul_e}×{ymax-ul_n} m\n" f"(0,0) bei {ul_lat}° Nord, {ul_lon}° Ost") plt.gcf().canvas.set_window_title(f'Höhendiagramm {name}') # Das Höhendiagramm anzeigen und zurück zum Programm … plt.show(block=False) # Bitte nicht erst auf eine günstige Gelegenheit zur Anzeige warten, # sondern das Diagramm sofort anzeigen! plt.gcf().canvas.flush_events() # Bild als PNG speichern ausname = name+".png" log(f"Schreibe PNG-Ausgabedatei: {ausname}") plt.savefig(ausname, bbox_inches='tight', dpi=600) ########### # XYZ-Datei # Alle verwendeten Punkte als simple XYZ-Datei sichern ausname = name+".xyz" log(f"Schreibe XYZ-Ausgabedatei: {ausname}") with open(ausname,"w") as aus: for x in range(ul_e,or_e+1,kl): for y in range(ul_n,or_n+1,kl): aus.write(f"{x} {y} {D[(x,y)]}\n") ############### # BIM-TXT-Datei # XYZ-Daten für BricsCAD mit Komma trennen und zum Koordinatenursprung # verschieben. ausname = name+".txt" log(f"Schreibe BricsCAD-BIM-Geländedatei: {ausname}") with open(ausname,"w") as aus: for x in range(ul_e,or_e+1,kl): for y in range(ul_n,or_n+1,kl): aus.write(f"{x-ul_e},{y-ul_n},{D[(x,y)]}\n") ############ # DXF-Export ausname = name+".dxf" log(f"Schreibe DXF-Datei mit 3D-Flächen: {ausname}") # Weil 3D-Solids in einem obskuren „Geheimformat“ gespeichert werden, wird # hier nur die umhüllende Fläche erzeugt. Diese muss im CAD-Programm mittels # der Befehle REGION ALLE und DMHEFTEN bzw. FLÄCHEFORM umgewandelt werden, # um einen richtigen Geländekörper zu erhalten. def hf(h): # "Höhenfarbe" # Gibt abhängig von der relativen Höhe eine von 17 Indexfarben zurück, # Erscheinungsbild ähnelt dem topographischer Karten. return (194, 182, 170, 160, 150, 134, 112, 100, 91, 71, 61, 50, 40, 42, 31, 9, 254)[int(17*(h-minhs)/(maxh-minhs))] with open(ausname,"w") as aus: # Kopf der DXF-Datei schreiben aus.write("0\nSECTION\n2\nHEADER\n" "9\n$ACADVER\n1\nAC1006\n" "9\n$INSBASE\n10\n0.0\n20\n0.0\n30\n0.0\n" "9\n$INSUNITS\n70\n6\n" "9\n$EXTMIN\n10\n0.0\n20\n0.0\n" f"9\n$EXTMAX\n10\n{xmax-ul_e}\n20\n{ymax-ul_n}\n" "9\n$LIMMIN\n10\n0.0\n20\n0.0\n" f"9\n$LIMMAX\n10\n{xmax-ul_e}\n20\n{ymax-ul_n}\n" "0\nENDSEC\n" "0\nSECTION\n2\nTABLES\n" "0\nTABLE\n2\nLAYER\n" "70\n2\n" "0\nLAYER\n2\nGelaendemodell.py\n" "70\n64\n" "62\n7\n" "6\nCONTINUOUS\n" "0\nENDTAB\n" "0\nENDSEC\n" "0\nSECTION\n2\nENTITIES\n") # Geländeoberfläche for x in range(ul_e,xmax+1-kl,kl): xu = x - ul_e for y in range(ul_n,ymax+1-kl,kl): yu = y - ul_n h1 = D[x,y] h2 = D[x+kl,y] h3 = D[x,y+kl] h4 = D[x+kl,y+kl] # Die vier Eckpunkte werden auf zwei Dreiecke aufgeteilt. # Als Diagonale wird die kürzere der beiden Verbindungen # 1-4 bzw. 2-3 gewählt: # entweder # 3 4 4 3 4 # 1 2 = 1 2 + 1 # oder # 3 4 3 3 4 # 1 2 = 1 2 + 2 # Ein Dreieck ist im DXF-Format ein Viereck, bei dem der letzte Punkt # doppelt vorkommt. if abs(h4-h1) < abs(h2-h3): # Diagonale 1-4 aus.write("0\n3DFACE\n8\nGelaendemodell.py\n" f"62\n{hf((h1+h4+h2)/3)}\n" f"10\n{xu}\n20\n{yu}\n30\n{h1}\n" f"11\n{xu+kl}\n21\n{yu+kl}\n31\n{h4}\n" f"12\n{xu+kl}\n22\n{yu}\n32\n{h2}\n" f"13\n{xu+kl}\n23\n{yu}\n33\n{h2}\n") aus.write("0\n3DFACE\n8\nGelaendemodell.py\n" f"62\n{hf((h1+h3+h4)/3)}\n" f"10\n{xu}\n20\n{yu}\n30\n{h1}\n" f"11\n{xu}\n21\n{yu+kl}\n31\n{h3}\n" f"12\n{xu+kl}\n22\n{yu+kl}\n32\n{h4}\n" f"13\n{xu+kl}\n23\n{yu+kl}\n33\n{h4}\n") else: # Diagonale 2-3 aus.write("0\n3DFACE\n8\nGelaendemodell.py\n" f"62\n{hf((h1+h3+h2)/3)}\n" f"10\n{xu}\n20\n{yu}\n30\n{h1}\n" f"11\n{xu}\n21\n{yu+kl}\n31\n{h3}\n" f"12\n{xu+kl}\n22\n{yu}\n32\n{h2}\n" f"13\n{xu+kl}\n23\n{yu}\n33\n{h2}\n") aus.write("0\n3DFACE\n8\nGelaendemodell.py\n" f"62\n{hf((h2+h3+h4)/3)}\n" f"10\n{xu+kl}\n20\n{yu}\n30\n{h2}\n" f"11\n{xu}\n21\n{yu+kl}\n31\n{h3}\n" f"12\n{xu+kl}\n22\n{yu+kl}\n32\n{h4}\n" f"13\n{xu+kl}\n23\n{yu+kl}\n33\n{h4}\n") # Unterseite aus.write("0\n3DFACE\n8\nGelaendemodell.py\n" "62\n47\n" f"10\n0\n20\n0\n30\n{minh}\n" f"11\n{xmax-ul_e}\n21\n0\n31\n{minh}\n" f"12\n{xmax-ul_e}\n22\n{ymax-ul_n}\n32\n{minh}\n" f"13\n0\n23\n{ymax-ul_n}\n33\n{minh}\n") # Linke Wand for y in range(ul_n,ymax+1-kl,kl): yu = y-ul_n h1 = D[ul_e,y] h2 = D[ul_e,y+kl] aus.write("0\n3DFACE\n8\nGelaendemodell.py\n" "62\n47\n" f"10\n0\n20\n{yu}\n30\n{minh}\n" f"11\n0\n21\n{yu+kl}\n31\n{minh}\n" f"12\n0\n22\n{yu+kl}\n32\n{h2}\n" f"13\n0\n23\n{yu}\n33\n{h1}\n") # Rechte Wand for y in range(ul_n,ymax+1-kl,kl): xu = xmax-ul_e yu = y-ul_n h1 = D[xmax,y] h2 = D[xmax,y+kl] aus.write("0\n3DFACE\n8\nGelaendemodell.py\n" "62\n47\n" f"10\n{xu}\n20\n{yu}\n30\n{minh}\n" f"11\n{xu}\n21\n{yu}\n31\n{h1}\n" f"12\n{xu}\n22\n{yu+kl}\n32\n{h2}\n" f"13\n{xu}\n23\n{yu+kl}\n33\n{minh}\n") # Vordere Wand for x in range(ul_e,xmax+1-kl,kl): h1 = D[x,ul_n] h2 = D[x+kl,ul_n] xu = x - ul_e aus.write("0\n3DFACE\n8\nGelaendemodell.py\n" "62\n47\n" f"10\n{xu}\n20\n0\n30\n{minh}\n" f"11\n{xu}\n21\n0\n31\n{h1}\n" f"12\n{xu+kl}\n22\n0\n32\n{h2}\n" f"13\n{xu+kl}\n23\n0\n33\n{minh}\n") # Hintere Wand for x in range(ul_e,xmax+1-kl,kl): xu = x - ul_e yu = ymax - ul_n h1 = D[x,ymax] h2 = D[x+kl,ymax] aus.write("0\n3DFACE\n8\nGelaendemodell.py\n" "62\n47\n" f"10\n{xu}\n20\n{yu}\n30\n{minh}\n" f"11\n{xu+kl}\n21\n{yu}\n31\n{minh}\n" f"12\n{xu+kl}\n22\n{yu}\n32\n{h2}\n" f"13\n{xu}\n23\n{yu}\n33\n{h1}\n") aus.write("0\nENDSEC\n0\nEOF\n") ################################## # Stereolitographiedatei STL ASCII ausname = name+".ascii.stl" log(f"Schreibe STL-Datei für 3D-Druck (ASCII): {ausname}") # Die Flächen umhüllen einen Quader, der unten auf Höhe minh aufliegt # und oben durch die Geländeoberfläche abgeschnitten wird. # Für die „Flächennormalen“ wird hier immer eine der Achsenrichtungen # eingesetzt. Für die Geländeoberfläche ist das beispielsweise die # nach oben gerichtete z-Achse (0, 0, 1). # Der genaue Winkel ist auch gar nicht relevant. Hauptsache, # die „Normale“ zeigt irgendwie aus dem umhüllten Körper heraus # und nicht in ihn hinein. with open(ausname,"w") as aus: aus.write("solid "+name+"\n") # Geländeoberfläche for x in range(ul_e,xmax+1-kl,kl): xu = x - ul_e for y in range(ul_n,ymax+1-kl,kl): yu = y - ul_n h1 = D[x,y] h2 = D[x+kl,y] h3 = D[x,y+kl] h4 = D[x+kl,y+kl] # Kürzere Diagonale wählen if abs(h4-h1) < abs(h2-h3): aus.write("facet normal 0 0 1\n" "outer loop\n" f"vertex {xu} {yu} {h1}\n" f"vertex {xu+kl} {yu+kl} {h4}\n" f"vertex {xu+kl} {yu} {h2}\n" "endloop\n" "endfacet\n") aus.write("facet normal 0 0 1\n" "outer loop\n" f"vertex {xu} {yu} {h1}\n" f"vertex {xu} {yu+kl} {h3}\n" f"vertex {xu+kl} {yu+kl} {h4}\n" "endloop\n" "endfacet\n") else: aus.write("facet normal 0 0 1\n" "outer loop\n" f"vertex {xu} {yu} {h1}\n" f"vertex {xu} {yu+kl} {h3}\n" f"vertex {xu+kl} {yu} {h2}\n" "endloop\n" "endfacet\n") aus.write("facet normal 0 0 1\n" "outer loop\n" f"vertex {xu+kl} {yu} {h2}\n" f"vertex {xu} {yu+kl} {h3}\n" f"vertex {xu+kl} {yu+kl} {h4}\n" "endloop\n" "endfacet\n") # Was für ein Aufwand für zwei popelige Dreiecke! # Die Unterseite und die Seitenflächen wiederholen im Moment # die Unterteilung der Oberseite. Dadurch hat die STL-Datei fast # doppelt so viele Dreiecke wie eigentlich nur nötig wären. # Durch geschickte Aufteilung könnte die Unterseite mit nur zwei # Dreiecken realisiert werden, wobei die Seitenflächen mit rund der # Hälfte der Dreiecke auskommen könnten. Bei großen Geländesteigungen # gibt es da aber ein paar Herausforderungen an den Algorithmus. # Unterseite for x in range(ul_e,xmax+1-kl,kl): xu = x - ul_e for y in range(ul_n,ymax+1-kl,kl): yu = y - ul_n aus.write("facet normal 0 0 -1\n" "outer loop\n" f"vertex {xu} {yu} {minh}\n" f"vertex {xu+kl} {yu} {minh}\n" f"vertex {xu+kl} {yu+kl} {minh}\n" "endloop\n" "endfacet\n") aus.write("facet normal 0 0 -1\n" "outer loop\n" f"vertex {xu} {yu} {minh}\n" f"vertex {xu+kl} {yu+kl} {minh}\n" f"vertex {xu} {yu+kl} {minh}\n" "endloop\n" "endfacet\n") # Linke Wand for y in range(ul_n,ymax+1-kl,kl): yu = y-ul_n h1 = D[ul_e,y] h2 = D[ul_e,y+kl] aus.write("facet normal -1 0 0\n" "outer loop\n" f"vertex 0 {yu} {minh}\n" f"vertex 0 {yu+kl} {h2}\n" f"vertex 0 {yu} {h1}\n" "endloop\n" "endfacet\n") aus.write("facet normal -1 0 0\n" "outer loop\n" f"vertex 0 {yu} {minh}\n" f"vertex 0 {yu+kl} {minh}\n" f"vertex 0 {yu+kl} {h2}\n" "endloop\n" "endfacet\n") # Rechte Wand for y in range(ul_n,ymax+1-kl,kl): xu = xmax-ul_e yu = y-ul_n h1 = D[xmax,y] h2 = D[xmax,y+kl] aus.write("facet normal 1 0 0\n" "outer loop\n" f"vertex {xu} {yu} {minh}\n" f"vertex {xu} {yu} {h1}\n" f"vertex {xu} {yu+kl} {h2}\n" "endloop\n" "endfacet\n") aus.write("facet normal 1 0 0\n" "outer loop\n" f"vertex {xu} {yu} {minh}\n" f"vertex {xu} {yu+kl} {h2}\n" f"vertex {xu} {yu+kl} {minh}\n" "endloop\n" "endfacet\n") # Vordere Wand for x in range(ul_e,xmax+1-kl,kl): h1 = D[x,ul_n] h2 = D[x+kl,ul_n] xu = x - ul_e aus.write("facet normal 0 -1 0\n" "outer loop\n" f"vertex {xu} 0 {minh}\n" f"vertex {xu} 0 {h1}\n" f"vertex {xu+kl} 0 {h2}\n" "endloop\n" "endfacet\n") aus.write("facet normal 0 -1 0\n" "outer loop\n" f"vertex {xu} 0 {minh}\n" f"vertex {xu+kl} 0 {h2}\n" f"vertex {xu+kl} 0 {minh}\n" "endloop\n" "endfacet\n") # Hintere Wand for x in range(ul_e,xmax+1-kl,kl): xu = x - ul_e yu = ymax - ul_n h1 = D[x,ymax] h2 = D[x+kl,ymax] aus.write("facet normal 0 1 0\n" "outer loop\n" f"vertex {xu} {yu} {minh}\n" f"vertex {xu+kl} {yu} {h2}\n" f"vertex {xu} {yu} {h1}\n" "endloop\n" "endfacet\n") aus.write("facet normal 0 1 0\n" "outer loop\n" f"vertex {xu} {yu} {minh}\n" f"vertex {xu+kl} {yu} {minh}\n" f"vertex {xu+kl} {yu} {h2}\n" "endloop\n" "endfacet\n") aus.write("endsolid "+name+"\n") ################################## # Stereolitographiedatei STL Binär # Hier geschieht das gleiche wie bei der STL-ASCII-Ausgabe, nur die # erzeugte Datei ist kleiner. # # https://de.wikipedia.org/wiki/STL-Schnittstelle # # Die struct-Codes unten bedeuten: # "<" niedrigwertiges Byte voran ("little-endian") # "I" vorzeichenlose Ganzzahl (4 Byte) # "12f" 12 Gleitkommawerte (je 4 Byte) # "h" kurze Ganzzahl (2 Byte) ausname = name+".binär.stl" log(f"Schreibe STL-Datei für 3D-Druck (binär): {ausname}") with open(ausname,"wb") as aus: # 80 Bytes ungenutzter Header aus.write(b'\0' * 80) # Wie viele Dreiecke hat das Modell insgesamt? nx = (xmax - ul_e) // kl ny = (ymax - ul_n) // kl ngesamt = 4 * nx*ny + 4 * nx + 4 * ny aus.write(struct.pack('<I', ngesamt)) # Geländeoberfläche for x in range(ul_e,xmax+1-kl,kl): xu = x - ul_e for y in range(ul_n,ymax+1-kl,kl): yu = y - ul_n h1 = D[x,y] h2 = D[x+kl,y] h3 = D[x,y+kl] h4 = D[x+kl,y+kl] if abs(h4-h1) < abs(h2-h3): aus.write(struct.pack("<12fh", 0, 0, 1, xu, yu, h1, xu+kl, yu+kl, h4, xu+kl, yu, h2, 0)) aus.write(struct.pack("<12fh", 0, 0, 1, xu, yu, h1, xu, yu+kl, h3, xu+kl, yu+kl, h4, 0)) else: aus.write(struct.pack("<12fh", 0, 0, 1, xu, yu, h1, xu, yu+kl, h3, xu+kl, yu, h2, 0)) aus.write(struct.pack("<12fh", 0, 0, 1, xu+kl, yu, h2, xu, yu+kl, h3, xu+kl, yu+kl, h4, 0)) # Unterseite for x in range(ul_e,xmax+1-kl,kl): xu = x - ul_e for y in range(ul_n,ymax+1-kl,kl): yu = y - ul_n aus.write(struct.pack("<12fh", 0, 0, -1, xu, yu, minh, xu+kl, yu+kl, minh, xu+kl, yu, minh, 0)) aus.write(struct.pack("<12fh", 0, 0, -1, xu, yu, minh, xu, yu+kl, minh, xu+kl, yu+kl, minh, 0)) # Linke Wand for y in range(ul_n,ymax+1-kl,kl): yu = y-ul_n h1 = D[ul_e,y] h2 = D[ul_e,y+kl] aus.write(struct.pack("<12fh", -1, 0, 0, 0, yu, minh, 0, yu+kl, h2, 0, yu, h1, 0)) aus.write(struct.pack("<12fh", -1, 0, 0, 0, yu, minh, 0, yu+kl, minh, 0, yu+kl, h2, 0)) # Rechte Wand xu = xmax-ul_e for y in range(ul_n,ymax+1-kl,kl): yu = y-ul_n h1 = D[xmax,y] h2 = D[xmax,y+kl] aus.write(struct.pack("<12fh", 1, 0, 0, xu, yu, minh, xu, yu, h1, xu, yu+kl, h2, 0)) aus.write(struct.pack("<12fh", 1, 0, 0, xu, yu, minh, xu, yu+kl, h2, xu, yu+kl, minh, 0)) # Vordere Wand for x in range(ul_e,xmax+1-kl,kl): h1 = D[x,ul_n] h2 = D[x+kl,ul_n] xu = x - ul_e aus.write(struct.pack("<12fh", 0, -1, 0, xu, 0, minh, xu, 0, h1, xu+kl, 0, h2, 0)) aus.write(struct.pack("<12fh", 0, -1, 0, xu, 0, minh, xu+kl, 0, h2, xu+kl, 0, minh, 0)) # Hintere Wand yu = ymax - ul_n for x in range(ul_e,xmax+1-kl,kl): xu = x - ul_e h1 = D[x,ymax] h2 = D[x+kl,ymax] aus.write(struct.pack("<12fh", 0, 1, 0, xu, yu, minh, xu+kl, yu, h2, xu, yu, h1, 0)) aus.write(struct.pack("<12fh", 0, 1, 0, xu, yu, minh, xu+kl, yu, minh, xu+kl, yu, h2, 0)) log("Programmlauf erfolgreich beendet.\n\n") print("Die Ausgabedateien können nun weiterverarbeitet werden.") input("\nProgramm schließen mit [Enter]") if plt: plt.close()
--
Dipl.-Ing. Martin Vogel
Leiter des Bauforums
Bücher:
CAD mit BricsCAD
Bauinformatik mit Python