Commit af8f59dd authored by Eric Duminil's avatar Eric Duminil
Browse files

Claude refactor

parent 608f73c0
Showing with 127 additions and 66 deletions
+127 -66
...@@ -4,16 +4,23 @@ LoD2 CityGML tiles are available for whole Baden-Württemberg, from LGL. ...@@ -4,16 +4,23 @@ LoD2 CityGML tiles are available for whole Baden-Württemberg, from LGL.
https://opengeodata.lgl-bw.de/#/(sidenav:product/12) https://opengeodata.lgl-bw.de/#/(sidenav:product/12)
This script downloads the requires tiles for given regions This script downloads the required tiles for given regions
(as WKT strings, Zipcode or Zipcodes, in *REGIONS* variable), and extracts the region. (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: Required:
* Python * Python
* pyproj project (https://pypi.org/project/pyproj/) * 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 Eric Duminil, 2025
""" """
import argparse
from pathlib import Path from pathlib import Path
from math import floor from math import floor
import subprocess import subprocess
...@@ -21,6 +28,8 @@ import re ...@@ -21,6 +28,8 @@ import re
import urllib.request import urllib.request
import time import time
import zipfile import zipfile
import logging
import sys
from pyproj import CRS from pyproj import CRS
from pyproj import Transformer from pyproj import Transformer
...@@ -29,29 +38,23 @@ from shapely import wkt ...@@ -29,29 +38,23 @@ from shapely import wkt
from shapely.ops import transform from shapely.ops import transform
from shapely.geometry import Point from shapely.geometry import Point
# TODO: Write tests
# TODO: Use logging
from get_coordinates_by_zipcode import get_coordinates_by_zipcode from get_coordinates_by_zipcode import get_coordinates_by_zipcode
COORDINATES_REGEX = re.compile(r"(\-?\d+\.\d*) (\-?\d+\.\d*)") # Setup logging
logging.basicConfig(
###### User input ########## level=logging.INFO,
# Values can be either a WKT POLYGON or MULTIPOLYGON, a Zipcode, or Zipcodes separated by a comma. format='%(asctime)s - %(levelname)s - %(message)s',
REGIONS = { handlers=[
"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))", logging.StreamHandler(sys.stdout)
# "Freiburg": "79098,79102", ]
# "AnotherRegion": "Another WKT Polygon...", )
# "YetAnotherRegion": "Another ZIP code", logger = logging.getLogger(__name__)
}
# Should RegionChooser extract the regions from multiple CityGMLs?
EXTRACT_REGIONS = True
############################
COORDINATES_REGEX = re.compile(r"(\-?\d+\.\d*) (\-?\d+\.\d*)")
CITYGML_SERVER = "https://opengeodata.lgl-bw.de/data/lod2" CITYGML_SERVER = "https://opengeodata.lgl-bw.de/data/lod2"
RASTER = 2 # [km] RASTER = 2 # [km]
KILOMETER = 1000 # [m] KILOMETER = 1000 # [m]
BUNDESLAND = 'bw' BUNDESLAND = 'bw'
# UTM32N, used in BW. https://epsg.io/32632 # UTM32N, used in BW. https://epsg.io/32632
...@@ -65,16 +68,15 @@ SCRIPT_DIR = Path(__file__).parent ...@@ -65,16 +68,15 @@ SCRIPT_DIR = Path(__file__).parent
WAIT_BETWEEN_DOWNLOADS = 5 # [s] Be nice to LGL Server. WAIT_BETWEEN_DOWNLOADS = 5 # [s] Be nice to LGL Server.
GML_GLOB = "LoD2_*/LoD2_*.gml" GML_GLOB = "LoD2_*/LoD2_*.gml"
if EXTRACT_REGIONS:
def find_simstadt_folder():
"""Find SimStadt installation on desktop"""
try: try:
SIMSTADT_FOLDER = next(x for x in Path.home().glob('Desktop/SimStadt*_0.*/') if x.is_dir()) 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}") logger.info(f"RegionChooser has been found in {simstadt_folder}")
return simstadt_folder
except StopIteration: except StopIteration:
exit("No SimStadt installation found!" return None
"\nPlease copy a SimStadt installation to the desktop,"
"\nset EXTRACT_REGIONS to False,"
"\nor set SIMSTADT_FOLDER manually: SIMSTADT_FOLDER = Path('/path/to/SimStadt')"
)
def coordinates_to_grid(longitude: float, latitude: float) -> tuple[int, int]: 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]: ...@@ -87,12 +89,12 @@ def coordinates_to_grid(longitude: float, latitude: float) -> tuple[int, int]:
return (x + 1, y) 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.""" """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") 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]] lons = [float(lon) for lon in coordinates[::2]]
lats = [float(lat) for lat in coordinates[1::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, ...@@ -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_lon, max_lon = min(lons), max(lons)
min_lat, max_lat = min(lats), max(lats) min_lat, max_lat = min(lats), max(lats)
print("%s (%.3f°N %.3f°E -> %.3f°N %.3f°E)" % logger.info("%s (%.3f°N %.3f°E -> %.3f°N %.3f°E)" %
(location_name, max_lat, min_lon, min_lat, max_lon)) (location_name, max_lat, min_lon, min_lat, max_lon))
x1, y1 = coordinates_to_grid(min_lon, min_lat) x1, y1 = coordinates_to_grid(min_lon, min_lat)
x2, y2 = coordinates_to_grid(max_lon, max_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 ...@@ -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}" citygml_url = f"{CITYGML_SERVER}/{citygml_zip}"
local_zip = output_dir / citygml_zip local_zip = output_dir / citygml_zip
if local_zip.exists(): 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: else:
print(f" Download {citygml_zip} to {output_dir.name}/ ", end='') logger.info(f" Download {citygml_zip} to {output_dir.name}/ ")
try: try:
urllib.request.urlretrieve(citygml_url, local_zip) urllib.request.urlretrieve(citygml_url, local_zip)
logger.info("✅ Download successful")
except urllib.error.HTTPError as e: except urllib.error.HTTPError as e:
print(f"❌ {e}") logger.error(f"❌ {e}")
continue continue
finally: finally:
time.sleep(WAIT_BETWEEN_DOWNLOADS) time.sleep(WAIT_BETWEEN_DOWNLOADS)
print("✅")
print(f" Extract {citygml_zip} to {output_dir.name}/ ", end='') logger.info(f" Extract {citygml_zip} to {output_dir.name}/ ")
print("✅")
print("")
with zipfile.ZipFile(local_zip, "r") as zip_ref: with zipfile.ZipFile(local_zip, "r") as zip_ref:
zip_ref.extractall(output_dir) 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.""" """Uses RegionChooser to extract a given region from all the CityGML files found in subfolder."""
output_file = output_dir / (location_name + '.gml') output_file = output_dir / (location_name + '.gml')
if output_file.exists(): if output_file.exists():
print(f" {output_file} already exists. Not extracting.") logger.info(f" {output_file} already exists. Not extracting.")
return return
region_chooser_libs = Path(SIMSTADT_FOLDER).expanduser() / 'lib/*'
region_chooser_libs = simstadt_folder / 'lib/*'
gml_inputs = list(output_dir.glob(GML_GLOB)) gml_inputs = list(output_dir.glob(GML_GLOB))
if len(gml_inputs) == 0: 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 return
params_path = output_dir / 'params.txt' params_path = output_dir / 'params.txt'
wkt_path = output_dir / 'region.wkt' 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: with open(wkt_path, 'w') as f:
f.write(local_wkt) f.write(local_wkt)
...@@ -181,11 +184,11 @@ def extract_region(output_dir: Path, location_name: str, wkt: str) -> None: ...@@ -181,11 +184,11 @@ def extract_region(output_dir: Path, location_name: str, wkt: str) -> None:
capture_output=True, capture_output=True,
check=False check=False
) )
if (result.stderr): if result.stderr:
print(result.stderr) logger.error(result.stderr)
if result.returncode != 0: if result.returncode != 0:
raise ValueError(f"RegionChooser failed with code {result.returncode}") raise ValueError(f"RegionChooser failed with code {result.returncode}")
print(" DONE!") logger.info(" DONE!")
def get_wkt(wkt_or_zipcode: str) -> str: def get_wkt(wkt_or_zipcode: str) -> str:
...@@ -200,30 +203,88 @@ 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(',')) 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): def convert_coordinates(match):
"""Convert WGS84 coordinates to UTM32N"""
longitude, latitude = match.groups() longitude, latitude = match.groups()
x, y = TO_LOCAL_CRS.transform(longitude, latitude) x, y = TO_LOCAL_CRS.transform(longitude, latitude)
return f"{x} {y}" return f"{x} {y}"
def convert_wkt_to_local(wkt): def convert_wkt_to_local(wkt_str):
return COORDINATES_REGEX.sub(convert_coordinates, wkt) """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__': if __name__ == '__main__':
main(REGIONS) main()
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment