GIS-basierte Analyse von Wanderwegequalitäten anhand verfügbarer Geodaten - Vergleich der Qualitätsmerkmale über die regionalen Naturpärke der Schweiz

Individuelle Projektarbeit in der Forschungsgruppe (PWRG2) HS25 ZHAW

Autor:in

Dominik Erni

Veröffentlichungsdatum

19. Januar 2026

Einleitung

Zielsetzung

Die Zielsetzung der Arbeit gliedert sich in zwei Bereiche: Einen methodischen Schwerpunkt sowie einen inhaltlichen Schwerpunkt. Methodisches Ziel ist die Aneignung und Vertiefung von Fähigkeiten im GIS-Skripting. Im Fokus stehen:

  • Der Einsatz von ArcPy zur Datenvorverarbeitung und räumlichen Analyse,

  • die Ergänzung von R zur Informationsvisualisierung der Ergebnisse,

  • der Aufbau eines reproduzierbaren Workflows, der wiederkehrende Analyseprozesse automatisiert.

Inhaltliches Ziel der Arbeit ist es, quantitative Unterschiede von Wanderwegen anhand ausgewählter Qualitätsparameter über die verschiedenen regionalen Naturpärke der Schweiz aufzuzeigen.

Inhaltliche Einordnung

Die Arbeit beschreibt einen methodischen Ansatz zur quantitativen Beschreibung von Wanderwegeigenschaften auf Basis frei verfügbarer Geodaten. Der Fokus liegt nicht auf einer Beurteilung der Wanderwegequalität, sondern auf der Darstellung deren Unterschiede zwischen den Untersuchungsperimetern.

Der Analyseprozess ist so konzipiert, dass er auf unterschiedliche Untersuchungsperimeter in der Schweiz übertragbar ist und als Grundlage für weiterführende Analysen, vertiefende Fragestellungen oder ergänzende Bewertungsansätze dienen kann. Zudem zeigt die Arbeit exemplarisch, wie GIS-basierte Analysen zur Unterstützung raumbezogener Vergleiche im Kontext von Tourismus, Freizeit und Raumplanung eingesetzt werden können.

Methodik

Datengrundlage

Für die Analyse werden ausgewählte Geodaten verwendet, die einerseits methodisch vielfältige Anwendungen innerhalb der GIS-Analyse erlauben und andererseits schweizweit flächendeckend verfügbar sind.

Tabelle 1: Übersicht Datengrundlagen
Urheberin Datenbezeichnug Datenformat Geometrietyp Rolle in Analyse
BAFU Pärke von nationaler Bedeutung (regionale Naturpärke) Geodatabase Vektor Polygon Untersuchungsperimeter
swisstopo swissTLM3D Wanderwege Geopackage Vektor Linie Referenzgeometrie
swisstopo DHM25 ASCII Grid Raster Qualitätsparameter: Topografische Ausprägungen
swisstopo swissTLMRegio Landcover Geodatabase Vektor Polygon Qualitätsparameter: Landschaftsliche Vielfalt
BAFU Lärmbelastung durch Strassenverkehr am Tag TIFF Raster Qualitätsparameter: Lärmbelastung
BAFU Bundesinventar der Amphibienlaichgebiete von nationaler Bedeutung Geodatabase Vektor Polygon Qualitätsparameter: Naturwert
BAFU Bundesinventar der Auengebiete von nationaler Bedeutung Geodatabase Vektor Polygon Qualitätsparameter: Naturwert
BAFU Bundesinventar der Flachmoore von nationaler Bedeutung Geodatabase Vektor Polygon Qualitätsparameter: Naturwert
BAFU Bundesinventar der Hoch- Übergangsmoore von nationaler Bedeutung Geodatabase Vektor Polygon Qualitätsparameter: Naturwert
BAFU Bundesinventar der Trockenwiesen- und Weiden von nationaler Bedeutung Geodatabase Vektor Polygon Qualitätsparameter: Naturwert
BAV Haltestellen des öffentlichen Verkehrs Geodatabase Vektor Punkt Qualitätsparameter: Nähe zu ÖV-Haltestellen
Open Street Map Restaurants OSM-Features Vektor Punkt Qualitätsparameter: Nähe zu Restaurants
Open Street Map Aussichtspunkte und Berggipfel OSM-Features Vektor Punkt Qualitätsparameter: Nähe zu Aussichtspunkten
Open Street Map Grillplätze und Trinkwasserstellen OSM-Features Vektor Punkt Qualitätsparameter: Nähe zu Freizeitinfrastruktur

Untersuchungsperimeter

Untersucht und verglichen werden die 17 regionalen Naturpärke von nationaler Bedeutung, die eine Kategorie des Inventars der Pärke von nationaler Bedeutung abbilden.

Abbildung 1: Perimeterbezeichnung: a) Parc Jura vaudois b) Parc du Doubs c) Parc régional Chasseral d) Parc naturel régional de la Vallée du Trient e) Parc naturel régional Gruyère Pays-d’Enhaut f) Naturpark Gantrisch g) Naturpark Diemtigtal h) Naturpark Thal i) Naturpark Pfyn-Finges j) UNESCO Biophäre Entlebuch k) Jurapark Aargau l) Landschaftspark Binntal m) Regionaler Naturpark Schaffhausen n) Parco Val Calanca o) Naturpark Beverin p) Parc Ela q) Biosfera Val Müstair.
Datengrundlage: swisstopo, swissTLMRegio. Genordet, ohne Massstab.

Aufbau der Analyse

Die Analyse basiert auf dem Wanderwegenetz als übergeordneter Referenzgeometrie. Sämtliche Auswertungen werden für alle Untersuchungsperimeter mit einer einheitlichen Methodik durchgeführt. Dadurch sind die Resultate zwischen den Perimetern direkt vergleichbar.

Zur Definition einer konsistenten räumlichen Auflösung wird das Wanderwegenetz vorgängig segmentiert. Diese Segmentierung bildet die kleinste räumliche Analyseeinheit und ist Voraussetzung für eine aussagekräftige Verknüpfung von Punkt-, Linien- und Rasterdaten mit dem Wegenetz.

Die räumliche Analyse und Datenaufbereitung erfolgt in Python unter Verwendung von ArcPy. Dieser Teil umfasst die Vorverarbeitung der Geodaten, die räumlichen Berechnungen sowie die Aggregation der Ergebnisse auf Segmentebene. Die berechneten Kennwerte werden anschliessend als tabellarische Daten ohne Geometrie exportiert.

Die statistische Auswertung sowie die Informationsvisualisierung erfolgen in R unter Verwendung von RStudio (Quelle). Dabei werden die aus ArcPy exportierten Tabellen weiterverarbeitet und in Form von Grafiken aufbereitet.

Die Analysen wurden in R (v4.4.1) unter RStudio sowie in Python (ArcPy, ArcGIS Pro 3.4.2) in VS Code durchgeführt. Die vollständigen Skripte (automatisiertes Python/ArcPy Skript mit Schleifenfunktion über Perimeterliste sowie das R Skript für die Informationsvisualisierungen) liegen als Anhänge bei (vgl. Kapitel 6.2 und Kapitel 6.3).

Abbildung 2: Analyse- und Auswertungsschema

Die Projektorganisation folgt einer Ordnerhierarchie. Die Ordnerstruktur ist funktional aufgebaut: Roh- und aufbereitete Geodaten werden zentral abgelegt, Analyse- und Auswertungsskripte sind getrennt organisiert und Ergebnisse aus der ArcPy-Analyse sowie der R-Auswertung werden in separaten Ausgabeordnern gespeichert. Temporäre Daten und Zwischenergebnisse werden ausschliesslich im Scratch-Bereich verwaltet.

Abbildung 3: Ordnerstrukturbaum (vereinfacht)

Das vollständige Ordnerstrukturverzeichnis liegt als Anhang bei (vgl. Kapitel 6.1).

GIS-Analyse

Daten vorverarbeiten

Umwandlung Grundlagedaten in einheitliche und lesbare Formate

Die für die Analyse verwendeten Geodaten wurden vorab in einheitliche und lesbare Formate überführt. Diese Schritte stellen eine einmalige Datenvorverarbeitung dar.

Die TLM-Wanderwege lagen ursprünglich als Geopackage vor und wurden über das Geoverarbeitungstool copyFeatures in eine File-Geodatabase überführt.

Code
arcpy.env.workspace = r"C:\Users\domin\Documents\Projekt_PWRG2\data\TLM\SWISSTLM3D_WANDERWEGE.gpkg"
out_gdb = r"C:\Users\domin\Documents\Projekt_PWRG2\data\TLM\swissTLM3d_wanderwege.gdb"

# Falls GDB noch nicht existiert
if not arcpy.Exists(out_gdb):
    arcpy.management.CreateFileGDB(os.path.dirname(out_gdb), os.path.basename(out_gdb))

for fc in arcpy.ListFeatureClasses():
    out_name = arcpy.ValidateTableName(fc, out_gdb)
    out_fc = os.path.join(out_gdb, out_name)

    arcpy.CopyFeatures_management(fc, out_fc)

    print(f"{fc} kopiert als {out_name} in GDB: {out_gdb}")

Das digitale Höhenmodell DHM25 wurde aus dem ESRI-ASCII-Grid-Format in ein für ArcGIS lesbares Rasterformat überführt. Dazu erfolgte das Einlesen der ASCII-Datei in QGIS sowie die Definition des ursprünglichen Koordinatenreferenzsystems (LV03, EPSG:21781) gemäss Metadaten von swisstopo. Anschliessend wurde das Höhenmodell als GeoTIFF exportiert und in das Zielkoordinatensystem LV95 (EPSG:2056) transformiert.

Perimeter definieren

Die Untersuchungsperimeter werden als Liste von Polygon-Feature-Classes definiert. Jeder Perimeter ist eindeutig benannt und dient als räumlicher Bezugsrahmen für die Analyse. Die Verarbeitung erfolgt repetitiv über alle Perimeter, wobei für jeden Perimeter derselbe Analyseablauf durch die Ausführung einer Schleifen-Funktion (for-loop) angewendet wird. Das vollständige Python-Skript hierzu liegt als Anahng bei.

Code
# Perimeterliste für Schleife
perimeters = [
    ("a", r"C:\Users\domin\Documents\Projekt_PWRG2\data\Perimeter\Perimeter.gdb\a"),
    ("b", r"C:\Users\domin\Documents\Projekt_PWRG2\data\Perimeter\Perimeter.gdb\b"),
    ("c", r"C:\Users\domin\Documents\Projekt_PWRG2\data\Perimeter\Perimeter.gdb\c"),
    ("d", r"C:\Users\domin\Documents\Projekt_PWRG2\data\Perimeter\Perimeter.gdb\d"),
    ("e", r"C:\Users\domin\Documents\Projekt_PWRG2\data\Perimeter\Perimeter.gdb\e"),
    ("f", r"C:\Users\domin\Documents\Projekt_PWRG2\data\Perimeter\Perimeter.gdb\f"),
    ("g", r"C:\Users\domin\Documents\Projekt_PWRG2\data\Perimeter\Perimeter.gdb\g"),
    ("h", r"C:\Users\domin\Documents\Projekt_PWRG2\data\Perimeter\Perimeter.gdb\h"),
    ("i", r"C:\Users\domin\Documents\Projekt_PWRG2\data\Perimeter\Perimeter.gdb\i"),
    ("j", r"C:\Users\domin\Documents\Projekt_PWRG2\data\Perimeter\Perimeter.gdb\j"),
    ("k", r"C:\Users\domin\Documents\Projekt_PWRG2\data\Perimeter\Perimeter.gdb\k"),
    ("l", r"C:\Users\domin\Documents\Projekt_PWRG2\data\Perimeter\Perimeter.gdb\l"),
    ("m", r"C:\Users\domin\Documents\Projekt_PWRG2\data\Perimeter\Perimeter.gdb\m"),
    ("n", r"C:\Users\domin\Documents\Projekt_PWRG2\data\Perimeter\Perimeter.gdb\n"),
    ("o", r"C:\Users\domin\Documents\Projekt_PWRG2\data\Perimeter\Perimeter.gdb\o"),
    ("p", r"C:\Users\domin\Documents\Projekt_PWRG2\data\Perimeter\Perimeter.gdb\p"),
    ("q", r"C:\Users\domin\Documents\Projekt_PWRG2\data\Perimeter\Perimeter.gdb\q")
]

# Hauptfunktion Auszüge zur Dokumentation (für vollständiges Skript siehe Anhang)
def run_analysis(perimeter_name, perimeter_fc):
    # Perimeter abrufen # ---------------------------------------------
    OutDir = r"C:\Users\domin\Documents\Projekt_PWRG2\output\Perimeter"
    perimeter_shp = r"perimeter.shp"
    print("Ausgewählter Perimeter: {0}.".format(perimeter_name)) # klassische print-Variante Python 3 gem. Literatur Tateosian, 2015. Neu: f-string.
    # fcs in shape konventieren
    arcpy.FeatureClassToFeatureClass_conversion(perimeter_fc, OutDir, perimeter_shp) 
    # Ende der Teilfunktion

Bezug der OSM-Features

Eine Auswahl von Zielorten, die als Proxy für Qualitätsparameter von Wanderinfrastruktur dienen, werden direkt aus OpenStreetMap bezogen. Der Abruf sowie das Herunterladen erfolgt über das Python-Modul osmnx und wird für jeden Untersuchungsperimeter separat durchgeführt. Die Abfrage basiert auf definierten OSM-Tags und umfasst:

  • Restaurants,

  • Aussichtspunkte und topografische Gipfel

  • sowie Infrastrukturpunkte wie Grillplätze und Trinkwasserstellen.

Die resultierenden Punktdaten werden reprojiziert und in einer File-Geodatabase gespeichert. Die restlichen Grundlagedaten (vgl. Tabelle 1) werden aus Gründen der Analyseperformance und Reproduzierbarkeit lokal gespeichert und nicht dynamisch bezogen.

Code
    # OSM Abfrage POI # --------------------------------------------
    # Perimeter/Area für OSM lesen in geopandas (CH1903+)
    perimeter = r"C:\Users\domin\Documents\Projekt_PWRG2\output\Perimeter\perimeter.shp"

    area = gpd.read_file(perimeter)
    area = area.to_crs(epsg=2056)

    # Für OSM-Abfrage WGS84
    area_wgs84 = area.to_crs(epsg=4326)
    polygon = area_wgs84.geometry.iloc[0] 

    # OSM-Tags definieren 
    tags = {
        "amenity": [
            "restaurant",
            "drinking_water",
            "bbq"              
        ],
        "natural": [
            "peak"             
        ],
        "tourism": [
            "viewpoint"        
        ]
    }

    # OSM-Abfrage
    pois = ox.features_from_polygon(polygon, tags)

    # Reprojektion zurück nach CH1903+
    pois = pois.to_crs(epsg=2056)

    pois_points = pois[pois.geometry.type == "Point"]
    pois_points["poi_type"] = pois_points.apply(get_poi_type, 1) # Anwendung Funktion "get_poi_type"

    # File GDB erstellen
    out_folder = r"C:\Users\domin\Documents\Projekt_PWRG2\data\OSM"
    gdb_name = "osm_pois.gdb"
    out_fc_name = "pois_osm"
    gdb_path = os.path.join(out_folder, gdb_name)
    if not arcpy.Exists(gdb_path):
        arcpy.CreateFileGDB_management(out_folder, gdb_name)

    # In GDB schreiben
    pois_points.to_file(
        gdb_path,
        driver="OpenFileGDB",
        layer=out_fc_name
    )
    # Ende der Teilfunktion

Begrenzung der Grundlagedaten auf Ausmass der Perimeter

Sämtliche Vektor- und Rasterdatensätze werden auf den jeweiligen Untersuchungsperimeter zugeschnitten. Die Umsetzung erfolgt automatisiert mittels Traversierung der Ordnerstruktur mit os.walk. Dabei werden alle in der Datenstruktur enthaltenen File-Geodatabases identifiziert und der jeweilige relative Pfad zum Datenverzeichnis bestimmt. Auf dieser Basis werden im Ausgabeverzeichnis automatisch entsprechende Unterordner erzeugt.

Die Begrenzung der Vektordaten erfolgt mittels Clip-Operationen. Rasterdaten werden geometriebasiert zugeschnitten. Dieser Schritt stellt sicher, dass alle nachfolgenden Analysen ausschliesslich innerhalb des jeweiligen Untersuchungsperimeters durchgeführt werden sowie der Berechnungsaufwand auf das Notwendigste beschränkt wird.

Code
    # Clip Grundlagedaten mit Perimeter # --------------------------------------------
    # Teil Vektordaten (für Feature Classes in allen Unterordner)
    # Ausnahme Ordner Perimeter und Order OSM

    rootDir = r"C:\Users\domin\Documents\Projekt_PWRG2\data"
    outDir  = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch"
    exclude_dirs = ["perimeter", "osm"]

    # Scratch-GDB erstellen
    scratch_gdb = os.path.join(outDir, "scratch.gdb")

    # scratch.gdb erstellen (falls nicht vorhanden)
    if not arcpy.Exists(scratch_gdb):
        arcpy.management.CreateFileGDB(scratch_gdb)

    for root, dirs, files in os.walk(rootDir):
        dirs[:] = [d for d in dirs if d.lower() not in exclude_dirs]
        
        for d in dirs:
            if d.lower().endswith(".gdb"):
                gdb_path = os.path.join(root, d)
                arcpy.env.workspace = gdb_path

                fcs = arcpy.ListFeatureClasses()
                for fc in fcs:
                    outfile = os.path.join(scratch_gdb, f"{fc}_clip")
                    
                    arcpy.analysis.Clip(in_features=fc,
                        clip_features=perimeter_fc,
                        out_feature_class=outfile)

    # Grundlagedaten mit Perimeter clippen
    # Teil Rasterdaten

    rootDir = r"C:\Users\domin\Documents\Projekt_PWRG2\data"
    outDir  = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch"
    exclude_dirs = {"perimeter", "osm"}

    for root, dirs, files in os.walk(rootDir):
        dirs[:] = [d for d in dirs if d.lower() not in exclude_dirs]

        for d in dirs:
            gdb_path = os.path.join(root, d)
            arcpy.env.workspace = gdb_path
            
            rasters = arcpy.ListRasters("*", "TIF")
            for ras in rasters:
                base = os.path.splitext(ras)[0] # Unterschied zu Vektordateien: Hier müssen die Endungen .tif etc entfernt werden.
                out_name = arcpy.ValidateTableName(f"{base}_clip", scratch_gdb) # Namens-Validierung hier auch ergänzt, weil Raster-Daten.
                outfile = os.path.join(scratch_gdb, out_name)

                arcpy.management.Clip(
                    in_raster=ras,
                    rectangle="#",             
                    out_raster=outfile,
                    in_template_dataset=perimeter_fc,
                    nodata_value="-9999",      
                    clipping_geometry="ClippingGeometry"
                )
    # Ende der Teilfunktion

Nachbearbeitung nach Clip

Nach dem Zuschnitt werden inhaltliche Nachbearbeitungen durchgeführt. Insbesondere werden die durch die Anwendung von clip verloren gegangene oder reduzierte Attributinformationen, wie Bezeichnungen der Landbedeckungsklassen, erneut ergänzt.

Des Weiteren erfolgt eine thematische Zusammenfassung der OSM-basierten Punktdaten in drei definierte Kategorien:

Tabelle 2: OSM-Kategorien
OSM-Tags Kategorie
Restaurants POI_restaurant
Aussichtspunkte / Berggipfel POI_view
Grillplätze / Trinkwasserstellen POI_infrastructure

Schliesslich werden die fünf Datensätze zu den Bundesinventaren (vgl. Tabelle 1) via Dissolve zu einem Datensatz «Biotopinventare» zusammengefasst.

Code
    # Nachbearbeitung nach Clip # ---------------------------------------------
    # Landcover Bezeichnungen hinzufügen -----
    clipped_fc = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch\scratch.gdb\TLMRegio_LandCover_clip"
    new_field = "LANDCOVER_LABEL"

    # Prüfen, ob Feld bereits existiert
    field_names = [f.name for f in arcpy.ListFields(clipped_fc)]
    if new_field not in field_names:
        arcpy.management.AddField(clipped_fc, new_field, "TEXT", field_length=50)

    mapping = {
        0: "Wald",
        1: "Fels",
        2: "Geroell",
        3: "Gletscher",
        4: "See",
        5: "Siedl",
        6: "Stadtzentr",
        7: "Stausee",
        8: "Obstanlage",
        9: "Reben",
        10: "Sumpf",
        11: "Kulturland"
    }

    with arcpy.da.UpdateCursor(clipped_fc, ["OBJVAL", "LANDCOVER_LABEL"]) as cursor:
        for val, label in cursor:
            cursor.updateRow((val, mapping.get(val, "Unbekannt")))

    # POIs kategorisieren für Analyse -----
    in_pois = r"C:\Users\domin\Documents\Projekt_PWRG2\data\OSM\osm_pois.gdb\pois_osm"
    out_gdb = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch\scratch.gdb"

    # Restaurants
    arcpy.conversion.FeatureClassToFeatureClass(
        in_features=in_pois,
        out_path=out_gdb,
        out_name="poi_restaurant",
        where_clause="poi_type = 'restaurant'"
    )

    # Gipfel und Aussichtspunkte
    arcpy.conversion.FeatureClassToFeatureClass(
        in_features=in_pois,
        out_path=out_gdb,
        out_name="poi_view",
        where_clause="poi_type IN ('peak', 'viewpoint')"
    )

    # Grillplätze und Trinkwasserstellen
    arcpy.conversion.FeatureClassToFeatureClass(
        in_features=in_pois,
        out_path=out_gdb,
        out_name="poi_infrastructure",
        where_clause="poi_type IN ('bbq', 'drinking_water')"
    )

    # Biotopinventare zusammenfassen -----
    scratch_gdb = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch\scratch.gdb"
    biotope_fcs = [
        os.path.join(scratch_gdb, "hochmoor_clip"),
        os.path.join(scratch_gdb, "flachmoor_clip"),
        os.path.join(scratch_gdb, "tww_clip"),
        os.path.join(scratch_gdb, "Auengebiete_clip"),
        os.path.join(scratch_gdb, "amphibLaichgebiet_clip"),
    ]

    out_biotope = os.path.join(scratch_gdb, "Biotopinventare")

    # Merge
    arcpy.management.Merge(biotope_fcs, out_biotope)

    out_biotope_dissolve = os.path.join(scratch_gdb, "Biotopinventare_Dissolved")

    # Dissolve
    arcpy.management.Dissolve(
        in_features=out_biotope,
        out_feature_class=out_biotope_dissolve,
        dissolve_field=None 
    )

    if arcpy.Exists(out_biotope):
        arcpy.management.Delete(out_biotope)
    # Ende der Teilfunktion

Segmentierung Wanderwegenetz

Das Wanderwegenetz wird geometrisch vereinheitlicht und in annährend gleich lange Segmente von 100 m unterteilt. Die Segmentierung erfolgt linienbasiert mittels Punktgenerierung via Geoverarbeitungstool GeneratePointsAlongLines entlang der Geometrie und anschliessender Linienaufteilung via SplitLineAtPoint. Die Segmentierung definiert die räumliche Auflösung der Analysen und ist für alle Perimeter identisch.

