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

Formatting

parent 1a46804f
...@@ -20,29 +20,28 @@ Required: ...@@ -20,29 +20,28 @@ Required:
Eric Duminil, 2025 Eric Duminil, 2025
""" """
import argparse import argparse
from pathlib import Path import logging
from math import floor
import subprocess
import re import re
import urllib.request import subprocess
import time import time
import urllib.request
import zipfile import zipfile
import logging from math import floor
from pathlib import Path
from pyproj import CRS
from pyproj import Transformer
from pyproj import CRS, Transformer
from shapely import wkt from shapely import wkt
from shapely.ops import transform
from shapely.geometry import Point from shapely.geometry import Point
from shapely.ops import transform
from get_coordinates_by_zipcode import get_coordinates_by_zipcode from get_coordinates_by_zipcode import get_coordinates_by_zipcode
# Setup logging # Setup logging
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=logging.INFO,
format='%(levelname)s - %(message)s', format="%(levelname)s - %(message)s",
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -51,12 +50,10 @@ COORDINATES_REGEX = re.compile(r"(\-?\d+\.\d*) (\-?\d+\.\d*)") ...@@ -51,12 +50,10 @@ 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
TO_LOCAL_CRS = Transformer.from_crs(CRS.from_epsg(4326), TO_LOCAL_CRS = Transformer.from_crs(CRS.from_epsg(4326), CRS.from_epsg(32632), always_xy=True)
CRS.from_epsg(32632),
always_xy=True)
UTM = 32 UTM = 32
...@@ -68,7 +65,7 @@ GML_GLOB = "LoD2_*/LoD2_*.gml" ...@@ -68,7 +65,7 @@ GML_GLOB = "LoD2_*/LoD2_*.gml"
def find_simstadt_folder(): def find_simstadt_folder():
"""Find SimStadt installation on desktop""" """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())
logger.info("RegionChooser has been found in %s", simstadt_folder) logger.info("RegionChooser has been found in %s", simstadt_folder)
return simstadt_folder return simstadt_folder
except StopIteration: except StopIteration:
...@@ -87,10 +84,10 @@ def coordinates_to_grid(longitude: float, latitude: float) -> tuple[int, int]: ...@@ -87,10 +84,10 @@ def coordinates_to_grid(longitude: float, latitude: float) -> tuple[int, int]:
def wkt_polygon_to_bounding_box(location_name: str, wkt_str: str) -> tuple[int, int, int, int]: def wkt_polygon_to_bounding_box(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_str: 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_str) 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]]
...@@ -98,8 +95,7 @@ def wkt_polygon_to_bounding_box(location_name: str, wkt_str: str) -> tuple[int, ...@@ -98,8 +95,7 @@ def wkt_polygon_to_bounding_box(location_name: str, wkt_str: str) -> tuple[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)
logger.info("%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)
...@@ -142,45 +138,47 @@ def download_whole_region(output_dir: Path, wkt_region: str, x1: int, x2: int, y ...@@ -142,45 +138,47 @@ def download_whole_region(output_dir: Path, wkt_region: str, x1: int, x2: int, y
def extract_region(output_dir: Path, location_name: str, wkt_str: str, simstadt_folder: Path) -> Path | None: def extract_region(output_dir: Path, location_name: str, wkt_str: str, simstadt_folder: Path) -> 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():
logger.info(" %s already exists. Not extracting.", output_file) logger.info(" %s already exists. Not extracting.", output_file)
return output_file return output_file
region_chooser_libs = simstadt_folder / '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:
logger.error( logger.error("Error: No CityGML found. At least part of the region should be in Baden-Württemberg!")
"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_str) local_wkt = convert_wkt_to_local(wkt_str)
logger.info(" Extracting %s.", output_file) logger.info(" Extracting %s.", output_file)
with open(wkt_path, 'w') as f: with open(wkt_path, "w") as f:
f.write(local_wkt) f.write(local_wkt)
with open(params_path, 'w', encoding='utf-8') as f: with open(params_path, "w", encoding="utf-8") as f:
f.write("--input\n\"") f.write('--input\n"')
f.write(','.join(f'{gml.as_posix()}' for gml in gml_inputs)) f.write(",".join(f"{gml.as_posix()}" for gml in gml_inputs))
f.write("\"\n") f.write('"\n')
f.write("--output\n") f.write("--output\n")
f.write(f'"{output_file.as_posix()}"\n') f.write(f'"{output_file.as_posix()}"\n')
f.write('--local\n') f.write("--local\n")
f.write("--wkt\n") f.write("--wkt\n")
f.write(f'"{wkt_path.as_posix()}"\n') f.write(f'"{wkt_path.as_posix()}"\n')
result = subprocess.run(
result = subprocess.run(['java', '-classpath', f'{region_chooser_libs}', [
'eu.simstadt.regionchooser.RegionChooserCLI', "java",
f'@{params_path}' "-classpath",
f"{region_chooser_libs}",
"eu.simstadt.regionchooser.RegionChooserCLI",
f"@{params_path}",
], ],
text=True, text=True,
capture_output=True, capture_output=True,
check=False check=False,
) )
if result.returncode != 0: if result.returncode != 0:
if result.stderr: if result.stderr:
...@@ -197,10 +195,10 @@ def get_wkt(wkt_or_zipcode: str) -> str: ...@@ -197,10 +195,10 @@ def get_wkt(wkt_or_zipcode: str) -> str:
"70567" "70567"
"70567,70569" "70567,70569"
""" """
if 'POLYGON' in wkt_or_zipcode: if "POLYGON" in wkt_or_zipcode:
return wkt_or_zipcode return wkt_or_zipcode
return get_coordinates_by_zipcode(wkt_or_zipcode.split(',')) return get_coordinates_by_zipcode(wkt_or_zipcode.split(","))
def convert_coordinates(match): def convert_coordinates(match):
...@@ -225,40 +223,56 @@ Examples: ...@@ -225,40 +223,56 @@ Examples:
python download_LoD2_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_LoD2_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_LoD2_from_LGL_BW.py Freiburg "79098,79102" --output-folder="/path/to/FreiburgFolder" python download_LoD2_from_LGL_BW.py Freiburg "79098,79102" --output-folder="/path/to/FreiburgFolder"
python download_LoD2_from_LGL_BW.py Möhringen "70567" --download-only python download_LoD2_from_LGL_BW.py Möhringen "70567" --download-only
""" """,
) )
parser.add_argument('name', type=str, parser.add_argument("name", type=str, help="Name of the region. Output files will use this name.")
help='Name of the region. Output files will use this name.')
parser.add_argument('region', type=str, parser.add_argument(
help='Desired region as as WKT POLYGON/MULTIPOLYGON string or zipcode(s) (comma-separated).') "region", type=str, help="Desired region as as WKT POLYGON/MULTIPOLYGON string or zipcode(s) (comma-separated)."
)
parser.add_argument('--download-only', action='store_true', parser.add_argument(
help='Only download files without extracting the region (default: False).') "--download-only",
action="store_true",
help="Only download files without extracting the region (default: False).",
)
parser.add_argument('--simstadt-folder', type=Path, default=None, parser.add_argument(
help='Path to SimStadt installation folder. By default, tries to find it on the Desktop.') "--simstadt-folder",
type=Path,
default=None,
help="Path to SimStadt installation folder. By default, tries to find it on the Desktop.",
)
parser.add_argument('--output-folder', type=Path, default=None, parser.add_argument(
help='Folder in which the tiles should be downloaded and extracted. By default, use the folder of the current script / name.proj.') "--output-folder",
type=Path,
default=None,
help="Folder in which the tiles should be downloaded and extracted. By default, use the folder of the current script / name.proj.",
)
return parser.parse_args() return parser.parse_args()
def main(location_name: str, wkt_or_zipcode: str, download_only: bool = False, def main(
simstadt_folder: Path | str | None = None, output_folder: Path | str | None = None) -> Path | None: location_name: str,
wkt_or_zipcode: str,
download_only: bool = False,
simstadt_folder: Path | str | None = None,
output_folder: Path | str | None = None,
) -> Path | None:
"""Main function to process arguments and run the download/extraction""" """Main function to process arguments and run the download/extraction"""
# Validate location name # Validate location name
if ' ' in location_name: if " " in location_name:
logger.warning("Location contains spaces, some workflows might fail.") logger.warning("Location contains spaces, some workflows might fail.")
if output_folder: if output_folder:
output_folder = Path(output_folder) output_folder = Path(output_folder)
else: else:
# Create output directory # Create output directory
output_folder = SCRIPT_DIR / (location_name + '.proj') output_folder = SCRIPT_DIR / (location_name + ".proj")
output_folder.mkdir(parents=True, exist_ok=True) output_folder.mkdir(parents=True, exist_ok=True)
...@@ -279,8 +293,7 @@ def main(location_name: str, wkt_or_zipcode: str, download_only: bool = False, ...@@ -279,8 +293,7 @@ def main(location_name: str, wkt_or_zipcode: str, download_only: bool = False,
if simstadt_folder: if simstadt_folder:
simstadt_folder = Path(simstadt_folder) simstadt_folder = Path(simstadt_folder)
else: else:
logger.error( logger.error("No SimStadt installation found! Please provide --simstadt-folder or use --download-only.")
"No SimStadt installation found! Please provide --simstadt-folder or use --download-only.")
return return
gml_path = extract_region(output_folder, location_name, wkt_str, simstadt_folder) gml_path = extract_region(output_folder, location_name, wkt_str, simstadt_folder)
...@@ -291,6 +304,6 @@ def main(location_name: str, wkt_or_zipcode: str, download_only: bool = False, ...@@ -291,6 +304,6 @@ def main(location_name: str, wkt_or_zipcode: str, download_only: bool = False,
return gml_path return gml_path
if __name__ == '__main__': if __name__ == "__main__":
args = parse_arguments() args = parse_arguments()
main(args.name, args.region, args.download_only, args.simstadt_folder, args.output_folder) main(args.name, args.region, args.download_only, args.simstadt_folder, args.output_folder)
...@@ -27,11 +27,12 @@ import argparse ...@@ -27,11 +27,12 @@ import argparse
import json import json
import re import re
from pathlib import Path from pathlib import Path
from shapely.geometry import shape from shapely.geometry import shape
from shapely.ops import unary_union from shapely.ops import unary_union
INPUT_FOLDER = Path('plz') INPUT_FOLDER = Path("plz")
PLZ_FILENAME: str = 'plz-5stellig.geojson' PLZ_FILENAME: str = "plz-5stellig.geojson"
PLZ_SHAPE_FILE = INPUT_FOLDER / PLZ_FILENAME PLZ_SHAPE_FILE = INPUT_FOLDER / PLZ_FILENAME
PRECISION: float = 10 # [m] PRECISION: float = 10 # [m]
ONE_DEGREE: float = 40e6 / 360 # [m] ONE_DEGREE: float = 40e6 / 360 # [m]
...@@ -42,16 +43,17 @@ CACHED: bool = False ...@@ -42,16 +43,17 @@ CACHED: bool = False
def _download_plz_shapes_if_needed() -> None: def _download_plz_shapes_if_needed() -> None:
if not PLZ_SHAPE_FILE.exists(): if not PLZ_SHAPE_FILE.exists():
from tqdm import tqdm
import requests import requests
from tqdm import tqdm
print("Downloading %s..." % PLZ_FILENAME) print("Downloading %s..." % PLZ_FILENAME)
URL = "https://downloads.suche-postleitzahl.org/v2/public/" + PLZ_FILENAME URL = "https://downloads.suche-postleitzahl.org/v2/public/" + PLZ_FILENAME
response = requests.get(URL, stream=True) response = requests.get(URL, stream=True)
INPUT_FOLDER.mkdir(exist_ok=True) INPUT_FOLDER.mkdir(exist_ok=True)
with open(PLZ_SHAPE_FILE, "wb") as handle: with open(PLZ_SHAPE_FILE, "wb") as handle:
for data in tqdm(response.iter_content(chunk_size=1024), unit='kB'): for data in tqdm(response.iter_content(chunk_size=1024), unit="kB"):
handle.write(data) handle.write(data)
print(' Done') print(" Done")
def _get_plz_shapes() -> dict: def _get_plz_shapes() -> dict:
...@@ -63,7 +65,7 @@ def _get_plz_shapes() -> dict: ...@@ -63,7 +65,7 @@ def _get_plz_shapes() -> dict:
try: try:
print("Parsing %s..." % PLZ_FILENAME) print("Parsing %s..." % PLZ_FILENAME)
with open(PLZ_SHAPE_FILE) as f: with open(PLZ_SHAPE_FILE) as f:
print(' Done') print(" Done")
PLZ_SHAPES = json.load(f) PLZ_SHAPES = json.load(f)
CACHED = True CACHED = True
return PLZ_SHAPES return PLZ_SHAPES
...@@ -77,21 +79,21 @@ def get_coordinates_by_zipcode(plz_patterns: list[str], precision: float = PRECI ...@@ -77,21 +79,21 @@ def get_coordinates_by_zipcode(plz_patterns: list[str], precision: float = PRECI
geometries = [] geometries = []
for plz_pattern in plz_patterns: for plz_pattern in plz_patterns:
found = False found = False
for plz_geojson in plz_shapes['features']: for plz_geojson in plz_shapes["features"]:
if re.match(plz_pattern, plz_geojson['properties']['plz']): if re.match(plz_pattern, plz_geojson["properties"]["plz"]):
found = True found = True
properties = plz_geojson['properties'] properties = plz_geojson["properties"]
print('## %s' % properties['note']) print("## %s" % properties["note"])
print('Population : %d' % properties['einwohner']) print("Population : %d" % properties["einwohner"])
print('Area : %.2f km²' % properties['qkm']) print("Area : %.2f km²" % properties["qkm"])
# NOTE : Geometry can be either a polygon, # NOTE : Geometry can be either a polygon,
# a MultiPolygon : 98694 Ilmenau # a MultiPolygon : 98694 Ilmenau
# or a polygon with holes : 31860 Emmerthal # or a polygon with holes : 31860 Emmerthal
print('WKT Polygon : ') print("WKT Polygon : ")
geometries.append(shape(plz_geojson['geometry'])) geometries.append(shape(plz_geojson["geometry"]))
if not found: if not found:
raise AttributeError(f"Sorry, no information could be found for PLZ={plz_pattern}") raise AttributeError(f"Sorry, no information could be found for PLZ={plz_pattern}")
...@@ -101,6 +103,7 @@ def get_coordinates_by_zipcode(plz_patterns: list[str], precision: float = PRECI ...@@ -101,6 +103,7 @@ def get_coordinates_by_zipcode(plz_patterns: list[str], precision: float = PRECI
print(wkt_polygon) print(wkt_polygon)
try: try:
import pyperclip import pyperclip
pyperclip.copy(wkt_polygon) pyperclip.copy(wkt_polygon)
print("WKT Polygon copied to clipboard.") print("WKT Polygon copied to clipboard.")
except (ModuleNotFoundError, RuntimeError): except (ModuleNotFoundError, RuntimeError):
...@@ -112,11 +115,9 @@ def get_coordinates_by_zipcode(plz_patterns: list[str], precision: float = PRECI ...@@ -112,11 +115,9 @@ def get_coordinates_by_zipcode(plz_patterns: list[str], precision: float = PRECI
return wkt_polygon return wkt_polygon
if __name__ == '__main__': if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Get WKT geometry for desired PLZs') parser = argparse.ArgumentParser(description="Get WKT geometry for desired PLZs")
parser.add_argument('plzs', metavar='PLZ', type=str, nargs='+', parser.add_argument("plzs", metavar="PLZ", type=str, nargs="+", help="desired PLZs")
help='desired PLZs') parser.add_argument("-p", "--precision", default=PRECISION, type=int, help="precision of returned polygon [m]")
parser.add_argument('-p', '--precision', default=PRECISION, type=int,
help='precision of returned polygon [m]')
args = parser.parse_args() args = parser.parse_args()
get_coordinates_by_zipcode(args.plzs, args.precision) get_coordinates_by_zipcode(args.plzs, args.precision)
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