Source code for districtheatingsim.osm.area_selection

"""
OSM area-selection helpers (GUI-free).
======================================

Pure geometry helpers shared by the OSM street and building download dialogs:
resolve a user-selected area (city name, buildings CSV, GeoJSON polygon, or a
polygon drawn on the map) into a WGS84 shapely geometry, and build the OSMnx
highway filter string.

These were extracted from the two near-identical worker methods in
``gui/LeafletTab/osm_dialogs.py`` so the logic lives in one tested place and so the
download threads no longer touch the GUI: a missing CSV column now raises
``ValueError`` (caught by the download thread → error signal) instead of calling
``QMessageBox`` from a worker thread.

:author: Dipl.-Ing. (FH) Jonas Pfeiffer
"""

# Rough metres-per-degree near central Europe, used to convert a metric buffer to
# the degree buffer applied in WGS84. Matches the original dialog code.
_METERS_PER_DEGREE = 111000.0

# Area-type labels (must match the dialog combo box entries).
AREA_CITY = "Stadt/Ortsname"
AREA_CSV = "Bereich um Gebäude aus CSV"
AREA_GEOJSON = "Polygon aus GeoJSON"
AREA_DRAWN = "Polygon auf Karte zeichnen"


[docs] def build_highway_filter(selected_types) -> str: """ Build an OSMnx ``custom_filter`` string from selected highway types. :param selected_types: Highway type keys (e.g. ``["primary", "residential"]``). :type selected_types: list[str] :return: OSMnx filter, e.g. ``'["highway"~"primary|residential"]'``; with no types selected, the unrestricted ``'["highway"]'``. :rtype: str """ if selected_types: types_str = "|".join(selected_types) return f'["highway"~"{types_str}"]' return '["highway"]'
[docs] def polygon_from_csv(csv_file, project_crs, buffer_m): """ Build a buffered WGS84 polygon around building points from a CSV file. :param csv_file: Path to a ``;``-delimited CSV with ``UTM_X``/``UTM_Y`` columns. :type csv_file: str :param project_crs: CRS of the CSV coordinates (e.g. ``"EPSG:25833"``). :type project_crs: str :param buffer_m: Buffer radius around the points, in metres. :type buffer_m: float :return: WGS84 (EPSG:4326) polygon covering the buffered points. :rtype: shapely.geometry.base.BaseGeometry :raises ValueError: If the CSV lacks ``UTM_X``/``UTM_Y`` columns. """ import geopandas as gpd import pandas as pd df = pd.read_csv(csv_file, delimiter=";") if "UTM_X" not in df.columns or "UTM_Y" not in df.columns: raise ValueError("CSV muss 'UTM_X' und 'UTM_Y' Spalten enthalten.") geometry = gpd.points_from_xy(df["UTM_X"], df["UTM_Y"]) gdf = gpd.GeoDataFrame(df, geometry=geometry, crs=project_crs) # Convert to WGS84 and buffer in degrees (approximate, matches original code). gdf_wgs84 = gdf.to_crs("EPSG:4326") buffer_deg = buffer_m / _METERS_PER_DEGREE return gdf_wgs84.unary_union.buffer(buffer_deg)
[docs] def polygon_from_geojson(geojson_file): """ Read a polygon GeoJSON file and return it as a single WGS84 geometry. :param geojson_file: Path to a GeoJSON file containing one or more polygons. :type geojson_file: str :return: WGS84 (EPSG:4326) union of the file's geometries. :rtype: shapely.geometry.base.BaseGeometry """ import geopandas as gpd gdf = gpd.read_file(geojson_file) if gdf.crs != "EPSG:4326": gdf = gdf.to_crs("EPSG:4326") return gdf.unary_union
[docs] def resolve_area_polygon(area_params, buffer_m): """ Resolve an area-selection parameter dict to a WGS84 polygon. Dispatches on ``area_params['area_type']``: a buildings CSV (buffered by ``buffer_m``), a GeoJSON polygon file, or a polygon drawn on the map. The city-name area type produces no polygon (the caller downloads by place name). :param area_params: Area-selection parameters with at least ``area_type`` and the file path for the selected type (``csv_file`` / ``polygon_file`` / ``drawn_polygon_file``) plus ``project_crs`` for CSV. :type area_params: dict :param buffer_m: Buffer radius (metres) applied for the CSV area type. :type buffer_m: float :return: WGS84 polygon for the selected area. :rtype: shapely.geometry.base.BaseGeometry :raises ValueError: For the city-name or an unknown area type (no polygon). """ area_type = area_params["area_type"] if area_type == AREA_CSV: return polygon_from_csv( area_params["csv_file"], area_params.get("project_crs", "EPSG:25833"), buffer_m, ) if area_type == AREA_GEOJSON: return polygon_from_geojson(area_params["polygon_file"]) if area_type == AREA_DRAWN: return polygon_from_geojson(area_params["drawn_polygon_file"]) raise ValueError(f"Kein Polygon für Bereichstyp: {area_type}")