Skip to content
GitLab
Explore
Projects
Groups
Snippets
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
af8f59dd
Commit
af8f59dd
authored
1 month ago
by
Eric Duminil
Browse files
Options
Download
Email Patches
Plain Diff
Claude refactor
parent
608f73c0
Changes
1
Hide whitespace changes
Inline
Side-by-side
Showing
1 changed file
download_files_from_LGL_BW.py
+127
-66
download_files_from_LGL_BW.py
with
127 additions
and
66 deletions
+127
-66
download_files_from_LGL_BW.py
+
127
-
66
View file @
af8f59dd
...
...
@@ -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!"
"
\n
Please copy a SimStadt installation to the desktop,"
"
\n
set EXTRACT_REGIONS to False,"
"
\n
or 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
()
This diff is collapsed.
Click to expand it.
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
Menu
Explore
Projects
Groups
Snippets