Code
    # Segmentierung Wanderwegenetz # ---------------------------------------------
    input_fc = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch\scratch.gdb\main_tlm_strassen_strasse_clip"

    root = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch"
    analysis_gdb_name = "scratch_analysis.gdb"
    analysis_gdb = os.path.join(root, analysis_gdb_name)

    segment_length = 100  # Meter

    # scratch_analysis.gdb erstellen (falls nicht vorhanden)
    if not arcpy.Exists(analysis_gdb):
        arcpy.management.CreateFileGDB(
            out_folder_path=root,
            out_name=analysis_gdb_name
        )

    # Dissolve
    dissolved_fc = os.path.join(analysis_gdb, "wanderwege_dissolved")

    arcpy.management.Dissolve(
        in_features=input_fc,
        out_feature_class=dissolved_fc,
        dissolve_field=None
    )

    # Punkte entlang der Linien erzeugen
    points_fc = os.path.join(analysis_gdb, "split_points")

    arcpy.management.GeneratePointsAlongLines(
        Input_Features=dissolved_fc,
        Output_Feature_Class=points_fc,
        Point_Placement="DISTANCE",
        Distance=f"{segment_length} Meters"
    )

    # Linien an diesen Punkten splitten
    out_name = f"wanderwege_seg"
    out_fc = os.path.join(analysis_gdb, out_name)
    if arcpy.Exists(out_fc):
        arcpy.Delete_management(out_fc)

    arcpy.management.SplitLineAtPoint(
        in_features=dissolved_fc,
        point_features=points_fc,
        out_feature_class=out_fc,
        search_radius="1 Meters"
    )

    # Segmentlängen berechnen
    arcpy.management.AddGeometryAttributes(
        out_fc,
        "LENGTH"
    )

    # Eindeutige Segment-ID generieren für spätere Geoverarbeitungen
    if "SEG_ID" not in [f.name for f in arcpy.ListFields(out_fc)]:
        arcpy.AddField_management(out_fc, "SEG_ID", "LONG")

    with arcpy.da.UpdateCursor(
        out_fc,
        ["OBJECTID", "SEG_ID"]
    ) as cur:
        for oid, _ in cur:
            cur.updateRow((oid, oid))

    # Cleanup
    arcpy.management.Delete(points_fc)
    arcpy.management.Delete(dissolved_fc)
    # Ende der Teilfunktion

Analyse Qualitätsparameter

Die Analyse der Qualitätsparameter basiert auf dem segmentierten Wanderwegenetz als geometrische Referenz. Je nach Geometrietyp der Grundlagedaten kommen unterschiedliche GIS-Analyseansätze zur Anwendung, weshalb die Unterkapitel entsprechend nach Geometrie unterteilt wurden. Die Ergebnisse werden entweder als neue Felder in die Attributtabelle der Wanderwegsegmente oder in separate Tabellen geschrieben.

Analyse Punktdaten

Die punktbasierten Qualitätsparameter (vgl. Tabelle 1) werden durch Distanzberechnungen zu den Wanderwegsegmenten ermittelt. Für jeden Datensatz wird die minimale euklidische Distanz zwischen dem jeweiligen Wanderwegsegment und dem nächstgelegenen Punkt berechnet (Geoverarbeitungstool: Near). Die euklidischen Distanzen dienen als Proxy für die Erreichbarkeit. Eine netzwerkbasierte Analyse auf Basis der Wanderwege wurde aufgrund der hohen Rechenintensivität nicht durchgeführt. Die Distanzwerte werden als numerische Attribute direkt auf den Referenzdatensatz Wanderwegsegmente geschrieben.

Code
    # Analyse Vektordaten # ---------------------------------------------
    # Punktdaten
    segments_fc = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch\scratch_analysis.gdb\wanderwege_seg"
    gdb_path = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch\scratch.gdb"

    poi_sets = {
        "OeV": os.path.join(gdb_path, "Betriebspunkt_OeV_clip"),
        "REST": os.path.join(gdb_path, "poi_restaurant"),
        "VIEW": os.path.join(gdb_path, "poi_view"),
        "INFRA": os.path.join(gdb_path, "poi_infrastructure")
    }

    for key, points_fc in poi_sets.items():

        # Near
        arcpy.analysis.Near(
            in_features=segments_fc,
            near_features=points_fc,
        )

        near_field = f"NEAR_{key}"

        # Near-Distanz-Feld anlegen
        if near_field not in [f.name for f in arcpy.ListFields(segments_fc)]:
            arcpy.management.AddField(
            segments_fc,
            near_field,
            "DOUBLE"
        )

        # NEAR_DIST in eigenes Feld kopieren
        with arcpy.da.UpdateCursor(
        segments_fc,
        ["NEAR_DIST", near_field]
        ) as cursor:
            for near_dist, stored_dist in cursor:
                cursor.updateRow((near_dist, near_dist))
    # Ende der Teilfunktion

Analyse Polygondaten

Der TLMRegio Landcover Datensatz beinhaltet elf Klassen (z.B. «Wald», «Siedlung», «Sumpf», etc.), welche einzeln analysiert werden. Mit Beizug des zusammengefassten Datensatzes “Biotopinventare” (vgl. Kapitel 2.4.1.5) erfolgt die Berechnung der somit zwölf Qualitätsparameter über räumliche Überlagerungen mit den Wanderwegsegmenten (Geoverarbeitungstool: Intersect). Für jede Klasse des Landcover Datensatzes wird ein temporärer Verschnitt erzeugt, auf dem die Länge der Wanderwegsegmente innerhalb des Polygons berechnet und summiert wird. Dieser Zwischenschritt ist für die Biotopinventare nicht nötig. Die summierten Längen sowie die berechneten Anteile an der Gesamtlänge des Wanderwegenetzes werden in eine Tabelle geschrieben.

Code
    # Polygondaten
    segments_fc = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch\scratch_analysis.gdb\wanderwege_seg"
    landcover_fc = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch\scratch.gdb\TLMRegio_LandCover_clip"
    scratch_gdb = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch\scratch_analysis.gdb"

    # Gesamtlänge Wanderwegnetz (Referenz)
    length_field = "LENGTH"
    total_length = 0.0
    with arcpy.da.SearchCursor(segments_fc, [length_field]) as cursor:
        total_length = sum(row[0] for row in cursor)
    print(f"Gesamtlänge Wanderwege für Perimeter {perimeter_name}: {total_length:.1f} m")

    # Ergebnistabelle erstellen
    result_table = os.path.join(scratch_gdb, "PolygonRatios")

    if arcpy.Exists(result_table):
        arcpy.management.Delete(result_table)

    arcpy.management.CreateTable(scratch_gdb, "PolygonRatios")
    arcpy.management.AddField(result_table, "Layer", "TEXT", 50)
    arcpy.management.AddField(result_table, "Category", "TEXT", 80)
    arcpy.management.AddField(result_table, "SumLength", "DOUBLE")
    arcpy.management.AddField(result_table, "Ratio", "DOUBLE")

    # Landcover
    landcover_field = "LANDCOVER_LABEL"
    landcover_values = get_unique_values(landcover_fc, landcover_field)

    for label in landcover_values:
        where = f"{landcover_field} = '{label}'"
        landcover_lyr = "landcover_lyr"
        arcpy.management.MakeFeatureLayer(landcover_fc, landcover_lyr, where)

        out_fc = os.path.join(scratch_gdb, f"lc_{label}")
        if arcpy.Exists(out_fc):
            arcpy.management.Delete(out_fc)

        arcpy.analysis.Intersect([segments_fc, landcover_lyr], out_fc)

        # Länge berechnen
        arcpy.management.AddGeometryAttributes(out_fc, "LENGTH")

        sum_len = 0.0
        with arcpy.da.SearchCursor(out_fc, ["LENGTH"]) as cursor:
            sum_len = sum(row[0] for row in cursor)

        ratio = sum_len / total_length if total_length > 0 else 0

        with arcpy.da.InsertCursor(
            result_table,
            ["Layer", "Category", "SumLength", "Ratio"]
        ) as ic:
            ic.insertRow(["Landcover", label, sum_len, ratio])
        
        # Cleanup
        arcpy.management.Delete(out_fc)

    # Biotopinventare
    biotope_fc = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch\scratch.gdb\Biotopinventare_Dissolved"
    out_fc = os.path.join(scratch_gdb, "biotope_intersect")

    # Intersect Wanderwegsegmente × Biotope
    arcpy.analysis.Intersect(
        [segments_fc, biotope_fc],
        out_fc
    )

    # Länge berechnen
    arcpy.management.AddGeometryAttributes(
        out_fc,
        "LENGTH",
        Length_Unit="METERS"
    )

    # Länge summieren
    sum_len = 0.0
    with arcpy.da.SearchCursor(out_fc, ["LENGTH"]) as cursor:
        sum_len = sum(row[0] for row in cursor)

    # Ratio berechnen
    ratio = sum_len / total_length if total_length > 0 else 0

    # In Ergebnistabelle schreiben
    with arcpy.da.InsertCursor(
        result_table,
        ["Layer", "Category", "SumLength", "Ratio"]
    ) as ic:
        ic.insertRow(["Biotopinventare", "ALL", sum_len, ratio])

    # Cleanup
    arcpy.management.Delete(out_fc)
    # Ende der Teilfunktion

Analyse Rasterdaten

Höhenausprägung (DHM25)

Je Wanderwegsegment werden die mittleren Höhenwerte mit dem Geoverarbeitungstool Add Surface Information berechnet. Diese Werte werden direkt als numerische Attribute in die Attributtabelle der Segmente geschrieben. Die Höhenwerte werden zur Vergleichbarkeit und Berechnung von Anteilen in Höhenstufen eingeteilt. Die berechneten Anteile an der Gesamtlänge des Wanderwegenetzes werden in eine Tabelle geschrieben.

Tabelle 3: Beschreibung der Höhenstufen
Höhenstufe Höhenwerte (m.ü.M.)
1 0 - 500
2 500 - 1000
3 1000 - 1500
4 1500 - 2000
5 2000 - 2500
6 über 2500
Code
    # Analyse Rasterdaten # ---------------------------------------------
    # Höhenintervalle
    scratch_gdb = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch\scratch_analysis.gdb"
    segments_fc = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch\scratch_analysis.gdb\wanderwege_seg"
    dhm25_raster = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch\scratch.gdb\dhm25_2056_clip"
    result_table_hoehen = os.path.join(scratch_gdb, "Hoehenanalyse")

    # Gesamtlänge Wanderwegnetz (Referenz)
    length_field = "LENGTH"
    total_length = 0.0
    with arcpy.da.SearchCursor(segments_fc, [length_field]) as cursor:
        total_length = sum(row[0] for row in cursor)

    # Höhenwert (Raster) auf Segmente übertragen 
    arcpy.ddd.AddSurfaceInformation(
        in_feature_class=segments_fc,
        in_surface=dhm25_raster,
        out_property="Z_MEAN",
        method="BILINEAR"
    )

    fields = [f.name for f in arcpy.ListFields(segments_fc)]
    if "HOEHE_MEAN" in fields:
        arcpy.management.DeleteField(segments_fc, "HOEHE_MEAN")

    arcpy.management.AlterField(
        segments_fc,
        "Z_MEAN",
        new_field_name="HOEHE_MEAN",
        new_field_alias="HOEHE_MEAN"
    )

    # Feld für Höhenklasse
    if "HOEHE_KL" not in [f.name for f in arcpy.ListFields(segments_fc)]:
        arcpy.AddField_management(segments_fc, "HOEHE_KL", "SHORT")

    with arcpy.da.UpdateCursor(
        segments_fc,
        ["HOEHE_MEAN", "HOEHE_KL"]
    ) as cursor:
        for z, kl in cursor:

            if z is None:
                kl = -1
            elif z <= 500:
                kl = 1
            elif 500 < z <= 1000:
                kl = 2
            elif 1000 < z <= 1500:
                kl = 3
            elif 1500 < z <= 2000:
                kl = 4
            elif 2000 < z <= 2500:
                kl = 5
            else:
                kl = 6   # >2500

            cursor.updateRow((z, kl))

    # Segmentlängen pro Höhenklasse summieren
    hoehen_sum = {
        1: 0.0,
        2: 0.0,
        3: 0.0,
        4: 0.0,
        5: 0.0,
        6: 0.0
    }

    with arcpy.da.SearchCursor(
        segments_fc,
        ["HOEHE_KL", "LENGTH"]
    ) as cursor:
        for kl, length in cursor:
            if kl in hoehen_sum:
                hoehen_sum[kl] += length

    # Ergebnistabelle erzeugen
    if arcpy.Exists(result_table_hoehen):
        arcpy.Delete_management(result_table_hoehen)

    arcpy.CreateTable_management(scratch_gdb, "Hoehenanalyse")
    arcpy.AddField_management(result_table_hoehen, "Hoehenklasse", "SHORT")
    arcpy.AddField_management(result_table_hoehen, "h_min", "SHORT")
    arcpy.AddField_management(result_table_hoehen, "h_max", "SHORT")
    arcpy.AddField_management(result_table_hoehen, "SumLength", "DOUBLE")
    arcpy.AddField_management(result_table_hoehen, "Ratio", "DOUBLE")

    # Ergebnisse in Tabelle schreiben
    hoehen_klassen = {
        1: (0, 500),
        2: (500, 1000),
        3: (1000, 1500),
        4: (1500, 2000),
        5: (2000, 2500),
        6: (2500, 9999)
    }

    with arcpy.da.InsertCursor(
        result_table_hoehen,
        ["Hoehenklasse", "h_min", "h_max", "SumLength", "Ratio"]
    ) as ic:

        for kl, (h_min, h_max) in hoehen_klassen.items():
            length = hoehen_sum.get(kl, 0.0)
            ratio = length / total_length if total_length > 0 else 0
            ic.insertRow([kl, h_min, h_max, length, ratio])
    # Ende der Teilfunktion

Lärmbelastung

Das Geoverarbeitungstool Add Surface Information funktioniert spezifisch für Höhenwerte, weshalb für die Analyse der Raster-Lärmwerte ein alternativer Analyseansatz verwendet wird. Für jede Segmentlinie wird ein Punkt via FeatureToPoint generiert. Dieser liegt i.d.R. in der geometrischen Mitte der Linie. Auf Grundlage der Punkte, kann via ExtractValuesToPoints der Rasterwert entnommen werden. Entsprechend ist der Lärmwert am mittleren Punkt ist repräsentativ für das gesamte Segment. Analog zur Höhenanalyse, werden die Lärmwerte zur Vergleichbarkeit und Berechnung von Anteilen in Lärmstufen eingeteilt. Die berechneten Anteile an der Gesamtlänge des Wanderwegenetzes werden in eine Tabelle geschrieben.

Tabelle 4: Beschreibung der Lärmstufen
Lärmstufe Schallwerte (dB)
1 40 - 50
2 50 - 60
3 über 60
Code
    # Lärmanalyse
    scratch_gdb = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch\scratch_analysis.gdb"
    segments_fc = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch\scratch_analysis.gdb\wanderwege_seg"
    laerm_raster = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch\scratch.gdb\laerm_strassenlaerm_tag_2056_clip"
    result_table_laerm = os.path.join(scratch_gdb, "Laermanalyse")

    midpoints_fc = os.path.join(scratch_gdb, "seg_midpoints")
    midpoints_fc_db = os.path.join(scratch_gdb, "seg_midpoints_db")

    for fc in [midpoints_fc, midpoints_fc_db]:
        if arcpy.Exists(fc):
            arcpy.management.Delete(fc)

    fields = [f.name for f in arcpy.ListFields(segments_fc)]
    if "RASTERVALU" in fields:
        arcpy.management.DeleteField(segments_fc, "RASTERVALU")

    arcpy.management.FeatureToPoint(
        in_features=segments_fc,
        out_feature_class=midpoints_fc,
        point_location="INSIDE"
    )

    arcpy.sa.ExtractValuesToPoints(
        in_point_features=midpoints_fc,
        in_raster=laerm_raster,
        out_point_features=midpoints_fc_db,
        interpolate_values="NONE"
    )

    arcpy.management.JoinField(
        in_data=segments_fc,
        in_field="SEG_ID",
        join_table=midpoints_fc_db,
        join_field="SEG_ID",
        fields=["RASTERVALU"]
    )

    fields = [f.name for f in arcpy.ListFields(segments_fc)]
    if "LAERM_MEAN" in fields:
        arcpy.management.DeleteField(segments_fc, "LAERM_MEAN")

    arcpy.management.AlterField(
        segments_fc,
        "RASTERVALU",
        new_field_name="LAERM_MEAN",
        new_field_alias="LAERM_MEAN"
    )

    # Feld für Lärmklasse
    if "LAERM_KL" not in [f.name for f in arcpy.ListFields(segments_fc)]:
        arcpy.AddField_management(segments_fc, "LAERM_KL", "SHORT")

    with arcpy.da.UpdateCursor(
        segments_fc,
        ["LAERM_MEAN", "LAERM_KL"]
    ) as cursor:
        for z, kl in cursor:
            if z is None:
                kl = -1
            elif 40 <= z < 50:
                kl = 1   # 40–50 dB
            elif 50 <= z < 60:
                kl = 2   # 50–60 dB
            elif z >= 60:
                kl = 3   # >60 dB
            else:
                kl = 0   # <40 dB 
            cursor.updateRow((z, kl))

    # Segmentlängen pro Lärmklasse summieren
    laerm_sum = {
        1: 0.0,  # 40–50 db
        2: 0.0,  # 50–60 db
        3: 0.0   # >60 db
    }

    with arcpy.da.SearchCursor(
        segments_fc,
        ["LAERM_KL", "LENGTH"]
    ) as cursor:
        for kl, length in cursor:
            if kl in laerm_sum:
                laerm_sum[kl] += length

    # Ergebnistabelle erzeugen
    if arcpy.Exists(result_table_laerm):
        arcpy.Delete_management(result_table_laerm)

    arcpy.CreateTable_management(scratch_gdb, "Laermanalyse")
    arcpy.AddField_management(result_table_laerm, "Laermklasse", "SHORT")
    arcpy.AddField_management(result_table_laerm, "db_min", "SHORT")
    arcpy.AddField_management(result_table_laerm, "db_max", "SHORT")
    arcpy.AddField_management(result_table_laerm, "SumLenght", "DOUBLE")
    arcpy.AddField_management(result_table_laerm, "Ratio", "DOUBLE")

    # Ergebnisse in Tabelle schreiben
    laerm_klassen = {
        1: (40, 50),
        2: (50, 60),
        3: (60, 999)
    }

    with arcpy.da.InsertCursor(
        result_table_laerm,
        ["Laermklasse", "db_min", "db_max", "SumLenght", "Ratio"]
    ) as ic:
        for kl, (db_min, db_max) in laerm_klassen.items():
            length = laerm_sum.get(kl, 0.0)
            ratio = length / total_length if total_length > 0 else 0
            ic.insertRow([kl, db_min, db_max, length, ratio])

    # Cleanup
    arcpy.management.Delete(midpoints_fc)
    arcpy.management.Delete(midpoints_fc_db)
    # Ende der Teilfunktion

Sicherung und Abschluss der GIS-Analyse

Nach Abschluss der Berechnungen bzw. einer Berechnungs-Schleife pro Perimeter werden die attributierten Wanderwegsegmente sowie die separaten Tabellen in eine Ausgabe-Geodatenbank überführt. Die Attributtabelle der Wanderwegsegmente wird ohne Geometrie gespeichert und dient der nachgelagerten Auswertung in R. Die temporären Layer und Arbeitsdaten werden gelöscht. Die Arbeitsdatenbank kann innerhalb der Analyseschleife aufgrund von LOCK-Dateien nicht gelöscht werden. Die Überschreibung durch arcpy.env.overwriteOutput = True funktioniert hingegen innerhalb der Schleife.

Code
    # Analyseabschluss # ---------------------------------------------
    # Name der Output-GDB
    output_gdb_name = f"{perimeter_name}.gdb"

    output_folder = r"C:\Users\domin\Documents\Projekt_PWRG2\output\Py"
    output_gdb = os.path.join(output_folder, output_gdb_name)

    # Pfade zu den Scratch-GDBs
    scratch_analysis_gdb = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch\scratch_analysis.gdb"
    scratch_gdb = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch\scratch.gdb"

    # Output-Ordner erstellen, falls er nicht existiert
    if not os.path.exists(output_folder):
        os.makedirs(output_folder)

    # Kopiere scratch_analysis.gdb nach Output
    if arcpy.Exists(output_gdb):
        arcpy.Delete_management(output_gdb)

    arcpy.Copy_management(scratch_analysis_gdb, output_gdb)
    print(f"GDB-Kopie erstellt: {output_gdb}")

    # Feature Class → Table (keine Geometrie)
    table_bez = "wanderwege_seg_tbl"
    out_name = table_bez
    out_path = output_gdb 

    arcpy.conversion.TableToTable(
        in_rows=segments_fc,
        out_path=out_path,
        out_name=out_name
    )

    # Perimeter umbenennen
    input_shp = r"C:\Users\domin\Documents\Projekt_PWRG2\output\Perimeter\perimeter.shp"
    output_folder = r"C:\Users\domin\Documents\Projekt_PWRG2\output\Perimeter"

    # Shapefile-Name aus GDB-Name ableiten
    output_name = os.path.splitext(output_gdb_name)[0] 
    output_shp = os.path.join(output_folder, f"{output_name}.shp")

    # Ausgabe-Ordner erstellen, falls er nicht existiert
    if not os.path.exists(output_folder):
        os.makedirs(output_folder)

    # Shapefile kopieren
    if arcpy.Exists(input_shp):
        arcpy.CopyFeatures_management(input_shp, output_shp)

    # Altes Shapefile löschen
    if arcpy.Exists(input_shp):
        arcpy.Delete_management(input_shp)
    # Ende der Teilfunktion

Informationsvisualisierung

Mit der Informationsvisualisierung werden die Ausgabe-Geodatenbanken (GDB) je Perimeter aufbereitet und für die Ergebnisinterpretation visualisiert.

Hilfsfunktionen werden definiert, um Layer aus den GDB einzulesen. Des Weiteren wird die jeweilige Perimeterbezeichnung aus dem Dateinamen der GDB extrahiert und als zusätzliches Attribut ergänzt. Für die Distanzanalysen wird die Attributtabelle in ein langes Format überführt und die Median-Distanzen pro Kategorie berechnet. Zur repetitiven Zusammenstellung der Gesamttabellen je Auswertungskategorie (siehe nachfolgende Auflistung) ausgehend der Ausgabe-Geodatenbanken wird map_dfr aus dem Paket purrr eingesetzt. Die Funktion kombiniert Schleifenlogik mit einer automatischen zeilenweisen Zusammenführung der Resultate. Die Auswertung und Informationsvisualisierungen umfassen:

  • Anteile des Wanderwegenetzes in

    • nationalen Biotopen,

    • Landcover-Klassen,

    • Höhenstufen,

    • Lärmstufen,

  • mittlere Distanzen zu den Points of Interest (POIs).

Diese werden mithilfe von Heatmaps und Balkendiagrammen mittels ggplot2 visualisiert, wobei die Perimeter vergleichend dargestellt und die jeweiligen Anteile bzw. Medianwerte farblich kodiert werden.

Das R-Skript liegt als Anahng bei.

Ergebnisse

Die Anteile der Wanderwege innerhalb der nationalen Biotopinventare am Gesamtwanderwegenetz variieren über die untersuchten Naturpärke hinweg stark und liegen zwischen nahezu 0 % und 7 %. Dabei weisen die Perimeter b und j die höchsten Anteile auf, während der Perimeter d einen deutlich geringeren Wert aufweist.

Abbildung 4: Perimeterbezeichnung: a) Parc Jura vaudois, b) Parc du Doubs, c) Parc régional Chasseral, d) Parc naturel régional de la Vallée du Trient, e) Parc naturel régional Gruyère Pays-d’Enhaut, f) Naturpark Gantrisch, g) Naturpark Diemtigtal, h) Naturpark Thal, i) Naturpark Pfyn-Finges, j) UNESCO Biophäre Entlebuch, k) Jurapark Aargau, l) Landschaftspark Binntal, m) Regionaler Naturpark Schaffhausen, n) Parco Val Calanca, o) Naturpark Beverin, p) Parc Ela, q) Biosfera Val Müstair.

Bei der Interpretation der Ergebnisse der Landcover-Klassen ist zu berücksichtigen, dass die Summe der Anteilswerte über alle Landcoverklassen pro Perimeter nicht 1 entspricht, da der Datensatz TLMRegio Landcover nicht vollständig flächig alle Bodenbedeckungen abdeckt. Landwirtschaftliche Flächen beispielsweise werden nicht definiert und sind daher nicht Bestandteil des Datensatzes.

Über alle Naturpärke hinweg dominieren Waldflächen, die den höchsten Anteil im Bezug auf das Wanderwegenetz ausmachen. Die beiden Naturpärke im Flachland, k und m, zeigen mit Bezug auf Wanderwege innerhalb von Siedlungen vergleichsweise hohe Anteile von rund 15 %.

