diff --git a/download_files_from_LGL_BW.py b/download_files_from_LGL_BW.py index 66faabca47b7c6124396ec75b0d8305532838120..6971dfc769a3f2484200a4157c5fa69137e3df3a 100644 --- a/download_files_from_LGL_BW.py +++ b/download_files_from_LGL_BW.py @@ -4,16 +4,23 @@ LoD2 CityGML tiles are available for whole Baden-Württemberg, from LGL. https://opengeodata.lgl-bw.de/#/(sidenav:product/12) -This script downloads the requires tiles for given regions -(as WKT strings, Zipcode or Zipcodes, in *REGIONS* variable), and extracts the region. +This script downloads the required tiles for given regions +(as WKT strings, Zipcode or Zipcodes), and extracts the region. + +Usage: + python download_files_from_LGL_BW.py StuttgartCenter "POLYGON((9.175287 48.780916, 9.185501 48.777522, 9.181467 48.773704, 9.174429 48.768472, 9.168807 48.773902, 9.175287 48.780916))" + python download_files_from_LGL_BW.py Freiburg "79098,79102" + python download_files_from_LGL_BW.py MyRegion "70567" --download-only + python download_files_from_LGL_BW.py CustomPath "POLYGON(...)" --simstadt-folder "/path/to/SimStadt" Required: * Python * pyproj project (https://pypi.org/project/pyproj/) -* SimStadt installed on the Desktop (for RegionChooser) +* SimStadt installed on the Desktop (for RegionChooser) if extracting regions Eric Duminil, 2025 """ +import argparse from pathlib import Path from math import floor import subprocess @@ -21,6 +28,8 @@ import re import urllib.request import time import zipfile +import logging +import sys from pyproj import CRS from pyproj import Transformer @@ -29,29 +38,23 @@ from shapely import wkt from shapely.ops import transform from shapely.geometry import Point -# TODO: Write tests -# TODO: Use logging - from get_coordinates_by_zipcode import get_coordinates_by_zipcode -COORDINATES_REGEX = re.compile(r"(\-?\d+\.\d*) (\-?\d+\.\d*)") - -###### User input ########## -# Values can be either a WKT POLYGON or MULTIPOLYGON, a Zipcode, or Zipcodes separated by a comma. -REGIONS = { - "StuttgartCenter": "POLYGON((9.175287 48.780916, 9.185501 48.777522, 9.181467 48.773704, 9.174429 48.768472, 9.168807 48.773902, 9.175287 48.780916))", - # "Freiburg": "79098,79102", - # "AnotherRegion": "Another WKT Polygon...", - # "YetAnotherRegion": "Another ZIP code", -} -# Should RegionChooser extract the regions from multiple CityGMLs? -EXTRACT_REGIONS = True -############################ +# Setup logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.StreamHandler(sys.stdout) + ] +) +logger = logging.getLogger(__name__) +COORDINATES_REGEX = re.compile(r"(\-?\d+\.\d*) (\-?\d+\.\d*)") CITYGML_SERVER = "https://opengeodata.lgl-bw.de/data/lod2" RASTER = 2 # [km] -KILOMETER = 1000 # [m] +KILOMETER = 1000 # [m] BUNDESLAND = 'bw' # UTM32N, used in BW. https://epsg.io/32632 @@ -65,16 +68,15 @@ SCRIPT_DIR = Path(__file__).parent WAIT_BETWEEN_DOWNLOADS = 5 # [s] Be nice to LGL Server. GML_GLOB = "LoD2_*/LoD2_*.gml" -if EXTRACT_REGIONS: + +def find_simstadt_folder(): + """Find SimStadt installation on desktop""" try: - SIMSTADT_FOLDER = next(x for x in Path.home().glob('Desktop/SimStadt*_0.*/') if x.is_dir()) - print(f"RegionChooser has been found in {SIMSTADT_FOLDER}") + simstadt_folder = next(x for x in Path.home().glob('Desktop/SimStadt*_0.*/') if x.is_dir()) + logger.info(f"RegionChooser has been found in {simstadt_folder}") + return simstadt_folder except StopIteration: - exit("No SimStadt installation found!" - "\nPlease copy a SimStadt installation to the desktop," - "\nset EXTRACT_REGIONS to False," - "\nor set SIMSTADT_FOLDER manually: SIMSTADT_FOLDER = Path('/path/to/SimStadt')" - ) + return None def coordinates_to_grid(longitude: float, latitude: float) -> tuple[int, int]: @@ -87,12 +89,12 @@ def coordinates_to_grid(longitude: float, latitude: float) -> tuple[int, int]: return (x + 1, y) -def wkt_polygon_to_grid_coords(location_name: str, wkt: str) -> tuple[int, int, int, int]: +def wkt_polygon_to_grid_coords(location_name: str, wkt_str: str) -> tuple[int, int, int, int]: """Returns (x, y) of lower-left and bottom-right tiles, containing a given region.""" - if 'POLYGON' not in wkt: + if 'POLYGON' not in wkt_str: raise ValueError(f"wkt for {location_name} should be a WKT POLYGON or MULTIPOLYGON") - coordinates = re.findall(r'\-?\d+\.\d+', wkt) + coordinates = re.findall(r'\-?\d+\.\d+', wkt_str) lons = [float(lon) for lon in coordinates[::2]] lats = [float(lat) for lat in coordinates[1::2]] @@ -100,8 +102,8 @@ def wkt_polygon_to_grid_coords(location_name: str, wkt: str) -> tuple[int, int, min_lon, max_lon = min(lons), max(lons) min_lat, max_lat = min(lats), max(lats) - print("%s (%.3f°N %.3f°E -> %.3f°N %.3f°E)" % - (location_name, max_lat, min_lon, min_lat, max_lon)) + logger.info("%s (%.3f°N %.3f°E -> %.3f°N %.3f°E)" % + (location_name, max_lat, min_lon, min_lat, max_lon)) x1, y1 = coordinates_to_grid(min_lon, min_lat) x2, y2 = coordinates_to_grid(max_lon, max_lat) @@ -123,43 +125,44 @@ def download_whole_region(output_dir: Path, wkt_region: str, x1: int, x2: int, y citygml_url = f"{CITYGML_SERVER}/{citygml_zip}" local_zip = output_dir / citygml_zip if local_zip.exists(): - print(f" {local_zip.name} already in {output_dir.name}/") + logger.info(f" {local_zip.name} already in {output_dir.name}/") else: - print(f" Download {citygml_zip} to {output_dir.name}/ ", end='') + logger.info(f" Download {citygml_zip} to {output_dir.name}/ ") try: urllib.request.urlretrieve(citygml_url, local_zip) + logger.info("✅ Download successful") except urllib.error.HTTPError as e: - print(f"⌠{e}") + logger.error(f"⌠{e}") continue finally: time.sleep(WAIT_BETWEEN_DOWNLOADS) - print("✅") - print(f" Extract {citygml_zip} to {output_dir.name}/ ", end='') - print("✅") - print("") + logger.info(f" Extract {citygml_zip} to {output_dir.name}/ ") with zipfile.ZipFile(local_zip, "r") as zip_ref: zip_ref.extractall(output_dir) + logger.info("✅ Extraction successful") -def extract_region(output_dir: Path, location_name: str, wkt: str) -> None: +def extract_region(output_dir: Path, location_name: str, wkt_str: str, simstadt_folder: Path) -> None: """Uses RegionChooser to extract a given region from all the CityGML files found in subfolder.""" output_file = output_dir / (location_name + '.gml') if output_file.exists(): - print(f" {output_file} already exists. Not extracting.") + logger.info(f" {output_file} already exists. Not extracting.") return - region_chooser_libs = Path(SIMSTADT_FOLDER).expanduser() / 'lib/*' + + region_chooser_libs = simstadt_folder / 'lib/*' gml_inputs = list(output_dir.glob(GML_GLOB)) if len(gml_inputs) == 0: - print("Error: No CityGML found. At least part of the region should be in Baden-Württemberg!") + logger.error( + "Error: No CityGML found. At least part of the region should be in Baden-Württemberg!") return params_path = output_dir / 'params.txt' wkt_path = output_dir / 'region.wkt' - local_wkt = convert_wkt_to_local(wkt) + local_wkt = convert_wkt_to_local(wkt_str) - print(f" Extracting {output_file}.") + logger.info(f" Extracting {output_file}.") with open(wkt_path, 'w') as f: f.write(local_wkt) @@ -181,11 +184,11 @@ def extract_region(output_dir: Path, location_name: str, wkt: str) -> None: capture_output=True, check=False ) - if (result.stderr): - print(result.stderr) + if result.stderr: + logger.error(result.stderr) if result.returncode != 0: raise ValueError(f"RegionChooser failed with code {result.returncode}") - print(" DONE!") + logger.info(" DONE!") def get_wkt(wkt_or_zipcode: str) -> str: @@ -200,30 +203,88 @@ def get_wkt(wkt_or_zipcode: str) -> str: return get_coordinates_by_zipcode(wkt_or_zipcode.split(',')) -def main(regions: dict[str, str]) -> None: - """Downloads ZIP files, extracts CityGML files, and selects desired region.""" - for location_name, wkt_or_zipcode in regions.items(): - if ' ' in location_name: - raise ValueError("Location name should not contain spaces: 'Some City' -> 'SomeCity'") - output_dir = SCRIPT_DIR / (location_name + '.proj') - output_dir.mkdir(parents=True, exist_ok=True) - wkt = get_wkt(wkt_or_zipcode) - x1, x2, y1, y2 = wkt_polygon_to_grid_coords(location_name, wkt) - download_whole_region(output_dir, wkt, x1, x2, y1, y2) - if EXTRACT_REGIONS: - extract_region(output_dir, location_name, wkt) - print() - def convert_coordinates(match): + """Convert WGS84 coordinates to UTM32N""" longitude, latitude = match.groups() x, y = TO_LOCAL_CRS.transform(longitude, latitude) return f"{x} {y}" -def convert_wkt_to_local(wkt): - return COORDINATES_REGEX.sub(convert_coordinates, wkt) +def convert_wkt_to_local(wkt_str): + """Convert WKT from WGS84 to UTM32N""" + return COORDINATES_REGEX.sub(convert_coordinates, wkt_str) + + +def parse_arguments(): + """Parse command line arguments""" + parser = argparse.ArgumentParser( + description="Download LoD2 CityGML tiles from LGL Baden-Württemberg and extract specific regions", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + python download_files_from_LGL_BW.py StuttgartCenter "POLYGON((9.175287 48.780916, 9.185501 48.777522, 9.181467 48.773704, 9.174429 48.768472, 9.168807 48.773902, 9.175287 48.780916))" + python download_files_from_LGL_BW.py Freiburg "79098,79102" + python download_files_from_LGL_BW.py MyRegion "70567" --download-only +""" + ) + + parser.add_argument('name', type=str, + help='Name of the region (no spaces allowed). Output files will use this name.') + + parser.add_argument('region', type=str, + help='Region specification as WKT POLYGON/MULTIPOLYGON string or zipcode(s) (comma-separated).') + + parser.add_argument('--download-only', action='store_true', + help='Only download files without extracting the region (default: False).') + + parser.add_argument('--simstadt-folder', type=Path, default=None, + help='Path to SimStadt installation folder. By default, tries to find it on the Desktop.') + + return parser.parse_args() + + +def main(): + """Main function to process arguments and run the download/extraction""" + args = parse_arguments() + + location_name = args.name + wkt_or_zipcode = args.region + download_only = args.download_only + simstadt_folder = args.simstadt_folder + + # Validate location name + if ' ' in location_name: + raise ValueError("Location name should not contain spaces: 'Some City' -> 'SomeCity'") + + # Create output directory + output_dir = SCRIPT_DIR / (location_name + '.proj') + output_dir.mkdir(parents=True, exist_ok=True) + + # Get WKT string + wkt_str = get_wkt(wkt_or_zipcode) + + # Get grid coordinates + x1, x2, y1, y2 = wkt_polygon_to_grid_coords(location_name, wkt_str) + + # Download region + download_whole_region(output_dir, wkt_str, x1, x2, y1, y2) + + # Extract region if not download-only + if not download_only: + if not simstadt_folder: + simstadt_folder = find_simstadt_folder() + if not simstadt_folder: + logger.error( + "No SimStadt installation found! Please provide --simstadt-folder or use --download-only.") + return + + extract_region(output_dir, location_name, wkt_str, simstadt_folder) + else: + logger.info("Download-only mode: Skipping region extraction.") + + logger.info(f"Processing of {location_name} complete!") if __name__ == '__main__': - main(REGIONS) + main()