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
af8f59dd
Commit
af8f59dd
authored
2 months 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.
...
@@ -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
"
\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')"
)
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
()
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