Abbildung 5: Perimeterbezeichnung: a) Parc Jura vaudois, b) Parc du Doubs, c) Parc régional Chasseral, d) Parc naturel régional de la Vallée du Trient, e) Parc naturel régional Gruyère Pays-d’Enhaut, f) Naturpark Gantrisch, g) Naturpark Diemtigtal, h) Naturpark Thal, i) Naturpark Pfyn-Finges, j) UNESCO Biophäre Entlebuch, k) Jurapark Aargau, l) Landschaftspark Binntal, m) Regionaler Naturpark Schaffhausen, n) Parco Val Calanca, o) Naturpark Beverin, p) Parc Ela, q) Biosfera Val Müstair.

Für den überwiegenden Teil der Naturpärke befindet sich das Wanderwegenetz innerhalb der Höhen von 500 bis 1500 m ü.M. Die beiden Naturpärke im Flachland (k und m) zeichnen sich durch einen hohen Anteil innerhalb der Höhenstufe 1 (bis 500 m ü.M.) aus. In den Höhenstufen 3 bis 5 (1000 bis 2500 m ü.M.) befinden sich die Perimeter n bis q, wobei eine erkennbare räumliche Clusterung in Graubünden besteht.

Abbildung 6: Perimeterbezeichnung: a) Parc Jura vaudois, b) Parc du Doubs, c) Parc régional Chasseral, d) Parc naturel régional de la Vallée du Trient, e) Parc naturel régional Gruyère Pays-d’Enhaut, f) Naturpark Gantrisch, g) Naturpark Diemtigtal, h) Naturpark Thal, i) Naturpark Pfyn-Finges, j) UNESCO Biophäre Entlebuch, k) Jurapark Aargau, l) Landschaftspark Binntal, m) Regionaler Naturpark Schaffhausen, n) Parco Val Calanca, o) Naturpark Beverin, p) Parc Ela, q) Biosfera Val Müstair.
Höhenstufen: 1) 0-500müM, 2) 500-1000müM, 3) 1000-1500müM, 4) 1500-2000müM, 5) 2000-2500müM, 6) >2500müM.

Die Anteile hoher Lärmbelastung sind insbesondere in den Naturpärken im Flachland (k und m) ausgeprägt, geprägt von der dort vorhandenen Transit-Infrastruktur. Zudem zeigen die Naturpärke in der Westschweiz (a bis c) vergleichsweise hohe Anteile in den oberen Lärmstufen.

Abbildung 7: Perimeterbezeichnung: a) Parc Jura vaudois, b) Parc du Doubs, c) Parc régional Chasseral, d) Parc naturel régional de la Vallée du Trient, e) Parc naturel régional Gruyère Pays-d’Enhaut, f) Naturpark Gantrisch, g) Naturpark Diemtigtal, h) Naturpark Thal, i) Naturpark Pfyn-Finges, j) UNESCO Biophäre Entlebuch, k) Jurapark Aargau, l) Landschaftspark Binntal, m) Regionaler Naturpark Schaffhausen, n) Parco Val Calanca, o) Naturpark Beverin, p) Parc Ela, q) Biosfera Val Müstair.
Lärmstufen: 1) 40-50dB, 2) 50-60dB, 3) >60dB.

ÖV-Abdeckung: In der Mehrzahl der Naturpärke befindet sich das Wanderwegenetz in mittlerer Entfernung von 1000 bis 1500 m zu den nächsten ÖV-Haltestellen. Die Perimeter e, g, j, n weisen grössere mittlere Distanzen von rund 2000 m auf.

Gastronomie: Über alle Naturpärke hinweg zeigen Wanderwege die grössten mittleren Entfernungen zu gastronomischen Einrichtungen. Eine räumliche Clusterung grösserer mittlerer Entfernungen ist in den Naturpärken n bis q in Graubünden erkennbar.

Freizeitinfrastruktur (Grillplätze, Trinkwasserstellen): Diese verteilen sich über alle Naturpärke mit mittleren Entfernungen von rund 1000 bis 1500 m, ausgenommen der Naturpark k (Jurapark Aargau), der die tiefste mittlere Entfernung von rund 500 m aufweist.

Aussichtspunkte: Die mittleren Entfernungen liegen über alle Naturpärke bei rund 1500 m, ohne erkennbare räumliche Clusterung in Abhängigkeit von der Topografie der Schweiz.

Abbildung 8: Perimeterbezeichnung: a) Parc Jura vaudois, b) Parc du Doubs, c) Parc régional Chasseral, d) Parc naturel régional de la Vallée du Trient, e) Parc naturel régional Gruyère Pays-d’Enhaut, f) Naturpark Gantrisch, g) Naturpark Diemtigtal, h) Naturpark Thal, i) Naturpark Pfyn-Finges, j) UNESCO Biophäre Entlebuch, k) Jurapark Aargau, l) Landschaftspark Binntal, m) Regionaler Naturpark Schaffhausen, n) Parco Val Calanca, o) Naturpark Beverin, p) Parc Ela, q) Biosfera Val Müstair.

Diskussion

Die Auswahl der Qualitätsparameter erschien geeignet, um Unterschiede in der Wanderwegeinfrastruktur zwischen den regionalen Naturpärken abzubilden. Dabei ermöglichten die gewählten Parameter eine Kombination aus Punkt-, Linien-, Polygondaten und Rasterdaten, was die Analyse vielfältig und praxisrelevant gestaltet.

Für die Distanzberechnungen wurde die ArcPy-Funktion Near eingesetzt. Im Gegensatz zu einfachen Pufferanalysen ermöglicht Near die exakte Berechnung der kürzesten Distanz jedes Wanderwegsegments zu einem Punktobjekt. Dadurch entstehen kontinuierliche Distanzwerte, die quantitative Unterschiede zwischen den Segmenten präzise abbilden, während Pufferanalysen nur binär erfassen, ob ein Objekt innerhalb eines definierten Radius liegt.

Die Ergebnisse zeigen regionale Unterschiede zwischen den Perimetern. Flachland-Naturpärke weisen vergleichsweise höhere Lärmbelastung und stärkere Durchdringung durch Siedlungsflächen auf, während alpine Perimeter grössere Höhenstufen und längere Abstände zu gastronomischen Einrichtungen aufweisen.

Ein Ausblick auf mögliche Erweiterungen umfasst insbesondere den Vergleich unterschiedlicher räumlicher Auflösungen bzw. Segmentlängen, um die Sensitivität der Ergebnisse zu prüfen, sowie die Implementierung netzwerkbasierter Analyseverfahren, die Distanzberechnungen zu POIs realistischer abbilden könnten.

Reflexion

Im Verlauf der Projektarbeit habe ich mich entschieden von der ursprünglich vorgesehenen Produktentwicklung gemäss Aufgabenbeschrieb in Complesis abzuweichen. Anstelle der Erarbeitung eines automatisierten Systems zur Generierung von Wanderroutenempfehlungen (und der damit verbundenen Erarbeitung eines Algorithmus und grafischer Benutzeroberfläche) habe ich den inhaltlichen Fokus auf eine klassische, analytisch-planerische Fragestellung gelegt: Die quantitative Untersuchung anhand Qualitätsparametern von Wanderwegen in den regionalen Naturpärken der Schweiz. Diese Umorientierung erfolgte aus mehreren Gründen. Im Vordergrund der Arbeit stand explizit die Aneignung von Programmierkenntnissen in Python und ArcPy ohne vorgängige Erfahrung in dieser Umgebung. Die Entwicklung eines vollständigen Routenempfehlungsalgorithmus hätte eine technisch und mathematisch anspruchsvolle Umsetzung als geschlossener Gesamtprozess erfordert, bei der einzelne Teilschritte kaum isoliert entwickelt und getestet werden können. Für den Lernprozess wäre dies wenig geeignet gewesen, da Fehlerquellen schwer lokalisierbar sind und der Code nur als Ganzes funktioniert. Die gewählte Wanderweganalyse erlaubt hingegen einen modularen und schrittweisen Aufbau des Analyseprozesses. Einzelne Geoprocessing-Schritte können unabhängig voneinander entwickelt, getestet und erweitert werden. Für mich reduzierte dies die Schwelle für den Start des Codings und erhöhte die Nachvollziehbarkeit sowie das Verständnis von Codes in der Analyse. Ein weiterer wesentlicher Grund für die Neuausrichtung liegt in der inhaltlichen Breite der gewählten Analyse. Die Qualitätsanalyse der Wanderwege ermöglicht den Einsatz einer Vielzahl unterschiedlicher Geodatenformate und Analyseverfahren, darunter Punkt-, Linien- und Polygondaten sowie Rasterdaten. Schliesslich ist die gewählte Fragestellung auch fachlich näher an meinem beruflichen Tätigkeitsbereich der Raumplanung angelehnt. Die Analyse von Infrastrukturqualitäten, Erreichbarkeit, Umweltbelastungen und landschaftlichen Eigenschaften repräsentiert eine typische planerische Aufgabe. Die erarbeiteten Methoden und Workflows können auf andere raumplanerische Fragestellungen übertragen werden, womit ein hoher praktischer Nutzen vorliegt.

Das methodische Ziel, Kenntnisse im GIS-Skripting mit ArcPy anzueignen und praxisnah anzuwenden, konnte ich erreichen. Die Aneignung dieser Kenntnisse war mit einem Aufwand von rund 80 h verbunden und lag über dem geschätzten Aufwand gemäss Aufgabenbeschrieb in Complesis. Dabei umfasste der Lernprozess den Einstieg in Python und insbesondere das Verständnis von ArcPy. Hierzu nutze ich die Literatur (Tateosian 2015) und (Theis 2022), ergänzt durch die ESRI-Referenzwebsite für ArcGIS Pro-Geoverarbeitungswerkzeuge (ESRI o. J.). Weiter beinhalten diese Stunden die Auseinandersetzung und Einarbeitung in die neue Programmierumgebungen (Details hierzu nachfolgend). Zur Entwicklung und Durchführung des Projekts brauchte ich ungefähr den geschätzten Aufwand von 90 h. Die Erarbeitung des vorliegenden Berichtes und sowie der Präsentation erfolgten auch im ungefähren Zeitrahmen von 20 - 30 h. Ein zentrales Lernergebnis bestand in der Erkenntnis, dass ArcPy zwar auf Python basiert, inhaltlich jedoch stark an ArcGIS gebunden ist. ArcPy kann nur innerhalb einer entsprechend konfigurierten ArcGIS-Python-Umgebung ausgeführt werden. In diesem Zusammenhang war für mich auch die Auseinandersetzung mit den Begriffen Programmierschnittstelle (API) sowie Entwicklungsumgebung (IDE) wichtig. ArcPy stellt eine API dar, über welche Funktionen von ArcGIS programmgesteuert ausgeführt werden können. Für die Arbeit mit ArcPy musste in der Entwicklungsumgebung explizit die von ArcGIS Pro bereitgestellte Python-Umgebung (arcgispro-py3) als Interpreter ausgewählt werden, da nur dort die erforderlichen Bibliotheken verfügbar sind. Fehlerhafte Einstellungen führten anfangs häufig dazu, dass ArcPy nicht korrekt geladen werden konnte.

