Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
Eric Duminil
Get Baden-Württemberg CityGML Opendata
Commits
a117f3b8
Commit
a117f3b8
authored
Jun 29, 2026
by
Eric Duminil
Browse files
Formatting
parent
1a46804f
Changes
2
Hide whitespace changes
Inline
Side-by-side
download_LoD2_from_LGL_BW.py
View file @
a117f3b8
...
...
@@ -20,29 +20,28 @@ Required:
Eric Duminil, 2025
"""
import
argparse
from
pathlib
import
Path
from
math
import
floor
import
subprocess
import
logging
import
re
import
urllib.request
import
subprocess
import
time
import
urllib.request
import
zipfile
import
logging
from
pyproj
import
CRS
from
pyproj
import
Transformer
from
math
import
floor
from
pathlib
import
Path
from
pyproj
import
CRS
,
Transformer
from
shapely
import
wkt
from
shapely.ops
import
transform
from
shapely.geometry
import
Point
from
shapely.ops
import
transform
from
get_coordinates_by_zipcode
import
get_coordinates_by_zipcode
# Setup logging
logging
.
basicConfig
(
level
=
logging
.
INFO
,
format
=
'
%(levelname)s - %(message)s
'
,
format
=
"
%(levelname)s - %(message)s
"
,
)
logger
=
logging
.
getLogger
(
__name__
)
...
...
@@ -51,12 +50,10 @@ COORDINATES_REGEX = re.compile(r"(\-?\d+\.\d*) (\-?\d+\.\d*)")
CITYGML_SERVER
=
"https://opengeodata.lgl-bw.de/data/lod2"
RASTER
=
2
# [km]
KILOMETER
=
1000
# [m]
BUNDESLAND
=
'
bw
'
BUNDESLAND
=
"
bw
"
# UTM32N, used in BW. https://epsg.io/32632
TO_LOCAL_CRS
=
Transformer
.
from_crs
(
CRS
.
from_epsg
(
4326
),
CRS
.
from_epsg
(
32632
),
always_xy
=
True
)
TO_LOCAL_CRS
=
Transformer
.
from_crs
(
CRS
.
from_epsg
(
4326
),
CRS
.
from_epsg
(
32632
),
always_xy
=
True
)
UTM
=
32
...
...
@@ -68,7 +65,7 @@ GML_GLOB = "LoD2_*/LoD2_*.gml"
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
())
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
)
return
simstadt_folder
except
StopIteration
:
...
...
@@ -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
]:
"""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"
)
coordinates
=
re
.
findall
(
r
'
\-?\d+\.\d+
'
,
wkt_str
)
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
]]
...
...
@@ -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_lat
,
max_lat
=
min
(
lats
),
max
(
lats
)
logger
.
info
(
"%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
)
...
...
@@ -142,46 +138,48 @@ 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
:
"""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
():
logger
.
info
(
" %s already exists. Not extracting."
,
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
))
if
len
(
gml_inputs
)
==
0
:
logger
.
error
(
"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
'
params_path
=
output_dir
/
"
params.txt
"
wkt_path
=
output_dir
/
"
region.wkt
"
local_wkt
=
convert_wkt_to_local
(
wkt_str
)
logger
.
info
(
" Extracting %s."
,
output_file
)
with
open
(
wkt_path
,
'w'
)
as
f
:
with
open
(
wkt_path
,
"w"
)
as
f
:
f
.
write
(
local_wkt
)
with
open
(
params_path
,
'w'
,
encoding
=
'
utf-8
'
)
as
f
:
f
.
write
(
"
--input
\n
\"
"
)
f
.
write
(
','
.
join
(
f
'
{
gml
.
as_posix
()
}
'
for
gml
in
gml_inputs
))
f
.
write
(
"
\
"\n
"
)
with
open
(
params_path
,
"w"
,
encoding
=
"
utf-8
"
)
as
f
:
f
.
write
(
'
--input
\n
"
'
)
f
.
write
(
","
.
join
(
f
"
{
gml
.
as_posix
()
}
"
for
gml
in
gml_inputs
))
f
.
write
(
'
"
\n
'
)
f
.
write
(
"--output
\n
"
)
f
.
write
(
f
'"
{
output_file
.
as_posix
()
}
"
\n
'
)
f
.
write
(
'
--local
\n
'
)
f
.
write
(
"
--local
\n
"
)
f
.
write
(
"--wkt
\n
"
)
f
.
write
(
f
'"
{
wkt_path
.
as_posix
()
}
"
\n
'
)
result
=
subprocess
.
run
([
'java'
,
'-classpath'
,
f
'
{
region_chooser_libs
}
'
,
'eu.simstadt.regionchooser.RegionChooserCLI'
,
f
'@
{
params_path
}
'
],
text
=
True
,
capture_output
=
True
,
check
=
False
)
result
=
subprocess
.
run
(
[
"java"
,
"-classpath"
,
f
"
{
region_chooser_libs
}
"
,
"eu.simstadt.regionchooser.RegionChooserCLI"
,
f
"@
{
params_path
}
"
,
],
text
=
True
,
capture_output
=
True
,
check
=
False
,
)
if
result
.
returncode
!=
0
:
if
result
.
stderr
:
logger
.
error
(
"%s"
,
result
.
stderr
)
...
...
@@ -197,10 +195,10 @@ def get_wkt(wkt_or_zipcode: str) -> str:
"70567"
"70567,70569"
"""
if
'
POLYGON
'
in
wkt_or_zipcode
:
if
"
POLYGON
"
in
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
):
...
...
@@ -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 Freiburg "79098,79102" --output-folder="/path/to/FreiburgFolder"
python download_LoD2_from_LGL_BW.py Möhringen "70567" --download-only
"""
"""
,
)
parser
.
add_argument
(
'name'
,
type
=
str
,
help
=
'Name of the region. Output files will use this name.'
)
parser
.
add_argument
(
"name"
,
type
=
str
,
help
=
"Name of the region. Output files will use this name."
)
parser
.
add_argument
(
'region'
,
type
=
str
,
help
=
'Desired region as as WKT POLYGON/MULTIPOLYGON string or zipcode(s) (comma-separated).'
)
parser
.
add_argument
(
"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'
,
help
=
'Only download files without extracting the region (default: False).'
)
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.'
)
parser
.
add_argument
(
"--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
,
help
=
'Folder in which the tiles should be downloaded and extracted. By default, use the folder of the current script / name.proj.'
)
parser
.
add_argument
(
"--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
()
def
main
(
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
:
def
main
(
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"""
# Validate location name
if
' '
in
location_name
:
if
" "
in
location_name
:
logger
.
warning
(
"Location contains spaces, some workflows might fail."
)
if
output_folder
:
output_folder
=
Path
(
output_folder
)
else
:
# 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
)
...
...
@@ -279,8 +293,7 @@ def main(location_name: str, wkt_or_zipcode: str, download_only: bool = False,
if
simstadt_folder
:
simstadt_folder
=
Path
(
simstadt_folder
)
else
:
logger
.
error
(
"No SimStadt installation found! Please provide --simstadt-folder or use --download-only."
)
logger
.
error
(
"No SimStadt installation found! Please provide --simstadt-folder or use --download-only."
)
return
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,
return
gml_path
if
__name__
==
'
__main__
'
:
if
__name__
==
"
__main__
"
:
args
=
parse_arguments
()
main
(
args
.
name
,
args
.
region
,
args
.
download_only
,
args
.
simstadt_folder
,
args
.
output_folder
)
get_coordinates_by_zipcode.py
View file @
a117f3b8
...
...
@@ -27,11 +27,12 @@ import argparse
import
json
import
re
from
pathlib
import
Path
from
shapely.geometry
import
shape
from
shapely.ops
import
unary_union
INPUT_FOLDER
=
Path
(
'
plz
'
)
PLZ_FILENAME
:
str
=
'
plz-5stellig.geojson
'
INPUT_FOLDER
=
Path
(
"
plz
"
)
PLZ_FILENAME
:
str
=
"
plz-5stellig.geojson
"
PLZ_SHAPE_FILE
=
INPUT_FOLDER
/
PLZ_FILENAME
PRECISION
:
float
=
10
# [m]
ONE_DEGREE
:
float
=
40e6
/
360
# [m]
...
...
@@ -42,16 +43,17 @@ CACHED: bool = False
def
_download_plz_shapes_if_needed
()
->
None
:
if
not
PLZ_SHAPE_FILE
.
exists
():
from
tqdm
import
tqdm
import
requests
from
tqdm
import
tqdm
print
(
"Downloading %s..."
%
PLZ_FILENAME
)
URL
=
"https://downloads.suche-postleitzahl.org/v2/public/"
+
PLZ_FILENAME
response
=
requests
.
get
(
URL
,
stream
=
True
)
INPUT_FOLDER
.
mkdir
(
exist_ok
=
True
)
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
)
print
(
'
Done
'
)
print
(
"
Done
"
)
def
_get_plz_shapes
()
->
dict
:
...
...
@@ -63,7 +65,7 @@ def _get_plz_shapes() -> dict:
try
:
print
(
"Parsing %s..."
%
PLZ_FILENAME
)
with
open
(
PLZ_SHAPE_FILE
)
as
f
:
print
(
'
Done
'
)
print
(
"
Done
"
)
PLZ_SHAPES
=
json
.
load
(
f
)
CACHED
=
True
return
PLZ_SHAPES
...
...
@@ -77,21 +79,21 @@ def get_coordinates_by_zipcode(plz_patterns: list[str], precision: float = PRECI
geometries
=
[]
for
plz_pattern
in
plz_patterns
:
found
=
False
for
plz_geojson
in
plz_shapes
[
'
features
'
]:
if
re
.
match
(
plz_pattern
,
plz_geojson
[
'
properties
'
][
'
plz
'
]):
for
plz_geojson
in
plz_shapes
[
"
features
"
]:
if
re
.
match
(
plz_pattern
,
plz_geojson
[
"
properties
"
][
"
plz
"
]):
found
=
True
properties
=
plz_geojson
[
'
properties
'
]
properties
=
plz_geojson
[
"
properties
"
]
print
(
'
## %s
'
%
properties
[
'
note
'
])
print
(
'
Population : %d
'
%
properties
[
'
einwohner
'
])
print
(
'
Area : %.2f km²
'
%
properties
[
'
qkm
'
])
print
(
"
## %s
"
%
properties
[
"
note
"
])
print
(
"
Population : %d
"
%
properties
[
"
einwohner
"
])
print
(
"
Area : %.2f km²
"
%
properties
[
"
qkm
"
])
# NOTE : Geometry can be either a polygon,
# a MultiPolygon : 98694 Ilmenau
# 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
:
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
print
(
wkt_polygon
)
try
:
import
pyperclip
pyperclip
.
copy
(
wkt_polygon
)
print
(
"WKT Polygon copied to clipboard."
)
except
(
ModuleNotFoundError
,
RuntimeError
):
...
...
@@ -112,11 +115,9 @@ def get_coordinates_by_zipcode(plz_patterns: list[str], precision: float = PRECI
return
wkt_polygon
if
__name__
==
'__main__'
:
parser
=
argparse
.
ArgumentParser
(
description
=
'Get WKT geometry for desired PLZs'
)
parser
.
add_argument
(
'plzs'
,
metavar
=
'PLZ'
,
type
=
str
,
nargs
=
'+'
,
help
=
'desired PLZs'
)
parser
.
add_argument
(
'-p'
,
'--precision'
,
default
=
PRECISION
,
type
=
int
,
help
=
'precision of returned polygon [m]'
)
if
__name__
==
"__main__"
:
parser
=
argparse
.
ArgumentParser
(
description
=
"Get WKT geometry for desired PLZs"
)
parser
.
add_argument
(
"plzs"
,
metavar
=
"PLZ"
,
type
=
str
,
nargs
=
"+"
,
help
=
"desired PLZs"
)
parser
.
add_argument
(
"-p"
,
"--precision"
,
default
=
PRECISION
,
type
=
int
,
help
=
"precision of returned polygon [m]"
)
args
=
parser
.
parse_args
()
get_coordinates_by_zipcode
(
args
.
plzs
,
args
.
precision
)
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment