Python-Projekt zur automatischen Baumrastererstellung

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. Batch-Verarbeitung:
    • Der Code ermöglicht die Auswahl mehrerer Eingabedateien für die Verarbeitung, wodurch eine Batch-Verarbeitung möglich ist.
  7. 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.
  8. 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:

  1. 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).
  2. 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.
  3. 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.
  4. 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).
  5. 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.
  6. 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()