Als Entwicklungsumgebung wurde hauptsächlich Visual Studio Code (VSCode) eingesetzt. Im Vergleich zu RStudio bietet VSCode deutlich weniger grafische Unterstützung und erfordert ein stärkeres Arbeiten auf Code- und Dateiebene. Gleichzeitig erwies sich VSCode als Vorteil, da unterschiedliche Arbeitsumgebungen wie Python-Skripte, Jupyter-Notebooks, R-Skripte und Quarto-Dokumente innerhalb einer einzigen Entwicklungsumgebung genutzt werden konnten. Für die Entwicklung der Analyse wurde zunächst mit Jupyter-Notebooks (konkreter: ArcGIS-Notebook) gearbeitet. Diese eigneten sich gut für den Lernprozess, da einzelne Codeabschnitte schrittweise ausgeführt und die Datei in VSCode sowie in ArcGIS bearbeitet werden konnte. Zudem erlaubte die Kombination von Code und Text eine übersichtliche Dokumentation der Analyseschritte. Nach Abschluss der Entwicklungsphase wurde der Code in ein Python-Skript überführt, um die Analyse automatisiert für mehrere Untersuchungsperimeter durchführen zu können. Für die Informationsvisualisierung wurden die Ergebnisse in R aufbereitet. Aus Gründen der Benutzerfreundlichkeit und besseren GUIs (grafische Unterstützung) wurde dafür RStudio verwendet. Die Erstellung des technischen Berichts erfolgte in Quarto, um eine im Vergleich zu Jupyter-Notebooks professionellere Strukturierung und Zitierfähigkeit zu gewährleisten. Python und R konnten in Quarto gemeinsam verwendet werden, solange die Outputs nicht gerendert werden. Der Quarto-Guide (Scheidegger u. a. o. J.) diente mir als technische Grundlage für die Berichterarbeitung.

Die Arbeit wurde als selbständige Projekterarbeitung durchgeführt.

Neben der verwendeten Literatur unterstützte mich KI in der Aneigung von Skripting-Verständnis und der Erarbeitung von Codes (OpenAI 2025).

Anhang

Ordnerstruktur

Skript ArcPy

Code
# Vollständiges Python-Skript zur Wanderweg-Analyse der regionalen Pärke der Schweiz 

# Für jeden Park wird die Berechnung in einer Schleife durchgeführt und in einer eindeutigen GDB gespeichert.

# Dominik Erni
# PWRG2


# Module Import
import arcpy
import os
import geopandas as gpd
import osmnx as ox


arcpy.env.overwriteOutput = True  

# Perimeter
perimeters = [
    ("a", r"C:\Users\domin\Documents\Projekt_PWRG2\data\Perimeter\Perimeter.gdb\a"),
    ("b", r"C:\Users\domin\Documents\Projekt_PWRG2\data\Perimeter\Perimeter.gdb\b"),
    ("c", r"C:\Users\domin\Documents\Projekt_PWRG2\data\Perimeter\Perimeter.gdb\c"),
    ("d", r"C:\Users\domin\Documents\Projekt_PWRG2\data\Perimeter\Perimeter.gdb\d"),
    ("e", r"C:\Users\domin\Documents\Projekt_PWRG2\data\Perimeter\Perimeter.gdb\e"),
    ("f", r"C:\Users\domin\Documents\Projekt_PWRG2\data\Perimeter\Perimeter.gdb\f"),
    ("g", r"C:\Users\domin\Documents\Projekt_PWRG2\data\Perimeter\Perimeter.gdb\g"),
    ("h", r"C:\Users\domin\Documents\Projekt_PWRG2\data\Perimeter\Perimeter.gdb\h"),
    ("i", r"C:\Users\domin\Documents\Projekt_PWRG2\data\Perimeter\Perimeter.gdb\i"),
    ("j", r"C:\Users\domin\Documents\Projekt_PWRG2\data\Perimeter\Perimeter.gdb\j"),
    ("k", r"C:\Users\domin\Documents\Projekt_PWRG2\data\Perimeter\Perimeter.gdb\k"),
    ("l", r"C:\Users\domin\Documents\Projekt_PWRG2\data\Perimeter\Perimeter.gdb\l"),
    ("m", r"C:\Users\domin\Documents\Projekt_PWRG2\data\Perimeter\Perimeter.gdb\m"),
    ("n", r"C:\Users\domin\Documents\Projekt_PWRG2\data\Perimeter\Perimeter.gdb\n"),
    ("o", r"C:\Users\domin\Documents\Projekt_PWRG2\data\Perimeter\Perimeter.gdb\o"),
    ("p", r"C:\Users\domin\Documents\Projekt_PWRG2\data\Perimeter\Perimeter.gdb\p"),
    ("q", r"C:\Users\domin\Documents\Projekt_PWRG2\data\Perimeter\Perimeter.gdb\q")
]


# Hilfsfunktionen
def get_poi_type(row):
    '''OSM-Typen filtern für Handhabung in Feature Class'''
    if row.get("amenity") in ["restaurant", "drinking_water", "bbq"]:
        return row["amenity"]
    if row.get("natural") == "peak":
        return "peak"
    if row.get("tourism") == "viewpoint":
        return "viewpoint"
    return "other"

def get_unique_values(fc, field):
    '''Auflistung von Werten innerhalb Attributfeld.'''
    values = set()
    with arcpy.da.SearchCursor(fc, [field]) as cursor:
        for row in cursor:
            if row[0] is not None:
                values.add(row[0])
    return sorted(values)


