Dieser Python-Code ist ein Werkzeug zur automatisierten Generierung von Baumdarstellungen innerhalb von definierten Flächen, die aus verschiedenen Vektordatenformaten (DXF, SVG, Shapefile) importiert werden können. Die generierten Baumpositionen können dann in verschiedene Ausgabeformate (DXF, SVG, Shapefile) exportiert werden.
Was dieser Code macht:
- Importieren von Vektordaten:
- Er kann Geometrien aus DXF-, SVG- und Shapefile-Dateien einlesen.
- Für DXF-Dateien werden LWPOLYLINE, POLYLINE und LINE-Entitäten verarbeitet.
- Für SVG-Dateien werden Pfade extrahiert.
- Für Shapefiles werden Polygone und Linien extrahiert.
- Benutzereingabe:
- Über ein tkinter-basiertes GUI werden Parameter wie die Auflösung der Baumkronen (als Polygone), die Anzahl unterschiedlicher Baumdurchmesser, deren Durchmesser, prozentualer Anteil und Farbe abgefragt.
- Es gibt eine Option, Bäume in Clustern anzuordnen, und der Benutzer kann den Prozentsatz der Bäume festlegen, die geclustert werden sollen.
- Der Benutzer kann die gewünschten Ausgabeformate (Shapefile, SVG, DXF) auswählen.
- Der Benutzer kann wählen, ob nur zufällige, nur Cluster-basierte oder beide Varianten der Baumverteilung generiert werden sollen.
- Baumplatzierung:
- Die Funktion generate_tree_positions platziert Bäume innerhalb der importierten Geometrien.
- Sie berücksichtigt die angegebenen Durchmesser und deren prozentualen Anteil an der Gesamtbaumbepflanzung.
- Es wird überprüft, ob neu platzierte Bäume mit bestehenden Bäumen überlappen, um realistische Abstände zu gewährleisten.
- Für die Platzierung wird die shape_path.contains_point-Methode verwendet, um sicherzustellen, dass Bäume innerhalb der definierten Form platziert werden.
- Es gibt eine Logik zur Erstellung eines Rasterpunktrasters für den Fall, dass nur ein Baumdurchmesser verwendet wird, was eine gleichmäßigere Verteilung ermöglicht.
- Interaktive Vorschau:
- Es wird ein separates tkinter-Fenster mit einer matplotlib-Visualisierung der importierten Geometrie und der generierten Bäume angezeigt.
- Ein Slider ermöglicht die interaktive Anpassung der „Füllungsintensität“, was die Dichte der platzierten Bäume beeinflusst.
- Dies erlaubt dem Benutzer, die Baumplatzierung vor dem endgültigen Export zu überprüfen und anzupassen.
- Export in verschiedene Formate:
- Die generierten Baumpositionen werden zusammen mit den importierten Geometrien in die ausgewählten Ausgabeformate exportiert.
- DXF: Bäume werden als Kreise auf separaten Layern nach ihrem Durchmesser gezeichnet. Die Farben werden als True-Color-Werte gespeichert.
- SVG: Die Visualisierung (inklusive der Bäume) wird als SVG-Datei gespeichert.
- Shapefile: Bäume werden als Polygone (approximierte Kreise) mit Attributen wie ID, Radius, Durchmesser und Farbe gespeichert. Der Benutzer wird nach dem Koordinatensystem (EPSG-Code) gefragt, falls es nicht aus der Eingabedatei übernommen werden kann.
- Batch-Verarbeitung:
- Der Code ermöglicht die Auswahl mehrerer Eingabedateien für die Verarbeitung, wodurch eine Batch-Verarbeitung möglich ist.
- Skalierung für SVG:
- Für SVG-Dateien fragt der Code nach einer maximalen Länge und skaliert die importierte Geometrie entsprechend. Dies ist nützlich, um SVG-Zeichnungen auf eine bestimmte Größe zu bringen.
- Eindeutige Dateinamen:
- Die Funktion get_unique_filename stellt sicher, dass beim Export keine Dateien überschrieben werden, indem eindeutige Dateinamen generiert werden (z.B. durch Hinzufügen von Versionsnummern).
Was diesen Code besonders macht:
- Integration verschiedener Formate: Die Fähigkeit, sowohl CAD-Formate (DXF, DWG) als auch GIS-Formate (Shapefile) und Grafikformate (SVG) zu lesen und zu schreiben, macht ihn sehr flexibel und vielseitig einsetzbar.
- Interaktive Baumplatzierung: Die Vorschaufunktion mit dem Füllintensitäts-Slider ermöglicht eine intuitive Anpassung der Baumdichte und -verteilung. Dies ist ein großer Vorteil gegenüber rein algorithmischen Ansätzen.
- Berücksichtigung von Baumdurchmessern und Proportionen: Die Möglichkeit, verschiedene Baumdurchmesser mit unterschiedlichen Anteilen und Farben zu definieren, ermöglicht realistischere und differenziertere Pflanzpläne.
- Optionale Clusterbildung: Die Möglichkeit, Bäume in Clustern anzuordnen, kann die Natürlichkeit der Pflanzung erhöhen.
- Benutzerfreundliche GUI: Die tkinter-Oberfläche erleichtert die Bedienung, auch für Benutzer ohne tiefe Programmierkenntnisse.
- Umfassende Parametrisierung: Viele Aspekte der Baumplatzierung und des Exports sind konfigurierbar.
- Behandlung von Koordinatensystemen: Die Berücksichtigung von Koordinatensystemen beim Export von Shapefiles ist wichtig für die Integration in GIS-Systeme.
Anwendungsbeispiele in einem landschaftsarchitektonischen Projekt:
- Erstellung von Pflanzplänen:
- Der Code kann verwendet werden, um automatisch Baumstandorte innerhalb von Projektgrenzen zu generieren, die durch DXF-Pläne (z.B. Gebäudegrundrisse, Wegeführungen), SVG-Zeichnungen oder Shapefiles (z.B. Grundstücksgrenzen) definiert sind.
- Landschaftsarchitekten können verschiedene Baumarten und -größen mit entsprechenden Proportionen festlegen und die Software die Platzierung übernehmen lassen.
- Die interaktive Vorschau ermöglicht es, die Dichte und Verteilung der Bäume zu visualisieren und anzupassen, um ästhetische und funktionale Anforderungen zu erfüllen (z.B. Sichtachsen, Schattenspenden).
- Visualisierung von Baumpflanzungen:
- Die SVG-Exportfunktion ermöglicht die Erstellung von Vektorgrafiken, die für Präsentationen und Pläne verwendet werden können. Die farbliche Unterscheidung der Baumkronen nach Durchmesser erhöht die Verständlichkeit.
- Erstellung von technischen Zeichnungen:
- Der DXF-Export ermöglicht die Integration der Baumstandorte in CAD-Pläne für die Bauausführung. Die Layer-basierte Organisation nach Baumdurchmesser kann die Organisation und Bearbeitung im CAD erleichtern.
- Datenerfassung für GIS:
- Der Shapefile-Export ermöglicht die Übergabe der Baumstandorte und ihrer Attribute (Durchmesser, Farbe) an GIS-Systeme für weitere Analysen (z.B. Berechnung der Baumkronenfläche, räumliche Beziehungen zu anderen Elementen).
- Entwurfsiterationen und Variantenstudien:
- Durch die einfache Parametrisierung können schnell verschiedene Pflanzvarianten mit unterschiedlichen Baumgrößen, Dichten und Anordnungen generiert und verglichen werden. Die Möglichkeit, zufällige und Cluster-basierte Anordnungen zu generieren, bietet Flexibilität bei der Gestaltung.
- Bestandsaufnahme und Analyse:
- Wenn vorhandene Baumstandorte in einem der unterstützten Formate vorliegen, könnte der Code angepasst werden, um diese zu visualisieren oder Analysen auf Basis der Baumdurchmesser durchzuführen.
Zusammenfassend lässt sich sagen, dass dieser Code ein wertvolles Werkzeug für Landschaftsarchitekten ist, um den Prozess der Baumplatzierung in Entwürfen zu automatisieren und zu visualisieren, die Effizienz zu steigern und kreative Möglichkeiten zu eröffnen. Die Kombination aus der Unterstützung verschiedener Dateiformate, der interaktiven Vorschau und der flexiblen Parametrisierung macht ihn zu einem mächtigen Instrument für die Planung und Dokumentation von Baumpflanzungen.
import os
import ezdxf
import numpy as np
import matplotlib
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt
import random
import tkinter as tk
from tkinter import filedialog, messagebox, simpledialog, colorchooser
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
from matplotlib.path import Path
from shapely.geometry import shape, mapping, Polygon, Point, MultiPolygon, LineString, MultiLineString
import fiona
from pyproj import CRS
# Korrekte Importierung von rgb2int
from ezdxf.colors import rgb2int
# Funktion zur Überprüfung, ob sich zwei Kreise überschneiden
def check_overlap(x1, y1, r1, x2, y2, r2, min_distance=0):
distance = np.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
return distance < r1 + r2 + min_distance
# Funktion zur Berechnung der Fläche der Form mittels Shoelace-Formel
def compute_shape_area(polylines):
total_area = 0.0
for polyline in polylines:
if len(polyline) < 3:
continue # Keine gültige Polygonfläche
x, y = zip(*polyline)
# Sicherstellen, dass das Polygon geschlossen ist
if (x[0], y[0]) != (x[-1], y[-1]):
x = list(x) + [x[0]]
y = list(y) + [y[0]]
total_area += 0.5 * np.abs(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1)))
return total_area
# Funktion zum Lesen der Eingabedatei basierend auf dem Dateiformat
def read_input_file(file_path):
extension = file_path.split('.')[-1].lower()
if extension == 'dxf':
return process_dxf(file_path)
elif extension == 'dwg':
messagebox.showinfo("Hinweis", "Bitte konvertiere die DWG-Datei zuerst in eine DXF-Datei.")
return []
elif extension == 'svg':
return process_svg(file_path)
elif extension == 'shp':
return process_shapefile(file_path)
else:
messagebox.showerror("Fehler", "Nicht unterstütztes Dateiformat.")
return []
# Funktion zum Schreiben der Ausgabedatei basierend auf dem gewünschten Format
def write_output_file(file_path, polylines, tree_positions, crs, fig=None):
extension = file_path.split('.')[-1].lower()
if extension == 'dxf':
write_dxf(file_path, polylines, tree_positions)
elif extension == 'svg':
if fig is not None:
write_svg_matplotlib(fig, file_path)
elif extension == 'shp':
# Wenn crs None ist, den Benutzer nach dem EPSG-Code fragen
if crs is None:
crs_input = simpledialog.askstring(
"Koordinatensystem für Export",
"Bitte gib den EPSG-Code des Koordinatensystems für das Shapefile ein (z.B. 32632 für UTM Zone 32N):"
)
if crs_input is None:
messagebox.showerror("Fehler", "Kein Koordinatensystem eingegeben.")
return
try:
crs_epsg = int(crs_input)
crs = CRS.from_epsg(crs_epsg)
except ValueError:
messagebox.showerror("Fehler", "Ungültiger EPSG-Code.")
return
write_shapefile(file_path[:-4], polylines, tree_positions, crs)
else:
messagebox.showerror("Fehler", "Nicht unterstütztes Exportformat.")
# Funktion zum Schreiben einer DXF-Datei einschließlich der Bäume
def write_dxf(file_path, polylines, tree_positions):
doc = ezdxf.new(dxfversion='R2000')
msp = doc.modelspace()
# Setze Einheiten auf Meter
doc.header['$INSUNITS'] = 6 # 6 entspricht Metern in DXF
# Layer für Geometrie erstellen
doc.layers.new(name='Geometrie', dxfattribs={'color': 7})
# Hinzufügen der Polylinien auf Layer 'Geometrie'
for polyline in polylines:
if len(polyline) < 2:
continue
msp.add_lwpolyline(polyline, format='xy', dxfattribs={'layer': 'Geometrie'})
# Gruppierung der Bäume nach Durchmesser und Erstellung von Layern
diameter_layers = {}
for tree in tree_positions:
x, y, radius, color = tree
diameter = radius * 2
layer_name = f"Bäume_{diameter}m"
if layer_name not in diameter_layers:
# Erstelle einen neuen Layer für diesen Durchmesser
doc.layers.new(name=layer_name)
diameter_layers[layer_name] = color # Speichere die Farbe für den Layer
# Füge den Baum auf dem entsprechenden Layer hinzu
circle = msp.add_circle(center=(x, y), radius=radius, dxfattribs={'layer': layer_name})
# Setze die Farbe des Kreises entsprechend der RGB-Werte
r, g, b = [int(c * 255) for c in color]
circle.dxf.true_color = rgb2int((r, g, b))
# Speichern der DXF-Datei
doc.saveas(file_path)
# Funktion zum Schreiben einer SVG-Datei mit Matplotlib
def write_svg_matplotlib(fig, file_path):
fig.savefig(file_path, format='svg', bbox_inches='tight')
# Funktion zum Schreiben eines Shapefiles
def write_shapefile(file_path, polylines, tree_positions, crs):
"""
Schreibt die Bäume in ein Shapefile mit dem angegebenen Koordinatensystem.
Die Bäume werden als Polygone (approximierte Kreise) gespeichert.
:param file_path: Pfad zum Shapefile (ohne Erweiterung)
:param polylines: Liste von Polylinien
:param tree_positions: Liste der Baumpositionen (x, y, radius, color)
:param crs: Koordinatensystem (CRS) für das Shapefile
"""
if crs is None:
messagebox.showerror("Fehler", "Kein Koordinatensystem für das Shapefile angegeben.")
return
# Schreiben der Bäume als Polygone (approximierte Kreise)
schema_trees = {
'geometry': 'Polygon',
'properties': {
'id': 'int',
'radius': 'float',
'diameter': 'float',
'color': 'str',
},
}
# Generiere einen eindeutigen Basisnamen für die Bäume
tree_base_name = get_unique_filename(file_path + "_baeume", "shp")
with fiona.open(tree_base_name, 'w', driver='ESRI Shapefile', crs=crs.to_wkt(), schema=schema_trees) as shp:
for idx, tree in enumerate(tree_positions):
x, y, radius, color = tree
# Erzeuge einen Kreis (Polygon) um den Punkt
point = Point(x, y)
circle_polygon = point.buffer(radius, resolution=circle_resolution)
shp.write({
'geometry': mapping(circle_polygon),
'properties': {
'id': idx,
'radius': radius,
'diameter': radius * 2,
'color': '#%02x%02x%02x' % (
int(color[0]*255), int(color[1]*255), int(color[2]*255)
),
},
})
# Funktion zur Generierung eines eindeutigen Dateinamens
def get_unique_filename(base_name, extension):
counter = 1
unique_name = f"{base_name}.{extension}"
while os.path.exists(unique_name):
unique_name = f"{base_name}_version_{counter}.{extension}"
counter += 1
return unique_name
# Funktion zur Auswahl der Ausgabeformate
def select_output_formats():
formats = []
def confirm():
if var_shp.get():
formats.append('shp')
if var_svg.get():
formats.append('svg')
if var_dxf.get():
formats.append('dxf')
format_window.destroy()
format_window = tk.Toplevel()
format_window.title("Ausgabeformate auswählen")
# Fenstergröße automatisch anpassen
format_window.update_idletasks()
width = format_window.winfo_reqwidth()
height = format_window.winfo_reqheight()
format_window.geometry(f"{width}x{height}")
var_shp = tk.IntVar()
var_svg = tk.IntVar()
var_dxf = tk.IntVar()
tk.Label(format_window, text="Bitte die gewünschten Ausgabeformate auswählen:").pack(anchor='w')
tk.Checkbutton(format_window, text="Shapefile (.shp)", variable=var_shp).pack(anchor='w')
tk.Checkbutton(format_window, text="SVG Datei (.svg)", variable=var_svg).pack(anchor='w')
tk.Checkbutton(format_window, text="DXF Datei (.dxf)", variable=var_dxf).pack(anchor='w')
tk.Button(format_window, text="OK", command=confirm).pack()
format_window.wait_window()
return formats
# Funktion zur Umwandlung eines Hex-Farbcodes in ein RGB-Tupel
def hex_to_rgb(hex_string):
hex_string = hex_string.lstrip('#')
return tuple(int(hex_string[i:i+2], 16) / 255 for i in (0, 2, 4))
# Funktion zum Verarbeiten von DXF-Dateien
def process_dxf(file_path):
try:
doc = ezdxf.readfile(file_path)
except IOError:
print(f"Die Datei konnte nicht gelesen werden: {file_path}")
return []
except ezdxf.DXFStructureError:
print(f"Die DXF-Datei ist ungültig oder beschädigt: {file_path}")
return []
msp = doc.modelspace()
polylines = []
for entity in msp:
if entity.dxftype() == 'LWPOLYLINE':
polyline_points = [tuple((point[0], point[1])) for point in entity]
polylines.append(polyline_points)
elif entity.dxftype() == 'POLYLINE':
polyline_points = []
for vertex in entity.vertices:
x = vertex.dxf.location[0]
y = vertex.dxf.location[1]
polyline_points.append((x, y))
polylines.append(polyline_points)
elif entity.dxftype() == 'LINE':
start_point = entity.dxf.start
end_point = entity.dxf.end
polyline_points = [start_point[:2], end_point[:2]]
polylines.append(polyline_points)
# Weitere Entitätstypen können hier hinzugefügt werden
return polylines
# Funktion zum Verarbeiten von SVG-Dateien
def process_svg(file_path):
from svgpathtools import svg2paths2
try:
paths, attributes, svg_attributes = svg2paths2(file_path)
except Exception as e:
print(f"Fehler beim Lesen der SVG-Datei: {e}")
return []
polylines = []
for path in paths:
polyline = []
for segment in path:
points = [segment.start, segment.end]
for point in points:
polyline.append((point.real, point.imag))
if polyline:
polylines.append(polyline)
return polylines
# Funktion zum Verarbeiten von Shapefiles
def process_shapefile(file_path):
"""
Verarbeitet das Shapefile und extrahiert Polygone oder Linien als Listen von Punkten.
:param file_path: Pfad zum Shapefile
:return: Liste von Polylinien (jeweils eine Liste von (x, y) Punkten)
"""
polylines = []
with fiona.open(file_path, 'r') as shp:
for feature in shp:
geom = shape(feature['geometry'])
if geom.is_empty:
continue
if isinstance(geom, (Polygon, MultiPolygon)):
# Extrahiere die äußeren Ringe
if isinstance(geom, Polygon):
polygons = [geom]
else:
polygons = list(geom.geoms)
for polygon in polygons:
exterior_coords = list(polygon.exterior.coords)
polylines.append(exterior_coords)
elif isinstance(geom, (LineString, MultiLineString)):
if isinstance(geom, LineString):
lines = [geom]
else:
lines = list(geom.geoms)
for line in lines:
coords = list(line.coords)
polylines.append(coords)
else:
# Andere Geometrietypen können hier behandelt werden
continue
return polylines
# Funktion zur interaktiven Eingabe aller Parameter in einem einzigen Fenster
def get_user_inputs():
user_inputs = {}
input_window = tk.Toplevel()
input_window.title("Eingabeparameter")
# Fenstergröße automatisch anpassen
input_window.update_idletasks()
width = input_window.winfo_reqwidth()
height = input_window.winfo_reqheight()
input_window.geometry(f"{width}x{height}")
# Funktion zum Schließen des Fensters
def submit():
try:
user_inputs['circle_resolution'] = int(entry_circle_resolution.get())
user_inputs['num_diameters'] = int(entry_num_diameters.get())
if user_inputs['num_diameters'] <= 0:
raise ValueError
# Baumdurchmesser, Proportionen und Farben sammeln
diameters = []
proportions = []
colors = []
for i in range(user_inputs['num_diameters']):
diameter = float(entries_diameters[i].get())
proportion = float(entries_proportions[i].get())
color_hex = entries_colors[i].get()
color_tuple = hex_to_rgb(color_hex)
if diameter <= 0 or not (0 <= proportion <= 100):
raise ValueError
diameters.append(diameter)
proportions.append(proportion)
colors.append(color_tuple)
user_inputs['diameters'] = diameters
user_inputs['proportions'] = proportions
user_inputs['colors'] = colors
# Clusteroption
user_inputs['use_clustering'] = var_use_clustering.get()
if user_inputs['use_clustering']:
user_inputs['cluster_percentage'] = float(entry_cluster_percentage.get())
if not (0 <= user_inputs['cluster_percentage'] <= 100):
raise ValueError
else:
user_inputs['cluster_percentage'] = 0
# Auswahl der Ausgabeformate
user_inputs['output_formats'] = select_output_formats()
if not user_inputs['output_formats']:
messagebox.showerror("Fehler", "Keine Ausgabeformate ausgewählt.")
return
# Auswahl der Varianten (Cluster, Zufällig, Beide)
variant = variant_var.get()
user_inputs['variant'] = variant
input_window.destroy()
except ValueError:
messagebox.showerror("Fehler", "Bitte gültige Eingaben machen.")
# Kreisauflösung
tk.Label(input_window, text="Kreisauflösung (z.B. 36):").grid(row=0, column=0, sticky='w')
entry_circle_resolution = tk.Entry(input_window)
entry_circle_resolution.insert(0, "36")
entry_circle_resolution.grid(row=0, column=1)
# Anzahl der Baumdurchmesser
tk.Label(input_window, text="Anzahl der unterschiedlichen Kronendurchmesser:").grid(row=1, column=0, sticky='w')
entry_num_diameters = tk.Entry(input_window)
entry_num_diameters.insert(0, "1")
entry_num_diameters.grid(row=1, column=1)
# Platz für dynamische Eingabefelder
frame_diameters = tk.Frame(input_window)
frame_diameters.grid(row=2, column=0, columnspan=2)
entries_diameters = []
entries_proportions = []
entries_colors = []
def update_diameter_entries(*args):
# Lösche vorhandene Einträge
for widget in frame_diameters.winfo_children():
widget.destroy()
entries_diameters.clear()
entries_proportions.clear()
entries_colors.clear()
try:
num_diameters = int(entry_num_diameters.get())
except ValueError:
return
for i in range(num_diameters):
tk.Label(frame_diameters, text=f"Kronendurchmesser {i+1} (in Metern):").grid(row=i*3, column=0, sticky='w')
entry_diameter = tk.Entry(frame_diameters)
entry_diameter.grid(row=i*3, column=1)
entries_diameters.append(entry_diameter)
tk.Label(frame_diameters, text=f"Prozentualer Anteil für Durchmesser {i+1} (%):").grid(row=i*3+1, column=0, sticky='w')
entry_proportion = tk.Entry(frame_diameters)
entry_proportion.grid(row=i*3+1, column=1)
entries_proportions.append(entry_proportion)
tk.Label(frame_diameters, text=f"Farbe für Durchmesser {i+1} (Hex-Code):").grid(row=i*3+2, column=0, sticky='w')
entry_color = tk.Entry(frame_diameters)
entry_color.insert(0, "#00ff00")
entry_color.grid(row=i*3+2, column=1)
entries_colors.append(entry_color)
def choose_color(index=i):
color_code = colorchooser.askcolor(title=f"Farbe für Durchmesser {index+1} auswählen")
if color_code[1]:
entries_colors[index].delete(0, tk.END)
entries_colors[index].insert(0, color_code[1])
btn_color = tk.Button(frame_diameters, text="Farbe auswählen", command=lambda idx=i: choose_color(idx))
btn_color.grid(row=i*3+2, column=2)
# Fenstergröße anpassen
input_window.update_idletasks()
width = input_window.winfo_reqwidth()
height = input_window.winfo_reqheight()
input_window.geometry(f"{width}x{height}")
entry_num_diameters.bind('<KeyRelease>', update_diameter_entries)
update_diameter_entries()
# Clusteroption
var_use_clustering = tk.BooleanVar()
tk.Checkbutton(input_window, text="Bäume in Clustern anordnen", variable=var_use_clustering).grid(row=3, column=0, sticky='w')
tk.Label(input_window, text="Cluster-Prozentsatz (0-100%):").grid(row=4, column=0, sticky='w')
entry_cluster_percentage = tk.Entry(input_window)
entry_cluster_percentage.insert(0, "50")
entry_cluster_percentage.grid(row=4, column=1)
# Auswahl der Varianten
tk.Label(input_window, text="Varianten für die Baumverteilung:").grid(row=5, column=0, sticky='w')
variant_var = tk.StringVar(value="both")
tk.Radiobutton(input_window, text="Nur zufällig", variable=variant_var, value="random").grid(row=6, column=0, sticky='w')
tk.Radiobutton(input_window, text="Nur Cluster", variable=variant_var, value="cluster").grid(row=7, column=0, sticky='w')
tk.Radiobutton(input_window, text="Beide Varianten", variable=variant_var, value="both").grid(row=8, column=0, sticky='w')
# Bestätigungsbutton
tk.Button(input_window, text="Weiter", command=submit).grid(row=9, column=0, columnspan=3)
input_window.wait_window()
return user_inputs
# Hauptfunktion zum Plotten der Bäume innerhalb der Form
def plot_trees_within_shape():
global circle_resolution # Damit die Variable in write_shapefile verwendet werden kann
# Haupt-Tkinter-Fenster erstellen
root = tk.Tk()
root.title("Baumraster-Erstellung")
# Benutzerinputs sammeln
user_inputs = get_user_inputs()
if not user_inputs:
root.destroy()
return
circle_resolution = user_inputs['circle_resolution']
num_diameters = user_inputs['num_diameters']
diameters = user_inputs['diameters']
proportions = user_inputs['proportions']
colors = user_inputs['colors']
use_clustering = user_inputs['use_clustering']
cluster_percentage = user_inputs['cluster_percentage']
output_formats = user_inputs['output_formats']
variant_selection = user_inputs['variant']
# Auswahl der Eingabedateien (Batch-Verarbeitung)
input_files = filedialog.askopenfilenames(
title="Bitte die Eingabedateien auswählen",
filetypes=[
("Unterstützte Dateien", "*.shp *.dxf *.svg"),
("Shapefile", "*.shp"),
("DXF Dateien", "*.dxf"),
("SVG Dateien", "*.svg")
]
)
if not input_files:
messagebox.showerror("Fehler", "Keine Eingabedateien ausgewählt.")
root.destroy()
return
# Auswahl des Ausgabeverzeichnisses
output_directory = filedialog.askdirectory(
title="Bitte das Ausgabeverzeichnis auswählen"
)
if not output_directory:
messagebox.showerror("Fehler", "Kein Ausgabeverzeichnis ausgewählt.")
root.destroy()
return
# Verarbeitung jeder Eingabedatei
for input_file in input_files:
filename = os.path.basename(input_file)
name_without_ext = os.path.splitext(filename)[0]
extension = input_file.split('.')[-1].lower()
polylines = read_input_file(input_file)
if len(polylines) == 0:
messagebox.showerror("Fehler", f"Keine gültigen Formen gefunden oder das Dateiformat wird nicht unterstützt: {filename}")
continue
# Wenn es sich um eine SVG-Datei handelt, nach maximaler Länge fragen und skalieren
if extension == 'svg':
# Kombinieren aller Punkte, um die Begrenzungsbox zu finden
all_points = np.concatenate(polylines)
min_x, min_y = np.min(all_points, axis=0)
max_x, max_y = np.max(all_points, axis=0)
shape_width = max_x - min_x
shape_height = max_y - min_y
shape_max_length = max(shape_width, shape_height)
# Eingabe der maximalen Länge für die Skalierung
try:
max_length_input = simpledialog.askstring("Skalierung", f"Bitte die maximale Länge für die Skalierung von {filename} (in Metern) angeben:")
if max_length_input is None:
messagebox.showerror("Fehler", "Keine Eingabe für die maximale Länge.")
continue
max_length = float(max_length_input)
except ValueError:
messagebox.showerror("Fehler", "Ungültige Eingabe für die maximale Länge.")
continue
if max_length <= 0:
messagebox.showerror("Fehler", "Die maximale Länge muss positiv sein.")
continue
scale = max_length / shape_max_length
# Skalieren aller Polylinien
scaled_polylines = []
for polyline in polylines:
scaled_polyline = [(scale * (x - min_x), scale * (y - min_y)) for x, y in polyline]
scaled_polylines.append(scaled_polyline)
# CRS ist nicht relevant für SVG
crs = None
else:
# Für andere Formate, CRS verarbeiten
if extension == 'shp':
with fiona.open(input_file, 'r') as shp:
crs = CRS(shp.crs)
else:
# CRS vom Benutzer abfragen
crs_input = simpledialog.askstring(
"Koordinatensystem",
f"Bitte gib den EPSG-Code des Koordinatensystems für {filename} ein (z.B. 32632 für UTM Zone 32N):"
)
if crs_input is None:
messagebox.showerror("Fehler", "Kein Koordinatensystem eingegeben.")
continue
try:
crs_epsg = int(crs_input)
crs = CRS.from_epsg(crs_epsg)
except ValueError:
messagebox.showerror("Fehler", "Ungültiger EPSG-Code.")
continue
# Verwende die Originalpolylinien ohne Skalierung
scaled_polylines = polylines
# Kombinieren aller Punkte, um die Begrenzungsbox zu finden
all_points = np.concatenate(polylines)
min_x, min_y = np.min(all_points, axis=0)
max_x, max_y = np.max(all_points, axis=0)
# Aktualisiere die Begrenzungsbox
scaled_all_points = np.concatenate(scaled_polylines)
min_x_scaled, min_y_scaled = np.min(scaled_all_points, axis=0)
max_x_scaled, max_y_scaled = np.max(scaled_all_points, axis=0)
# Erstellen von Path-Objekten für jede Polyline
paths = []
for polyline in scaled_polylines:
if len(polyline) < 2:
continue # Nicht genügend Punkte für einen Pfad
verts = list(polyline)
if verts[0] != verts[-1]:
verts.append(verts[0]) # Polygon schließen
codes = [Path.MOVETO] + [Path.LINETO] * (len(verts) - 2) + [Path.CLOSEPOLY]
path = Path(verts, codes)
paths.append(path)
if not paths:
messagebox.showerror("Fehler", f"Keine gültigen geschlossenen Polylinien gefunden in {filename}.")
continue
# Erstellen eines zusammengesetzten Path-Objekts
shape_path = Path.make_compound_path(*paths)
# Neues Fenster für die interaktive Vorschau erstellen
preview_window = tk.Toplevel(root)
preview_window.title(f"Vorschau und Anpassung - {filename}")
# Vorschaufenster bildschirmfüllend anzeigen
preview_window.attributes('-fullscreen', True)
# Frame für Plot und Steuerungselemente
frame = tk.Frame(preview_window)
frame.pack(fill=tk.BOTH, expand=True)
# Matplotlib-Figur erstellen
fig = Figure(figsize=(8, 6))
ax = fig.add_subplot(111)
canvas = FigureCanvasTkAgg(fig, master=frame)
canvas.draw()
canvas.get_tk_widget().pack(side=tk.LEFT, fill=tk.BOTH, expand=1)
# Steuerungsframe für Slider und Button
control_frame = tk.Frame(frame)
control_frame.pack(side=tk.RIGHT, fill=tk.Y)
# Füllungsintensität Slider
fill_intensity = tk.DoubleVar(value=1.0)
def update_fill_intensity(val):
nonlocal tree_positions
tree_positions, _ = generate_tree_positions(current_variant)
update_trees()
slider = tk.Scale(control_frame, from_=0.1, to=2.0, resolution=0.1, orient=tk.VERTICAL, variable=fill_intensity, label="Füllungsintensität", command=update_fill_intensity)
slider.pack(fill=tk.Y, padx=10, pady=10)
# Button zum Schließen und Fortfahren
def on_close():
preview_window.destroy()
close_button = tk.Button(control_frame, text="Bearbeitung abschließen", command=on_close)
close_button.pack(pady=10)
# Funktion zur Erstellung der Baumpositionen
def generate_tree_positions(variant):
tree_positions = []
diameter_tree_counts = {}
max_trees_per_diameter = 100000 # Erhöht, um mehr Bäume zuzulassen
intensity = fill_intensity.get()
tree_data = sorted(zip(diameters, proportions, colors), key=lambda x: -x[0])
# Berechnung der Gesamtfläche der Form
total_area = compute_shape_area(scaled_polylines)
# Berechnung der Gesamtanzahl der Bäume pro Durchmesser
total_trees = {}
for diameter, proportion, _ in tree_data:
radius = diameter / 2
total_trees[diameter] = int((proportion / 100) * total_area / (np.pi * (radius ** 2)) * intensity)
if num_diameters == 1:
diameter_tree_counts = {}
diameter, proportion, color = tree_data[0]
radius = diameter / 2
# Mindestabstand berechnen
if diameter >= 3:
min_distance = diameter * 1.3 # Zentrum-zu-Zentrum-Abstand
else:
min_distance = diameter # Kein zusätzlicher Abstand
# Rasterpunkte erstellen
x_min = min_x_scaled + radius
x_max = max_x_scaled - radius
y_min = min_y_scaled + radius
y_max = max_y_scaled - radius
x_coords = np.arange(x_min, x_max + min_distance, min_distance)
y_coords = np.arange(y_min, y_max + min_distance, min_distance)
# Maximal zulässige Anzahl von Bäumen berechnen
max_trees = min(total_trees[diameter], max_trees_per_diameter)
for x in x_coords:
for y in y_coords:
if shape_path.contains_point((x, y)):
tree_positions.append((x, y, radius, color))
if len(tree_positions) >= max_trees:
break
if len(tree_positions) >= max_trees:
break
diameter_tree_counts[diameter] = len(tree_positions)
else:
diameter_tree_counts = {diameter: 0 for diameter in diameters}
# Platzierung der Bäume für jeden Durchmesser
for diameter, proportion, color in tree_data:
radius = diameter / 2
trees_to_place = min(total_trees[diameter], max_trees_per_diameter)
attempts = 0
while diameter_tree_counts[diameter] < trees_to_place and attempts < 100000:
x_pos = random.uniform(min_x_scaled + radius, max_x_scaled - radius)
y_pos = random.uniform(min_y_scaled + radius, max_y_scaled - radius)
if not shape_path.contains_point((x_pos, y_pos)):
attempts += 1
continue
overlaps = False
for (existing_x, existing_y, existing_radius, _) in tree_positions:
if check_overlap(x_pos, y_pos, radius, existing_x, existing_y, existing_radius, min_distance=radius * 0.3):
overlaps = True
break
if not overlaps:
tree_positions.append((x_pos, y_pos, radius, color))
diameter_tree_counts[diameter] += 1
attempts += 1
return tree_positions, diameter_tree_counts
# Funktion zur Aktualisierung der Bäume
def update_trees():
nonlocal tree_positions, tree_circles, total_area
# Berechne neue Gesamtfläche
total_area = compute_shape_area(scaled_polylines)
# Bäume neu generieren
tree_positions, diameter_tree_counts = generate_tree_positions(current_variant)
# Lösche vorhandene Baumkronen
for circle in tree_circles:
circle.remove()
tree_circles.clear()
# Begrenze die Anzahl der Bäume in der Vorschau für bessere Leistung
max_preview_trees = 1000
preview_tree_positions = tree_positions[:max_preview_trees]
# Neue Bäume plotten
for x_pos, y_pos, radius, color in preview_tree_positions:
circle = plt.Circle((x_pos, y_pos), radius, edgecolor=color, facecolor='none', linewidth=0.8)
ax.add_artist(circle)
tree_circles.append(circle)
canvas.draw()
# Baumdaten sortieren
tree_data = sorted(zip(diameters, proportions, colors), key=lambda x: -x[0])
# Berechnung der Gesamtfläche der Form
total_area = compute_shape_area(scaled_polylines)
print(f"Gesamtfläche der Form in {filename}: {total_area:.2f} Quadratmeter")
# Initiale Bäume generieren
current_variant = variant_selection
tree_positions, diameter_tree_counts = generate_tree_positions(current_variant)
# Plotten der initialen Form und Bäume
ax.clear()
ax.set_title(f"Interaktive Vorschau - {filename}")
ax.set_xlabel('Entfernung Ost (m)')
ax.set_ylabel('Entfernung Nord (m)')
# Plotten der Form
for path in paths:
patch = plt.Polygon(path.vertices, closed=True, fill=False, edgecolor='black')
ax.add_patch(patch)
# Plotten der initialen Bäume
tree_circles = []
# Begrenze die Anzahl der Bäume in der Vorschau
max_preview_trees = 1000
preview_tree_positions = tree_positions[:max_preview_trees]
for x_pos, y_pos, radius, color in preview_tree_positions:
circle = plt.Circle((x_pos, y_pos), radius, edgecolor=color, facecolor='none', linewidth=0.8)
ax.add_artist(circle)
tree_circles.append(circle)
# Achsenlimits setzen
ax.set_xlim(min_x_scaled - 10, max_x_scaled + 10)
ax.set_ylim(min_y_scaled - 10, max_y_scaled + 10)
ax.set_aspect('equal', adjustable='box')
canvas.draw()
# Warten, bis das Vorschaufenster geschlossen wird
root.wait_window(preview_window)
# Nach der Bearbeitung: Bäume generieren und Dateien exportieren
variants = []
if variant_selection == 'both':
variants_to_generate = [('random', 'random'), ('cluster', 'cluster')]
elif variant_selection == 'random':
variants_to_generate = [('random', 'random')]
elif variant_selection == 'cluster':
variants_to_generate = [('cluster', 'cluster')]
else:
variants_to_generate = [('selected', current_variant)]
# Verwende die aktuelle Füllungsintensität
final_fill_intensity = fill_intensity.get()
for variant_name, use_clustering_variant in variants_to_generate:
# Generiere die Bäume für die Ausgabe mit voller Anzahl
tree_positions, diameter_tree_counts = generate_tree_positions(use_clustering_variant)
tree_count = len(tree_positions)
# Dateinamen für die Ausgabe erstellen
output_filename_base = os.path.join(output_directory, f"{name_without_ext}_{variant_name}_output")
# Für jedes ausgewählte Ausgabeformat, die Datei schreiben
for export_extension in output_formats:
# Verwende die Funktion, um einen eindeutigen Dateinamen zu erhalten
current_export_file = get_unique_filename(output_filename_base, export_extension)
# Schreiben der Ausgabedatei basierend auf dem gewählten Format
write_output_file(current_export_file, scaled_polylines, tree_positions, crs, fig=fig if export_extension == 'svg' else None)
# Erfolgreiche Speicherung
print(f"Das Schema wurde erfolgreich unter {current_export_file} gespeichert.\nAnzahl der gezeichneten Bäume in {filename}: {tree_count}")
messagebox.showinfo("Fertig", "Die Verarbeitung aller Dateien ist abgeschlossen.")
root.destroy()
if __name__ == "__main__":
plot_trees_within_shape()