# Hauptfunktion Analyse
def run_analysis(perimeter_name, perimeter_fc):
    # Perimeter abrufen # ---------------------------------------------
    OutDir = r"C:\Users\domin\Documents\Projekt_PWRG2\output\Perimeter"
    perimeter_shp = r"perimeter.shp"
    print("Ausgewählter Perimeter: {0}.".format(perimeter_name)) # klassische print-Variante Python 3 gem. Literatur Tateosian, 2015. Neu: f-string.
    # fcs in shape konventieren
    arcpy.FeatureClassToFeatureClass_conversion(perimeter_fc, OutDir, perimeter_shp)

    # OSM Abfrage POI # ---------------------------------------------
    # Perimeter/Area für OSM lesen in geopandas (CH1903+)
    perimeter = r"C:\Users\domin\Documents\Projekt_PWRG2\output\Perimeter\perimeter.shp"

    area = gpd.read_file(perimeter)
    area = area.to_crs(epsg=2056)

    # Für OSM-Abfrage WGS84
    area_wgs84 = area.to_crs(epsg=4326)
    polygon = area_wgs84.geometry.iloc[0] 

    # OSM-Tags definieren 
    tags = {
        "amenity": [
            "restaurant",
            "drinking_water",
            "bbq"              
        ],
        "natural": [
            "peak"             
        ],
        "tourism": [
            "viewpoint"        
        ]
    }

    # OSM-Abfrage
    pois = ox.features_from_polygon(polygon, tags)

    # Reprojektion zurück nach CH1903+
    pois = pois.to_crs(epsg=2056)

    pois_points = pois[pois.geometry.type == "Point"]
    pois_points["poi_type"] = pois_points.apply(get_poi_type, 1) # Anwendung Funktion "get_poi_type"

    # File GDB erstellen
    out_folder = r"C:\Users\domin\Documents\Projekt_PWRG2\data\OSM"
    gdb_name = "osm_pois.gdb"
    out_fc_name = "pois_osm"
    gdb_path = os.path.join(out_folder, gdb_name)
    if not arcpy.Exists(gdb_path):
        arcpy.CreateFileGDB_management(out_folder, gdb_name)

    # In GDB schreiben
    pois_points.to_file(
        gdb_path,
        driver="OpenFileGDB",
        layer=out_fc_name
    )

    # Clip Grundlagedaten mit Perimeter # ---------------------------------------------
    # Teil Vektordaten (für Feature Classes in allen Unterordner)
    # Ausnahme Ordner Perimeter und Order OSM

    rootDir = r"C:\Users\domin\Documents\Projekt_PWRG2\data"
    outDir  = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch"
    exclude_dirs = ["perimeter", "osm"]

    # Scratch-GDB erstellen
    scratch_gdb = os.path.join(outDir, "scratch.gdb")

    # scratch.gdb erstellen (falls nicht vorhanden)
    if not arcpy.Exists(scratch_gdb):
        arcpy.management.CreateFileGDB(scratch_gdb)

    for root, dirs, files in os.walk(rootDir):
        dirs[:] = [d for d in dirs if d.lower() not in exclude_dirs]
        
        for d in dirs:
            if d.lower().endswith(".gdb"):
                gdb_path = os.path.join(root, d)
                arcpy.env.workspace = gdb_path

                fcs = arcpy.ListFeatureClasses()
                for fc in fcs:
                    outfile = os.path.join(scratch_gdb, f"{fc}_clip")
                    
                    arcpy.analysis.Clip(in_features=fc,
                        clip_features=perimeter_fc,
                        out_feature_class=outfile)

    # Grundlagedaten mit Perimeter clippen
    # Teil Rasterdaten

    rootDir = r"C:\Users\domin\Documents\Projekt_PWRG2\data"
    outDir  = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch"
    exclude_dirs = {"perimeter", "osm"}

    for root, dirs, files in os.walk(rootDir):
        dirs[:] = [d for d in dirs if d.lower() not in exclude_dirs]

        for d in dirs:
            gdb_path = os.path.join(root, d)
            arcpy.env.workspace = gdb_path
            
            rasters = arcpy.ListRasters("*", "TIF")
            for ras in rasters:
                base = os.path.splitext(ras)[0]                 # Unterschied zu Vektordateien: Hier müssen die Endungen .tif etc entfernt werden.
                out_name = arcpy.ValidateTableName(f"{base}_clip", scratch_gdb) # Namens-Validierung hier auch ergänzt, weil Raster-Daten.
                outfile = os.path.join(scratch_gdb, out_name)

                arcpy.management.Clip(
                    in_raster=ras,
                    rectangle="#",             
                    out_raster=outfile,
                    in_template_dataset=perimeter_fc,
                    nodata_value="-9999",      
                    clipping_geometry="ClippingGeometry"
                )

    # Nachbearbeitung nach Clip # ---------------------------------------------
    # Landcover Bezeichnungen hinzufügen
    clipped_fc = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch\scratch.gdb\TLMRegio_LandCover_clip"
    new_field = "LANDCOVER_LABEL"

    # Prüfen, ob Feld bereits existiert
    field_names = [f.name for f in arcpy.ListFields(clipped_fc)]
    if new_field not in field_names:
        arcpy.management.AddField(clipped_fc, new_field, "TEXT", field_length=50)

    mapping = {
        0: "Wald",
        1: "Fels",
        2: "Geroell",
        3: "Gletscher",
        4: "See",
        5: "Siedl",
        6: "Stadtzentr",
        7: "Stausee",
        8: "Obstanlage",
        9: "Reben",
        10: "Sumpf",
        11: "Kulturland"
    }

    with arcpy.da.UpdateCursor(clipped_fc, ["OBJVAL", "LANDCOVER_LABEL"]) as cursor:
        for val, label in cursor:
            cursor.updateRow((val, mapping.get(val, "Unbekannt")))

    # POIs kategorisieren für Analyse
    in_pois = r"C:\Users\domin\Documents\Projekt_PWRG2\data\OSM\osm_pois.gdb\pois_osm"
    out_gdb = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch\scratch.gdb"

    # Restaurants
    arcpy.conversion.FeatureClassToFeatureClass(
        in_features=in_pois,
        out_path=out_gdb,
        out_name="poi_restaurant",
        where_clause="poi_type = 'restaurant'"
    )

    # Gipfel und Aussichtspunkte
    arcpy.conversion.FeatureClassToFeatureClass(
        in_features=in_pois,
        out_path=out_gdb,
        out_name="poi_view",
        where_clause="poi_type IN ('peak', 'viewpoint')"
    )

    # Grillplätze und Trinkwasserstellen
    arcpy.conversion.FeatureClassToFeatureClass(
        in_features=in_pois,
        out_path=out_gdb,
        out_name="poi_infrastructure",
        where_clause="poi_type IN ('bbq', 'drinking_water')"
    )

    # Biotopinventare zusammenfassen
    scratch_gdb = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch\scratch.gdb"
    biotope_fcs = [
        os.path.join(scratch_gdb, "hochmoor_clip"),
        os.path.join(scratch_gdb, "flachmoor_clip"),
        os.path.join(scratch_gdb, "tww_clip"),
        os.path.join(scratch_gdb, "Auengebiete_clip"),
        os.path.join(scratch_gdb, "amphibLaichgebiet_clip"),
    ]

    out_biotope = os.path.join(scratch_gdb, "Biotopinventare")

    # Merge
    arcpy.management.Merge(biotope_fcs, out_biotope)

    out_biotope_dissolve = os.path.join(scratch_gdb, "Biotopinventare_Dissolved")

    # Dissolve
    arcpy.management.Dissolve(
        in_features=out_biotope,
        out_feature_class=out_biotope_dissolve,
        dissolve_field=None 
    )

    if arcpy.Exists(out_biotope):
        arcpy.management.Delete(out_biotope)

    # Segmentierung Wanderwegenetz # ---------------------------------------------
    input_fc = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch\scratch.gdb\main_tlm_strassen_strasse_clip"

    root = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch"
    analysis_gdb_name = "scratch_analysis.gdb"
    analysis_gdb = os.path.join(root, analysis_gdb_name)

    segment_length = 100  # Meter

    # scratch_analysis.gdb erstellen (falls nicht vorhanden)
    if not arcpy.Exists(analysis_gdb):
        arcpy.management.CreateFileGDB(
            out_folder_path=root,
            out_name=analysis_gdb_name
        )

    # Dissolve
    dissolved_fc = os.path.join(analysis_gdb, "wanderwege_dissolved")

    arcpy.management.Dissolve(
        in_features=input_fc,
        out_feature_class=dissolved_fc,
        dissolve_field=None
    )

    # Punkte entlang der Linien erzeugen
    points_fc = os.path.join(analysis_gdb, "split_points")

    arcpy.management.GeneratePointsAlongLines(
        Input_Features=dissolved_fc,
        Output_Feature_Class=points_fc,
        Point_Placement="DISTANCE",
        Distance=f"{segment_length} Meters"
    )

    # Linien an diesen Punkten splitten
    out_name = f"wanderwege_seg"
    out_fc = os.path.join(analysis_gdb, out_name)
    if arcpy.Exists(out_fc):
        arcpy.Delete_management(out_fc)

    arcpy.management.SplitLineAtPoint(
        in_features=dissolved_fc,
        point_features=points_fc,
        out_feature_class=out_fc,
        search_radius="1 Meters"
    )

    # Segmentlängen berechnen
    arcpy.management.AddGeometryAttributes(
        out_fc,
        "LENGTH"
    )

    # Eindeutige Segment-ID generieren für spätere Geoverarbeitungen
    if "SEG_ID" not in [f.name for f in arcpy.ListFields(out_fc)]:
        arcpy.AddField_management(out_fc, "SEG_ID", "LONG")

    with arcpy.da.UpdateCursor(
        out_fc,
        ["OBJECTID", "SEG_ID"]
    ) as cur:
        for oid, _ in cur:
            cur.updateRow((oid, oid))

    # Cleanup
    arcpy.management.Delete(points_fc)
    arcpy.management.Delete(dissolved_fc)

    # Analyse Vektordaten # ---------------------------------------------
    # Punktdaten
    segments_fc = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch\scratch_analysis.gdb\wanderwege_seg"
    gdb_path = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch\scratch.gdb"

    poi_sets = {
        "OeV": os.path.join(gdb_path, "Betriebspunkt_OeV_clip"),
        "REST": os.path.join(gdb_path, "poi_restaurant"),
        "VIEW": os.path.join(gdb_path, "poi_view"),
        "INFRA": os.path.join(gdb_path, "poi_infrastructure")
    }

    for key, points_fc in poi_sets.items():

        # Near
        arcpy.analysis.Near(
            in_features=segments_fc,
            near_features=points_fc,
        )

        near_field = f"NEAR_{key}"

        # Near-Distanz-Feld anlegen
        if near_field not in [f.name for f in arcpy.ListFields(segments_fc)]:
            arcpy.management.AddField(
            segments_fc,
            near_field,
            "DOUBLE"
        )

        # NEAR_DIST in eigenes Feld kopieren
        with arcpy.da.UpdateCursor(
        segments_fc,
        ["NEAR_DIST", near_field]
        ) as cursor:
            for near_dist, stored_dist in cursor:
                cursor.updateRow((near_dist, near_dist))

    # Polygondaten
    segments_fc = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch\scratch_analysis.gdb\wanderwege_seg"
    landcover_fc = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch\scratch.gdb\TLMRegio_LandCover_clip"
    scratch_gdb = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch\scratch_analysis.gdb"

    # Gesamtlänge Wanderwegnetz (Referenz)
    length_field = "LENGTH"
    total_length = 0.0
    with arcpy.da.SearchCursor(segments_fc, [length_field]) as cursor:
        total_length = sum(row[0] for row in cursor)
    print(f"Gesamtlänge Wanderwege für Perimeter {perimeter_name}: {total_length:.1f} m")

    # Ergebnistabelle erstellen
    result_table = os.path.join(scratch_gdb, "PolygonRatios")

    if arcpy.Exists(result_table):
        arcpy.management.Delete(result_table)

    arcpy.management.CreateTable(scratch_gdb, "PolygonRatios")
    arcpy.management.AddField(result_table, "Layer", "TEXT", 50)
    arcpy.management.AddField(result_table, "Category", "TEXT", 80)
    arcpy.management.AddField(result_table, "SumLength", "DOUBLE")
    arcpy.management.AddField(result_table, "Ratio", "DOUBLE")

    # Landcover
    landcover_field = "LANDCOVER_LABEL"
    landcover_values = get_unique_values(landcover_fc, landcover_field)

    for label in landcover_values:
        where = f"{landcover_field} = '{label}'"
        landcover_lyr = "landcover_lyr"
        arcpy.management.MakeFeatureLayer(landcover_fc, landcover_lyr, where)

        out_fc = os.path.join(scratch_gdb, f"lc_{label}")
        if arcpy.Exists(out_fc):
            arcpy.management.Delete(out_fc)

        arcpy.analysis.Intersect([segments_fc, landcover_lyr], out_fc)

        # Länge berechnen
        arcpy.management.AddGeometryAttributes(out_fc, "LENGTH")

        sum_len = 0.0
        with arcpy.da.SearchCursor(out_fc, ["LENGTH"]) as cursor:
            sum_len = sum(row[0] for row in cursor)

        ratio = sum_len / total_length if total_length > 0 else 0

        with arcpy.da.InsertCursor(
            result_table,
            ["Layer", "Category", "SumLength", "Ratio"]
        ) as ic:
            ic.insertRow(["Landcover", label, sum_len, ratio])
        
        # Cleanup
        arcpy.management.Delete(out_fc)

    # Biotopinventare
    biotope_fc = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch\scratch.gdb\Biotopinventare_Dissolved"
    out_fc = os.path.join(scratch_gdb, "biotope_intersect")

    # Intersect Wanderwegsegmente × Biotope
    arcpy.analysis.Intersect(
        [segments_fc, biotope_fc],
        out_fc
    )

    # Länge berechnen
    arcpy.management.AddGeometryAttributes(
        out_fc,
        "LENGTH",
        Length_Unit="METERS"
    )

    # Länge summieren
    sum_len = 0.0
    with arcpy.da.SearchCursor(out_fc, ["LENGTH"]) as cursor:
        sum_len = sum(row[0] for row in cursor)

    # Ratio berechnen
    ratio = sum_len / total_length if total_length > 0 else 0

    # In Ergebnistabelle schreiben
    with arcpy.da.InsertCursor(
        result_table,
        ["Layer", "Category", "SumLength", "Ratio"]
    ) as ic:
        ic.insertRow(["Biotopinventare", "ALL", sum_len, ratio])

    # Cleanup
    arcpy.management.Delete(out_fc)

    # Analyse Rasterdaten # ---------------------------------------------
    # Höhenintervalle
    scratch_gdb = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch\scratch_analysis.gdb"
    segments_fc = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch\scratch_analysis.gdb\wanderwege_seg"
    dhm25_raster = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch\scratch.gdb\dhm25_2056_clip"
    result_table_hoehen = os.path.join(scratch_gdb, "Hoehenanalyse")

    # Gesamtlänge Wanderwegnetz (Referenz)
    length_field = "LENGTH"
    total_length = 0.0
    with arcpy.da.SearchCursor(segments_fc, [length_field]) as cursor:
        total_length = sum(row[0] for row in cursor)

    # Höhenwert (Raster) auf Segmente übertragen 
    arcpy.ddd.AddSurfaceInformation(
        in_feature_class=segments_fc,
        in_surface=dhm25_raster,
        out_property="Z_MEAN",
        method="BILINEAR"
    )

    fields = [f.name for f in arcpy.ListFields(segments_fc)]
    if "HOEHE_MEAN" in fields:
        arcpy.management.DeleteField(segments_fc, "HOEHE_MEAN")

    arcpy.management.AlterField(
        segments_fc,
        "Z_MEAN",
        new_field_name="HOEHE_MEAN",
        new_field_alias="HOEHE_MEAN"
    )

    # Feld für Höhenklasse
    if "HOEHE_KL" not in [f.name for f in arcpy.ListFields(segments_fc)]:
        arcpy.AddField_management(segments_fc, "HOEHE_KL", "SHORT")

    with arcpy.da.UpdateCursor(
        segments_fc,
        ["HOEHE_MEAN", "HOEHE_KL"]
    ) as cursor:
        for z, kl in cursor:

            if z is None:
                kl = -1
            elif z <= 500:
                kl = 1
            elif 500 < z <= 1000:
                kl = 2
            elif 1000 < z <= 1500:
                kl = 3
            elif 1500 < z <= 2000:
                kl = 4
            elif 2000 < z <= 2500:
                kl = 5
            else:
                kl = 6   # >2500

            cursor.updateRow((z, kl))

    # Segmentlängen pro Höhenklasse summieren
    hoehen_sum = {
        1: 0.0,
        2: 0.0,
        3: 0.0,
        4: 0.0,
        5: 0.0,
        6: 0.0
    }

    with arcpy.da.SearchCursor(
        segments_fc,
        ["HOEHE_KL", "LENGTH"]
    ) as cursor:
        for kl, length in cursor:
            if kl in hoehen_sum:
                hoehen_sum[kl] += length

    # Ergebnistabelle erzeugen
    if arcpy.Exists(result_table_hoehen):
        arcpy.Delete_management(result_table_hoehen)

    arcpy.CreateTable_management(scratch_gdb, "Hoehenanalyse")
    arcpy.AddField_management(result_table_hoehen, "Hoehenklasse", "SHORT")
    arcpy.AddField_management(result_table_hoehen, "h_min", "SHORT")
    arcpy.AddField_management(result_table_hoehen, "h_max", "SHORT")
    arcpy.AddField_management(result_table_hoehen, "SumLength", "DOUBLE")
    arcpy.AddField_management(result_table_hoehen, "Ratio", "DOUBLE")

    # Ergebnisse in Tabelle schreiben
    hoehen_klassen = {
        1: (0, 500),
        2: (500, 1000),
        3: (1000, 1500),
        4: (1500, 2000),
        5: (2000, 2500),
        6: (2500, 9999)
    }

    with arcpy.da.InsertCursor(
        result_table_hoehen,
        ["Hoehenklasse", "h_min", "h_max", "SumLength", "Ratio"]
    ) as ic:

        for kl, (h_min, h_max) in hoehen_klassen.items():
            length = hoehen_sum.get(kl, 0.0)
            ratio = length / total_length if total_length > 0 else 0
            ic.insertRow([kl, h_min, h_max, length, ratio])

    # Lärm
    scratch_gdb = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch\scratch_analysis.gdb"
    segments_fc = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch\scratch_analysis.gdb\wanderwege_seg"
    laerm_raster = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch\scratch.gdb\laerm_strassenlaerm_tag_2056_clip"
    result_table_laerm = os.path.join(scratch_gdb, "Laermanalyse")

    midpoints_fc = os.path.join(scratch_gdb, "seg_midpoints")
    midpoints_fc_db = os.path.join(scratch_gdb, "seg_midpoints_db")

    for fc in [midpoints_fc, midpoints_fc_db]:
        if arcpy.Exists(fc):
            arcpy.management.Delete(fc)

    fields = [f.name for f in arcpy.ListFields(segments_fc)]
    if "RASTERVALU" in fields:
        arcpy.management.DeleteField(segments_fc, "RASTERVALU")

    arcpy.management.FeatureToPoint(
        in_features=segments_fc,
        out_feature_class=midpoints_fc,
        point_location="INSIDE"
    )

    arcpy.sa.ExtractValuesToPoints(
        in_point_features=midpoints_fc,
        in_raster=laerm_raster,
        out_point_features=midpoints_fc_db,
        interpolate_values="NONE"
    )

    arcpy.management.JoinField(
        in_data=segments_fc,
        in_field="SEG_ID",
        join_table=midpoints_fc_db,
        join_field="SEG_ID",
        fields=["RASTERVALU"]
    )

    fields = [f.name for f in arcpy.ListFields(segments_fc)]
    if "LAERM_MEAN" in fields:
        arcpy.management.DeleteField(segments_fc, "LAERM_MEAN")

    arcpy.management.AlterField(
        segments_fc,
        "RASTERVALU",
        new_field_name="LAERM_MEAN",
        new_field_alias="LAERM_MEAN"
    )

    # Feld für Lärmklasse
    if "LAERM_KL" not in [f.name for f in arcpy.ListFields(segments_fc)]:
        arcpy.AddField_management(segments_fc, "LAERM_KL", "SHORT")

    with arcpy.da.UpdateCursor(
        segments_fc,
        ["LAERM_MEAN", "LAERM_KL"]
    ) as cursor:
        for z, kl in cursor:
            if z is None:
                kl = -1
            elif 40 <= z < 50:
                kl = 1   # 40–50 dB
            elif 50 <= z < 60:
                kl = 2   # 50–60 dB
            elif z >= 60:
                kl = 3   # >60 dB
            else:
                kl = 0   # <40 dB 
            cursor.updateRow((z, kl))

    # Segmentlängen pro Lärmklasse summieren
    laerm_sum = {
        1: 0.0,  # 40–50 db
        2: 0.0,  # 50–60 db
        3: 0.0   # >60 db
    }

    with arcpy.da.SearchCursor(
        segments_fc,
        ["LAERM_KL", "LENGTH"]
    ) as cursor:
        for kl, length in cursor:
            if kl in laerm_sum:
                laerm_sum[kl] += length

    # Ergebnistabelle erzeugen
    if arcpy.Exists(result_table_laerm):
        arcpy.Delete_management(result_table_laerm)

    arcpy.CreateTable_management(scratch_gdb, "Laermanalyse")
    arcpy.AddField_management(result_table_laerm, "Laermklasse", "SHORT")
    arcpy.AddField_management(result_table_laerm, "db_min", "SHORT")
    arcpy.AddField_management(result_table_laerm, "db_max", "SHORT")
    arcpy.AddField_management(result_table_laerm, "SumLenght", "DOUBLE")
    arcpy.AddField_management(result_table_laerm, "Ratio", "DOUBLE")

    # Ergebnisse in Tabelle schreiben
    laerm_klassen = {
        1: (40, 50),
        2: (50, 60),
        3: (60, 999)
    }

    with arcpy.da.InsertCursor(
        result_table_laerm,
        ["Laermklasse", "db_min", "db_max", "SumLenght", "Ratio"]
    ) as ic:
        for kl, (db_min, db_max) in laerm_klassen.items():
            length = laerm_sum.get(kl, 0.0)
            ratio = length / total_length if total_length > 0 else 0
            ic.insertRow([kl, db_min, db_max, length, ratio])

    # Cleanup
    arcpy.management.Delete(midpoints_fc)
    arcpy.management.Delete(midpoints_fc_db)

    # Analyseabschluss # ---------------------------------------------
    # Name der Output-GDB
    output_gdb_name = f"{perimeter_name}.gdb"

    output_folder = r"C:\Users\domin\Documents\Projekt_PWRG2\output\Py"
    output_gdb = os.path.join(output_folder, output_gdb_name)

    # Pfade zu den Scratch-GDBs
    scratch_analysis_gdb = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch\scratch_analysis.gdb"
    scratch_gdb = r"C:\Users\domin\Documents\Projekt_PWRG2\scratch\scratch.gdb"

    # Output-Ordner erstellen, falls er nicht existiert
    if not os.path.exists(output_folder):
        os.makedirs(output_folder)

    # Kopiere scratch_analysis.gdb nach Output
    if arcpy.Exists(output_gdb):
        arcpy.Delete_management(output_gdb)

    arcpy.Copy_management(scratch_analysis_gdb, output_gdb)
    print(f"GDB-Kopie erstellt: {output_gdb}")

    # Feature Class → Table (keine Geometrie)
    table_bez = "wanderwege_seg_tbl"
    out_name = table_bez
    out_path = output_gdb 

    arcpy.conversion.TableToTable(
        in_rows=segments_fc,
        out_path=out_path,
        out_name=out_name
    )

    # Perimeter umbenennen
    input_shp = r"C:\Users\domin\Documents\Projekt_PWRG2\output\Perimeter\perimeter.shp"
    output_folder = r"C:\Users\domin\Documents\Projekt_PWRG2\output\Perimeter"

    # Shapefile-Name aus GDB-Name ableiten
    output_name = os.path.splitext(output_gdb_name)[0] 
    output_shp = os.path.join(output_folder, f"{output_name}.shp")

    # Ausgabe-Ordner erstellen, falls er nicht existiert
    if not os.path.exists(output_folder):
        os.makedirs(output_folder)

    # Shapefile kopieren
    if arcpy.Exists(input_shp):
        arcpy.CopyFeatures_management(input_shp, output_shp)

    # Altes Shapefile löschen
    if arcpy.Exists(input_shp):
        arcpy.Delete_management(input_shp)


# Ausführung Hauptfunktion mit LOOP durch Perimeterliste
for perimeter_name, perimeter_fc in perimeters:
    run_analysis(perimeter_name, perimeter_fc)

Skript R

Code
# Vollständiges R-Skript zur Auswertung und Ergebnisvisualisierung Wanderweg-Analyse der regionalen Pärke der Schweiz 

# Dominik Erni
# PWRG2

library(sf)
library(dplyr)
library(purrr)
library(ggplot2)
library(tidyr)


# Funktionen -------------------
read_table_with_perimeter <- function(gdb, layer_name) {
  perimeter <- tools::file_path_sans_ext(basename(gdb))
  st_read(gdb, layer = layer_name, quiet = TRUE) |>
    st_drop_geometry() |>
    mutate(Perimeter = perimeter)
}

read_near_medians <- function(gdb, layer_name) {
  
  perimeter <- tools::file_path_sans_ext(basename(gdb))
  
  st_read(gdb, layer = layer_name, quiet = TRUE) |>
    st_drop_geometry() |>
    select(NEAR_OeV, NEAR_REST, NEAR_VIEW, NEAR_INFRA) |>
    pivot_longer(
      cols = everything(),
      names_to = "Kategorie",
      values_to = "Distanz"
    ) |>
    summarise(
      Median = median(Distanz, na.rm = TRUE),
      .by = Kategorie
    ) |>
    mutate(Perimeter = perimeter)
}


# Aufrufen der GDB -----------------
gdb_path <- "C:/Users/domin/Documents/Projekt_PWRG2/output/Py"

gdbs <- list.files(
  gdb_path,
  pattern = "\\.gdb$",
  full.names = TRUE
)


# Tabelle PolygonRatios --------------------
PolyRatios_all <- map_dfr( # purrr: Integriert hier Foor-Loop + Zusammenstellung (BindRows).
  gdbs,
  read_table_with_perimeter,
  layer_name = "PolygonRatios"
)

PolyRatios_biotope <- PolyRatios_all |>
  filter(Layer == "Biotopinventare")
PolyRatios_landcover <- PolyRatios_all |>
  filter(Layer == "Landcover")


ggplot(
  PolyRatios_biotope,
  aes(x = Ratio, y = Perimeter)
) +
  geom_col(fill = "skyblue") +
  theme_minimal() +
  labs(
    title = "Biotopinventaranalyse",
    x = "Verhältnis zum gesamten Wanderwegenetz\nim Perimeter",
    y = "Perimeter"
  ) +
  scale_x_continuous(
    expand = expansion(mult = c(0, 0.05))
  )


ggplot(
  PolyRatios_landcover,
  aes(x = Category, y = Perimeter, fill = Ratio)
) +
  geom_tile(color = "white", linewidth = 0.3) +
  scale_fill_gradient(
    low = "skyblue",
    high = "red",
    name = "Verhältnis zum\ngesamten Wanderwegenetz\nim Perimeter"
  ) +
  theme_minimal() +
  theme(
    axis.text.x = element_text(angle = 30, hjust = 1),
    panel.grid = element_blank()
  ) +
  labs(
    title = "Landcoveranalyse",
    x = "Klasse",
    y = "Perimeter"
  )


# Tabelle Hoehen --------------------
HoehenRatios_all <- map_dfr(
  gdbs,
  read_table_with_perimeter,
  layer_name = "Hoehenanalyse"
)

HoehenRatios_all <- HoehenRatios_all |>
  mutate(Hoehenklasse = factor(Hoehenklasse, levels = sort(unique(Hoehenklasse))))

ggplot(
  HoehenRatios_all,
  aes(x = Hoehenklasse, y = Perimeter, fill = Ratio)
) +
  geom_tile(color = "white", linewidth = 0.3) +
  scale_fill_gradient(
    low = "skyblue",
    high = "red",
    name = "Verhältnis zum\ngesamten Wanderwegenetz\nim Perimeter"
  ) +
  theme_minimal() +
  theme(
    axis.text.x = element_text(angle = 0, hjust = 1),
    panel.grid = element_blank()
  ) +
  labs(
    title = "Höhenstufenanalyse",
    x = "Höhenstufe",
    y = "Perimeter"
  )




# Tabelle Laerm ---------------------
LaermRatios_all <- map_dfr(
  gdbs,
  read_table_with_perimeter,
  layer_name = "Laermanalyse"
)

ggplot(
  LaermRatios_all,
  aes(x = Laermklasse, y = Perimeter, fill = Ratio)
) +
  geom_tile(color = "white", linewidth = 0.3) +
  scale_fill_gradient(
    low = "skyblue",
    high = "red",
    name = "Verhältnis zum\ngesamten Wanderwegenetz\nim Perimeter"
  ) +
  theme_minimal() +
  theme(
    axis.text.x = element_text(angle = 0, hjust = 1),
    panel.grid = element_blank()
  ) +
  labs(
    title = "Strassenlärmanalyse",
    x = "Lärmstufe",
    y = "Perimeter"
  )




# Tabelle Segmente Wanderwege (inkl. Medianberechnungen in R) ----------------
Near_medians_all <- map_dfr(
  gdbs,
  read_near_medians,
  layer_name = "wanderwege_seg_tbl"
)

Near_medians_all <- Near_medians_all |>
  mutate(
    Kategorie = factor(
      Kategorie,
      levels = c(
        "NEAR_OeV",
        "NEAR_REST",
        "NEAR_INFRA",
        "NEAR_VIEW"
      ),
      labels = c(
        "OeV-Haltestellen",
        "Gastronomie",
        "Freizeitinfrastruktur",
        "Aussichtspunkten"
      )
    )
  )

ggplot(
  Near_medians_all,
  aes(x = Kategorie, y = Perimeter, fill = Median)
) +
  geom_tile(color = "white", linewidth = 0.3) +
  scale_fill_gradient(
    low = "skyblue",
    high = "red",
    name = "Median-Distanz (m)"
  ) +
  theme_minimal() +
  theme(
    axis.text.x = element_text(angle = 30, hjust = 1),
    panel.grid = element_blank()
  ) +
  labs(
    title = "Distanzanalyse zu den POI's",
    x = "POI Kategorie",
    y = "Perimeter"
  )

Literatur

ESRI. o. J. ArcGIS Pro Geoprocessing Tool Reference—ArcGIS Pro Documentation. https://pro.arcgis.com/en/pro-app/latest/tool-reference/main/arcgis-pro-tool-reference.htm. Zugegriffen 11. Januar 2026.
OpenAI. 2025. ChatGPT (GPT-5.1) [Large Language Model]“. https://chat.openai.com/.
Scheidegger, Carlos, Gordon Woodhull, Christophe Dervieux, Charles Teague, und J. J. Allaire. o. J. „Quarto Guide. Quarto. https://quarto.org/docs/guide/. Zugegriffen 19. Januar 2026.
Tateosian, Laura. 2015. Python for ArcGIS. 1. Aufl. Springer Cham.
Theis, Thomas. 2022. Einstieg in python : Ideal für Programmiereinsteiger. 7th ed. Bonn: Rheinwerk Verlag.