voxcity 0.7.0__py3-none-any.whl → 1.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- voxcity/__init__.py +14 -14
- voxcity/exporter/__init__.py +12 -12
- voxcity/exporter/cityles.py +633 -633
- voxcity/exporter/envimet.py +733 -728
- voxcity/exporter/magicavoxel.py +333 -333
- voxcity/exporter/netcdf.py +238 -238
- voxcity/exporter/obj.py +1480 -1480
- voxcity/generator/__init__.py +47 -44
- voxcity/generator/api.py +721 -675
- voxcity/generator/grids.py +381 -379
- voxcity/generator/io.py +94 -94
- voxcity/generator/pipeline.py +282 -282
- voxcity/generator/update.py +429 -0
- voxcity/generator/voxelizer.py +18 -6
- voxcity/geoprocessor/__init__.py +75 -75
- voxcity/geoprocessor/draw.py +1488 -1219
- voxcity/geoprocessor/merge_utils.py +91 -91
- voxcity/geoprocessor/mesh.py +806 -806
- voxcity/geoprocessor/network.py +708 -708
- voxcity/geoprocessor/raster/buildings.py +435 -428
- voxcity/geoprocessor/raster/export.py +93 -93
- voxcity/geoprocessor/raster/landcover.py +5 -2
- voxcity/geoprocessor/utils.py +824 -824
- voxcity/models.py +113 -113
- voxcity/simulator/solar/__init__.py +66 -43
- voxcity/simulator/solar/integration.py +336 -336
- voxcity/simulator/solar/sky.py +668 -0
- voxcity/simulator/solar/temporal.py +792 -434
- voxcity/utils/__init__.py +11 -0
- voxcity/utils/classes.py +194 -0
- voxcity/utils/lc.py +80 -39
- voxcity/utils/shape.py +230 -0
- voxcity/visualizer/__init__.py +24 -24
- voxcity/visualizer/builder.py +43 -43
- voxcity/visualizer/grids.py +141 -141
- voxcity/visualizer/maps.py +187 -187
- voxcity/visualizer/renderer.py +1145 -928
- {voxcity-0.7.0.dist-info → voxcity-1.0.2.dist-info}/METADATA +90 -49
- {voxcity-0.7.0.dist-info → voxcity-1.0.2.dist-info}/RECORD +42 -38
- {voxcity-0.7.0.dist-info → voxcity-1.0.2.dist-info}/WHEEL +0 -0
- {voxcity-0.7.0.dist-info → voxcity-1.0.2.dist-info}/licenses/AUTHORS.rst +0 -0
- {voxcity-0.7.0.dist-info → voxcity-1.0.2.dist-info}/licenses/LICENSE +0 -0
voxcity/generator/api.py
CHANGED
|
@@ -1,675 +1,721 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import numpy as np
|
|
3
|
-
|
|
4
|
-
from ..models import PipelineConfig
|
|
5
|
-
from .pipeline import VoxCityPipeline
|
|
6
|
-
from .grids import get_land_cover_grid
|
|
7
|
-
from .io import save_voxcity
|
|
8
|
-
|
|
9
|
-
from ..downloader.citygml import load_buid_dem_veg_from_citygml
|
|
10
|
-
from ..downloader.mbfp import get_mbfp_gdf
|
|
11
|
-
from ..downloader.osm import load_gdf_from_openstreetmap
|
|
12
|
-
from ..downloader.eubucco import load_gdf_from_eubucco
|
|
13
|
-
from ..downloader.overture import load_gdf_from_overture
|
|
14
|
-
from ..downloader.gba import load_gdf_from_gba
|
|
15
|
-
from ..downloader.gee import (
|
|
16
|
-
get_roi,
|
|
17
|
-
save_geotiff_open_buildings_temporal,
|
|
18
|
-
save_geotiff_dsm_minus_dtm,
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
from ..geoprocessor.raster import (
|
|
22
|
-
create_building_height_grid_from_gdf_polygon,
|
|
23
|
-
create_vegetation_height_grid_from_gdf_polygon,
|
|
24
|
-
create_dem_grid_from_gdf_polygon,
|
|
25
|
-
)
|
|
26
|
-
from ..utils.lc import get_land_cover_classes
|
|
27
|
-
from ..geoprocessor.io import get_gdf_from_gpkg
|
|
28
|
-
from ..visualizer.grids import visualize_numerical_grid
|
|
29
|
-
from ..utils.logging import get_logger
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
_logger = get_logger(__name__)
|
|
33
|
-
|
|
34
|
-
_SOURCE_URLS = {
|
|
35
|
-
# General
|
|
36
|
-
'OpenStreetMap': 'https://www.openstreetmap.org',
|
|
37
|
-
'Local file': None,
|
|
38
|
-
'None': None,
|
|
39
|
-
'Flat': None,
|
|
40
|
-
# Buildings
|
|
41
|
-
'Microsoft Building Footprints': 'https://github.com/microsoft/GlobalMLBuildingFootprints',
|
|
42
|
-
'Open Building 2.5D Temporal': 'https://sites.research.google/gr/open-buildings/temporal/',
|
|
43
|
-
'EUBUCCO v0.1': 'https://eubucco.com/',
|
|
44
|
-
'Overture': 'https://overturemaps.org/',
|
|
45
|
-
'GBA': 'https://gee-community-catalog.org/projects/gba/',
|
|
46
|
-
'Global Building Atlas': 'https://gee-community-catalog.org/projects/gba/',
|
|
47
|
-
'England 1m DSM - DTM': 'https://developers.google.com/earth-engine/datasets/catalog/UK_EA_ENGLAND_1M_TERRAIN_2022',
|
|
48
|
-
'Netherlands 0.5m DSM - DTM': 'https://developers.google.com/earth-engine/datasets/catalog/AHN_AHN4',
|
|
49
|
-
# Land cover
|
|
50
|
-
'OpenEarthMapJapan': 'https://www.open-earth-map.org/demo/Japan/leaflet.html',
|
|
51
|
-
'Urbanwatch': 'https://gee-community-catalog.org/projects/urban-watch/',
|
|
52
|
-
'ESA WorldCover': 'https://developers.google.com/earth-engine/datasets/catalog/ESA_WorldCover_v200',
|
|
53
|
-
'ESRI 10m Annual Land Cover': 'https://gee-community-catalog.org/projects/S2TSLULC/',
|
|
54
|
-
'Dynamic World V1': 'https://developers.google.com/earth-engine/datasets/catalog/GOOGLE_DYNAMICWORLD_V1',
|
|
55
|
-
# Canopy height
|
|
56
|
-
'High Resolution 1m Global Canopy Height Maps': 'https://gee-community-catalog.org/projects/meta_trees/',
|
|
57
|
-
'ETH Global Sentinel-2 10m Canopy Height (2020)': 'https://gee-community-catalog.org/projects/canopy/',
|
|
58
|
-
'Static': None,
|
|
59
|
-
# DEM
|
|
60
|
-
'USGS 3DEP 1m': 'https://developers.google.com/earth-engine/datasets/catalog/USGS_3DEP_1m',
|
|
61
|
-
'England 1m DTM': 'https://developers.google.com/earth-engine/datasets/catalog/UK_EA_ENGLAND_1M_TERRAIN_2022',
|
|
62
|
-
'DEM France 1m': 'https://developers.google.com/earth-engine/datasets/catalog/IGN_RGE_ALTI_1M_2_0',
|
|
63
|
-
'DEM France 5m': 'https://gee-community-catalog.org/projects/france5m/',
|
|
64
|
-
'AUSTRALIA 5M DEM': 'https://developers.google.com/earth-engine/datasets/catalog/AU_GA_AUSTRALIA_5M_DEM',
|
|
65
|
-
'Netherlands 0.5m DTM': 'https://developers.google.com/earth-engine/datasets/catalog/AHN_AHN4',
|
|
66
|
-
'FABDEM': 'https://gee-community-catalog.org/projects/fabdem/',
|
|
67
|
-
'DeltaDTM': 'https://gee-community-catalog.org/projects/delta_dtm/',
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
def _url_for_source(name):
|
|
71
|
-
try:
|
|
72
|
-
return _SOURCE_URLS.get(name)
|
|
73
|
-
except Exception:
|
|
74
|
-
return None
|
|
75
|
-
|
|
76
|
-
def _center_of_rectangle(rectangle_vertices):
|
|
77
|
-
"""
|
|
78
|
-
Compute center (lon, lat) of a rectangle defined by vertices [(lon, lat), ...].
|
|
79
|
-
Accepts open or closed rings; uses simple average of vertices.
|
|
80
|
-
"""
|
|
81
|
-
lons = [p[0] for p in rectangle_vertices]
|
|
82
|
-
lats = [p[1] for p in rectangle_vertices]
|
|
83
|
-
return (sum(lons) / len(lons), sum(lats) / len(lats))
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def auto_select_data_sources(rectangle_vertices):
|
|
87
|
-
"""
|
|
88
|
-
Automatically choose data sources for buildings, land cover, canopy height, and DEM
|
|
89
|
-
based on the target area's location.
|
|
90
|
-
|
|
91
|
-
Rules (heuristic, partially inferred from latest availability):
|
|
92
|
-
- Buildings (base): 'OpenStreetMap'.
|
|
93
|
-
- Buildings (complementary):
|
|
94
|
-
* USA, Europe, Australia -> 'Microsoft Building Footprints'
|
|
95
|
-
* England -> 'England 1m DSM - DTM' (height from DSM-DTM)
|
|
96
|
-
* Netherlands -> 'Netherlands 0.5m DSM - DTM' (height from DSM-DTM)
|
|
97
|
-
* Africa, South Asia, SE Asia, Latin America & Caribbean -> 'Open Building 2.5D Temporal'
|
|
98
|
-
* Otherwise -> 'None'
|
|
99
|
-
- Land cover: USA -> 'Urbanwatch'; Japan -> 'OpenEarthMapJapan'; otherwise 'OpenStreetMap'.
|
|
100
|
-
(If OSM is insufficient, consider 'ESA WorldCover' manually.)
|
|
101
|
-
- Canopy height: 'High Resolution 1m Global Canopy Height Maps'.
|
|
102
|
-
- DEM: High-resolution where available (USA, England, Australia, France, Netherlands), else 'FABDEM'.
|
|
103
|
-
|
|
104
|
-
Returns a dict with keys: building_source, building_complementary_source,
|
|
105
|
-
land_cover_source, canopy_height_source, dem_source.
|
|
106
|
-
"""
|
|
107
|
-
try:
|
|
108
|
-
from ..geoprocessor.utils import get_country_name
|
|
109
|
-
except Exception:
|
|
110
|
-
get_country_name = None
|
|
111
|
-
|
|
112
|
-
center_lon, center_lat = _center_of_rectangle(rectangle_vertices)
|
|
113
|
-
|
|
114
|
-
# Country detection (best-effort)
|
|
115
|
-
country = None
|
|
116
|
-
if get_country_name is not None:
|
|
117
|
-
try:
|
|
118
|
-
country = get_country_name(center_lon, center_lat)
|
|
119
|
-
except Exception:
|
|
120
|
-
country = None
|
|
121
|
-
|
|
122
|
-
# Report detected country (best-effort)
|
|
123
|
-
try:
|
|
124
|
-
_logger.info(
|
|
125
|
-
"Detected country for ROI center (%.4f, %.4f): %s",
|
|
126
|
-
center_lon,
|
|
127
|
-
center_lat,
|
|
128
|
-
country or "Unknown",
|
|
129
|
-
)
|
|
130
|
-
except Exception:
|
|
131
|
-
pass
|
|
132
|
-
|
|
133
|
-
# Region helpers
|
|
134
|
-
eu_countries = {
|
|
135
|
-
'Austria', 'Belgium', 'Bulgaria', 'Croatia', 'Cyprus', 'Czechia', 'Czech Republic',
|
|
136
|
-
'Denmark', 'Estonia', 'Finland', 'France', 'Germany', 'Greece', 'Hungary', 'Ireland',
|
|
137
|
-
'Italy', 'Latvia', 'Lithuania', 'Luxembourg', 'Malta', 'Netherlands', 'Poland',
|
|
138
|
-
'Portugal', 'Romania', 'Slovakia', 'Slovenia', 'Spain', 'Sweden'
|
|
139
|
-
}
|
|
140
|
-
is_usa = (country == 'United States' or country == 'United States of America') or (-170 <= center_lon <= -65 and 20 <= center_lat <= 72)
|
|
141
|
-
is_canada = (country == 'Canada')
|
|
142
|
-
is_australia = (country == 'Australia')
|
|
143
|
-
is_france = (country == 'France')
|
|
144
|
-
is_england = (country == 'United Kingdom') # Approximation: dataset covers England specifically
|
|
145
|
-
is_netherlands = (country == 'Netherlands')
|
|
146
|
-
is_japan = (country == 'Japan') or (127 <= center_lon <= 146 and 24 <= center_lat <= 46)
|
|
147
|
-
is_europe = (country in eu_countries) or (-75 <= center_lon <= 60 and 25 <= center_lat <= 85)
|
|
148
|
-
|
|
149
|
-
# Broad regions for OB 2.5D Temporal (prefer country membership; fallback to bbox if unknown)
|
|
150
|
-
africa_countries = {
|
|
151
|
-
'Algeria', 'Angola', 'Benin', 'Botswana', 'Burkina Faso', 'Burundi', 'Cabo Verde',
|
|
152
|
-
'Cameroon', 'Central African Republic', 'Chad', 'Comoros', 'Congo',
|
|
153
|
-
'Republic of the Congo', 'Democratic Republic of the Congo', 'Congo (DRC)',
|
|
154
|
-
'DR Congo', 'Cote dIvoire', "Côte d’Ivoire", 'Ivory Coast', 'Djibouti', 'Egypt',
|
|
155
|
-
'Equatorial Guinea', 'Eritrea', 'Eswatini', 'Ethiopia', 'Gabon', 'Gambia', 'Ghana',
|
|
156
|
-
'Guinea', 'Guinea-Bissau', 'Kenya', 'Lesotho', 'Liberia', 'Libya', 'Madagascar',
|
|
157
|
-
'Malawi', 'Mali', 'Mauritania', 'Mauritius', 'Morocco', 'Mozambique', 'Namibia',
|
|
158
|
-
'Niger', 'Nigeria', 'Rwanda', 'Sao Tome and Principe', 'Senegal', 'Seychelles',
|
|
159
|
-
'Sierra Leone', 'Somalia', 'South Africa', 'South Sudan', 'Sudan', 'Tanzania', 'Togo',
|
|
160
|
-
'Tunisia', 'Uganda', 'Zambia', 'Zimbabwe', 'Western Sahara'
|
|
161
|
-
}
|
|
162
|
-
south_asia_countries = {
|
|
163
|
-
'Afghanistan', 'Bangladesh', 'Bhutan', 'India', 'Maldives', 'Nepal', 'Pakistan', 'Sri Lanka'
|
|
164
|
-
}
|
|
165
|
-
se_asia_countries = {
|
|
166
|
-
'Brunei', 'Cambodia', 'Indonesia', 'Laos', 'Lao PDR', 'Malaysia', 'Myanmar',
|
|
167
|
-
'Philippines', 'Singapore', 'Thailand', 'Timor-Leste', 'Vietnam', 'Viet Nam'
|
|
168
|
-
}
|
|
169
|
-
latam_carib_countries = {
|
|
170
|
-
# Latin America (Mexico, Central, South America) + Caribbean
|
|
171
|
-
'Mexico',
|
|
172
|
-
'Belize', 'Costa Rica', 'El Salvador', 'Guatemala', 'Honduras', 'Nicaragua', 'Panama',
|
|
173
|
-
'Argentina', 'Bolivia', 'Brazil', 'Chile', 'Colombia', 'Ecuador', 'Guyana',
|
|
174
|
-
'Paraguay', 'Peru', 'Suriname', 'Uruguay', 'Venezuela',
|
|
175
|
-
'Antigua and Barbuda', 'Bahamas', 'Barbados', 'Cuba', 'Dominica', 'Dominican Republic',
|
|
176
|
-
'Grenada', 'Haiti', 'Jamaica', 'Saint Kitts and Nevis', 'Saint Lucia',
|
|
177
|
-
'Saint Vincent and the Grenadines', 'Trinidad and Tobago',
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
# Normalize some common aliases for matching
|
|
181
|
-
_alias = {
|
|
182
|
-
'United States of America': 'United States',
|
|
183
|
-
'Czech Republic': 'Czechia',
|
|
184
|
-
'Viet Nam': 'Vietnam',
|
|
185
|
-
'Lao PDR': 'Laos',
|
|
186
|
-
'Ivory Coast': "Côte d’Ivoire",
|
|
187
|
-
'Congo, Democratic Republic of the': 'Democratic Republic of the Congo',
|
|
188
|
-
'Congo, Republic of the': 'Republic of the Congo',
|
|
189
|
-
}
|
|
190
|
-
country_norm = _alias.get(country, country) if country else None
|
|
191
|
-
|
|
192
|
-
in_africa = (country_norm in africa_countries) if country_norm else (-25 <= center_lon <= 80 and -55 <= center_lat <= 45)
|
|
193
|
-
in_south_asia = (country_norm in south_asia_countries) if country_norm else (50 <= center_lon <= 100 and 0 <= center_lat <= 35)
|
|
194
|
-
in_se_asia = (country_norm in se_asia_countries) if country_norm else (90 <= center_lon <= 150 and -10 <= center_lat <= 25)
|
|
195
|
-
in_latam_carib = (country_norm in latam_carib_countries) if country_norm else (-110 <= center_lon <= -30 and -60 <= center_lat <= 30)
|
|
196
|
-
|
|
197
|
-
# Building base source
|
|
198
|
-
building_source = 'OpenStreetMap'
|
|
199
|
-
|
|
200
|
-
# Building complementary source
|
|
201
|
-
building_complementary_source = 'None'
|
|
202
|
-
if is_england:
|
|
203
|
-
building_complementary_source = 'England 1m DSM - DTM'
|
|
204
|
-
elif is_netherlands:
|
|
205
|
-
building_complementary_source = 'Netherlands 0.5m DSM - DTM'
|
|
206
|
-
elif is_usa or is_australia or is_europe:
|
|
207
|
-
building_complementary_source = 'Microsoft Building Footprints'
|
|
208
|
-
elif in_africa or in_south_asia or in_se_asia or in_latam_carib:
|
|
209
|
-
building_complementary_source = 'Open Building 2.5D Temporal'
|
|
210
|
-
|
|
211
|
-
# Land cover source
|
|
212
|
-
if is_usa:
|
|
213
|
-
land_cover_source = 'Urbanwatch'
|
|
214
|
-
elif is_japan:
|
|
215
|
-
land_cover_source = 'OpenEarthMapJapan'
|
|
216
|
-
else:
|
|
217
|
-
land_cover_source = 'OpenStreetMap'
|
|
218
|
-
|
|
219
|
-
# Canopy height source
|
|
220
|
-
canopy_height_source = 'High Resolution 1m Global Canopy Height Maps'
|
|
221
|
-
|
|
222
|
-
# DEM source
|
|
223
|
-
if is_usa:
|
|
224
|
-
dem_source = 'USGS 3DEP 1m'
|
|
225
|
-
elif is_england:
|
|
226
|
-
dem_source = 'England 1m DTM'
|
|
227
|
-
elif is_australia:
|
|
228
|
-
dem_source = 'AUSTRALIA 5M DEM'
|
|
229
|
-
elif is_france:
|
|
230
|
-
dem_source = 'DEM France 1m'
|
|
231
|
-
elif is_netherlands:
|
|
232
|
-
dem_source = 'Netherlands 0.5m DTM'
|
|
233
|
-
else:
|
|
234
|
-
dem_source = 'FABDEM'
|
|
235
|
-
|
|
236
|
-
return {
|
|
237
|
-
'building_source': building_source,
|
|
238
|
-
'building_complementary_source': building_complementary_source,
|
|
239
|
-
'land_cover_source': land_cover_source,
|
|
240
|
-
'canopy_height_source': canopy_height_source,
|
|
241
|
-
'dem_source': dem_source,
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
def get_voxcity(rectangle_vertices, meshsize, building_source=None, land_cover_source=None, canopy_height_source=None, dem_source=None, building_complementary_source=None, building_gdf=None, terrain_gdf=None, **kwargs):
|
|
246
|
-
"""
|
|
247
|
-
Generate a VoxCity model with automatic or custom data source selection.
|
|
248
|
-
|
|
249
|
-
This function supports both auto mode and custom mode:
|
|
250
|
-
- Auto mode: When sources are not specified (None), they are automatically selected based on location
|
|
251
|
-
- Custom mode: When sources are explicitly specified, they are used as-is
|
|
252
|
-
- Hybrid mode: Specify some sources and auto-select others
|
|
253
|
-
|
|
254
|
-
Args:
|
|
255
|
-
rectangle_vertices: List of (lon, lat) tuples defining the area of interest
|
|
256
|
-
meshsize: Grid resolution in meters (required)
|
|
257
|
-
building_source: Building base source (default: auto-selected based on location)
|
|
258
|
-
land_cover_source: Land cover source (default: auto-selected based on location)
|
|
259
|
-
canopy_height_source: Canopy height source (default: auto-selected based on location)
|
|
260
|
-
dem_source: Digital elevation model source (default: auto-selected based on location)
|
|
261
|
-
building_complementary_source: Building complementary source (default: auto-selected based on location)
|
|
262
|
-
building_gdf: Optional pre-loaded building GeoDataFrame
|
|
263
|
-
terrain_gdf: Optional pre-loaded terrain GeoDataFrame
|
|
264
|
-
**kwargs: Additional options for building, land cover, canopy, DEM, visualization, and I/O
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
#
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
if
|
|
279
|
-
|
|
280
|
-
if
|
|
281
|
-
sources_to_select.append('
|
|
282
|
-
if
|
|
283
|
-
sources_to_select.append('
|
|
284
|
-
if
|
|
285
|
-
sources_to_select.append('
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
if
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
'
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
if
|
|
320
|
-
|
|
321
|
-
if
|
|
322
|
-
|
|
323
|
-
if
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
if
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
#
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
"
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
"
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
except Exception:
|
|
467
|
-
pass
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
"
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
1
|
+
import os
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
from ..models import PipelineConfig
|
|
5
|
+
from .pipeline import VoxCityPipeline
|
|
6
|
+
from .grids import get_land_cover_grid
|
|
7
|
+
from .io import save_voxcity
|
|
8
|
+
|
|
9
|
+
from ..downloader.citygml import load_buid_dem_veg_from_citygml
|
|
10
|
+
from ..downloader.mbfp import get_mbfp_gdf
|
|
11
|
+
from ..downloader.osm import load_gdf_from_openstreetmap
|
|
12
|
+
from ..downloader.eubucco import load_gdf_from_eubucco
|
|
13
|
+
from ..downloader.overture import load_gdf_from_overture
|
|
14
|
+
from ..downloader.gba import load_gdf_from_gba
|
|
15
|
+
from ..downloader.gee import (
|
|
16
|
+
get_roi,
|
|
17
|
+
save_geotiff_open_buildings_temporal,
|
|
18
|
+
save_geotiff_dsm_minus_dtm,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from ..geoprocessor.raster import (
|
|
22
|
+
create_building_height_grid_from_gdf_polygon,
|
|
23
|
+
create_vegetation_height_grid_from_gdf_polygon,
|
|
24
|
+
create_dem_grid_from_gdf_polygon,
|
|
25
|
+
)
|
|
26
|
+
from ..utils.lc import get_land_cover_classes
|
|
27
|
+
from ..geoprocessor.io import get_gdf_from_gpkg
|
|
28
|
+
from ..visualizer.grids import visualize_numerical_grid
|
|
29
|
+
from ..utils.logging import get_logger
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
_logger = get_logger(__name__)
|
|
33
|
+
|
|
34
|
+
_SOURCE_URLS = {
|
|
35
|
+
# General
|
|
36
|
+
'OpenStreetMap': 'https://www.openstreetmap.org',
|
|
37
|
+
'Local file': None,
|
|
38
|
+
'None': None,
|
|
39
|
+
'Flat': None,
|
|
40
|
+
# Buildings
|
|
41
|
+
'Microsoft Building Footprints': 'https://github.com/microsoft/GlobalMLBuildingFootprints',
|
|
42
|
+
'Open Building 2.5D Temporal': 'https://sites.research.google/gr/open-buildings/temporal/',
|
|
43
|
+
'EUBUCCO v0.1': 'https://eubucco.com/',
|
|
44
|
+
'Overture': 'https://overturemaps.org/',
|
|
45
|
+
'GBA': 'https://gee-community-catalog.org/projects/gba/',
|
|
46
|
+
'Global Building Atlas': 'https://gee-community-catalog.org/projects/gba/',
|
|
47
|
+
'England 1m DSM - DTM': 'https://developers.google.com/earth-engine/datasets/catalog/UK_EA_ENGLAND_1M_TERRAIN_2022',
|
|
48
|
+
'Netherlands 0.5m DSM - DTM': 'https://developers.google.com/earth-engine/datasets/catalog/AHN_AHN4',
|
|
49
|
+
# Land cover
|
|
50
|
+
'OpenEarthMapJapan': 'https://www.open-earth-map.org/demo/Japan/leaflet.html',
|
|
51
|
+
'Urbanwatch': 'https://gee-community-catalog.org/projects/urban-watch/',
|
|
52
|
+
'ESA WorldCover': 'https://developers.google.com/earth-engine/datasets/catalog/ESA_WorldCover_v200',
|
|
53
|
+
'ESRI 10m Annual Land Cover': 'https://gee-community-catalog.org/projects/S2TSLULC/',
|
|
54
|
+
'Dynamic World V1': 'https://developers.google.com/earth-engine/datasets/catalog/GOOGLE_DYNAMICWORLD_V1',
|
|
55
|
+
# Canopy height
|
|
56
|
+
'High Resolution 1m Global Canopy Height Maps': 'https://gee-community-catalog.org/projects/meta_trees/',
|
|
57
|
+
'ETH Global Sentinel-2 10m Canopy Height (2020)': 'https://gee-community-catalog.org/projects/canopy/',
|
|
58
|
+
'Static': None,
|
|
59
|
+
# DEM
|
|
60
|
+
'USGS 3DEP 1m': 'https://developers.google.com/earth-engine/datasets/catalog/USGS_3DEP_1m',
|
|
61
|
+
'England 1m DTM': 'https://developers.google.com/earth-engine/datasets/catalog/UK_EA_ENGLAND_1M_TERRAIN_2022',
|
|
62
|
+
'DEM France 1m': 'https://developers.google.com/earth-engine/datasets/catalog/IGN_RGE_ALTI_1M_2_0',
|
|
63
|
+
'DEM France 5m': 'https://gee-community-catalog.org/projects/france5m/',
|
|
64
|
+
'AUSTRALIA 5M DEM': 'https://developers.google.com/earth-engine/datasets/catalog/AU_GA_AUSTRALIA_5M_DEM',
|
|
65
|
+
'Netherlands 0.5m DTM': 'https://developers.google.com/earth-engine/datasets/catalog/AHN_AHN4',
|
|
66
|
+
'FABDEM': 'https://gee-community-catalog.org/projects/fabdem/',
|
|
67
|
+
'DeltaDTM': 'https://gee-community-catalog.org/projects/delta_dtm/',
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
def _url_for_source(name):
|
|
71
|
+
try:
|
|
72
|
+
return _SOURCE_URLS.get(name)
|
|
73
|
+
except Exception:
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
def _center_of_rectangle(rectangle_vertices):
|
|
77
|
+
"""
|
|
78
|
+
Compute center (lon, lat) of a rectangle defined by vertices [(lon, lat), ...].
|
|
79
|
+
Accepts open or closed rings; uses simple average of vertices.
|
|
80
|
+
"""
|
|
81
|
+
lons = [p[0] for p in rectangle_vertices]
|
|
82
|
+
lats = [p[1] for p in rectangle_vertices]
|
|
83
|
+
return (sum(lons) / len(lons), sum(lats) / len(lats))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def auto_select_data_sources(rectangle_vertices):
|
|
87
|
+
"""
|
|
88
|
+
Automatically choose data sources for buildings, land cover, canopy height, and DEM
|
|
89
|
+
based on the target area's location.
|
|
90
|
+
|
|
91
|
+
Rules (heuristic, partially inferred from latest availability):
|
|
92
|
+
- Buildings (base): 'OpenStreetMap'.
|
|
93
|
+
- Buildings (complementary):
|
|
94
|
+
* USA, Europe, Australia -> 'Microsoft Building Footprints'
|
|
95
|
+
* England -> 'England 1m DSM - DTM' (height from DSM-DTM)
|
|
96
|
+
* Netherlands -> 'Netherlands 0.5m DSM - DTM' (height from DSM-DTM)
|
|
97
|
+
* Africa, South Asia, SE Asia, Latin America & Caribbean -> 'Open Building 2.5D Temporal'
|
|
98
|
+
* Otherwise -> 'None'
|
|
99
|
+
- Land cover: USA -> 'Urbanwatch'; Japan -> 'OpenEarthMapJapan'; otherwise 'OpenStreetMap'.
|
|
100
|
+
(If OSM is insufficient, consider 'ESA WorldCover' manually.)
|
|
101
|
+
- Canopy height: 'High Resolution 1m Global Canopy Height Maps'.
|
|
102
|
+
- DEM: High-resolution where available (USA, England, Australia, France, Netherlands), else 'FABDEM'.
|
|
103
|
+
|
|
104
|
+
Returns a dict with keys: building_source, building_complementary_source,
|
|
105
|
+
land_cover_source, canopy_height_source, dem_source.
|
|
106
|
+
"""
|
|
107
|
+
try:
|
|
108
|
+
from ..geoprocessor.utils import get_country_name
|
|
109
|
+
except Exception:
|
|
110
|
+
get_country_name = None
|
|
111
|
+
|
|
112
|
+
center_lon, center_lat = _center_of_rectangle(rectangle_vertices)
|
|
113
|
+
|
|
114
|
+
# Country detection (best-effort)
|
|
115
|
+
country = None
|
|
116
|
+
if get_country_name is not None:
|
|
117
|
+
try:
|
|
118
|
+
country = get_country_name(center_lon, center_lat)
|
|
119
|
+
except Exception:
|
|
120
|
+
country = None
|
|
121
|
+
|
|
122
|
+
# Report detected country (best-effort)
|
|
123
|
+
try:
|
|
124
|
+
_logger.info(
|
|
125
|
+
"Detected country for ROI center (%.4f, %.4f): %s",
|
|
126
|
+
center_lon,
|
|
127
|
+
center_lat,
|
|
128
|
+
country or "Unknown",
|
|
129
|
+
)
|
|
130
|
+
except Exception:
|
|
131
|
+
pass
|
|
132
|
+
|
|
133
|
+
# Region helpers
|
|
134
|
+
eu_countries = {
|
|
135
|
+
'Austria', 'Belgium', 'Bulgaria', 'Croatia', 'Cyprus', 'Czechia', 'Czech Republic',
|
|
136
|
+
'Denmark', 'Estonia', 'Finland', 'France', 'Germany', 'Greece', 'Hungary', 'Ireland',
|
|
137
|
+
'Italy', 'Latvia', 'Lithuania', 'Luxembourg', 'Malta', 'Netherlands', 'Poland',
|
|
138
|
+
'Portugal', 'Romania', 'Slovakia', 'Slovenia', 'Spain', 'Sweden'
|
|
139
|
+
}
|
|
140
|
+
is_usa = (country == 'United States' or country == 'United States of America') or (-170 <= center_lon <= -65 and 20 <= center_lat <= 72)
|
|
141
|
+
is_canada = (country == 'Canada')
|
|
142
|
+
is_australia = (country == 'Australia')
|
|
143
|
+
is_france = (country == 'France')
|
|
144
|
+
is_england = (country == 'United Kingdom') # Approximation: dataset covers England specifically
|
|
145
|
+
is_netherlands = (country == 'Netherlands')
|
|
146
|
+
is_japan = (country == 'Japan') or (127 <= center_lon <= 146 and 24 <= center_lat <= 46)
|
|
147
|
+
is_europe = (country in eu_countries) or (-75 <= center_lon <= 60 and 25 <= center_lat <= 85)
|
|
148
|
+
|
|
149
|
+
# Broad regions for OB 2.5D Temporal (prefer country membership; fallback to bbox if unknown)
|
|
150
|
+
africa_countries = {
|
|
151
|
+
'Algeria', 'Angola', 'Benin', 'Botswana', 'Burkina Faso', 'Burundi', 'Cabo Verde',
|
|
152
|
+
'Cameroon', 'Central African Republic', 'Chad', 'Comoros', 'Congo',
|
|
153
|
+
'Republic of the Congo', 'Democratic Republic of the Congo', 'Congo (DRC)',
|
|
154
|
+
'DR Congo', 'Cote dIvoire', "Côte d’Ivoire", 'Ivory Coast', 'Djibouti', 'Egypt',
|
|
155
|
+
'Equatorial Guinea', 'Eritrea', 'Eswatini', 'Ethiopia', 'Gabon', 'Gambia', 'Ghana',
|
|
156
|
+
'Guinea', 'Guinea-Bissau', 'Kenya', 'Lesotho', 'Liberia', 'Libya', 'Madagascar',
|
|
157
|
+
'Malawi', 'Mali', 'Mauritania', 'Mauritius', 'Morocco', 'Mozambique', 'Namibia',
|
|
158
|
+
'Niger', 'Nigeria', 'Rwanda', 'Sao Tome and Principe', 'Senegal', 'Seychelles',
|
|
159
|
+
'Sierra Leone', 'Somalia', 'South Africa', 'South Sudan', 'Sudan', 'Tanzania', 'Togo',
|
|
160
|
+
'Tunisia', 'Uganda', 'Zambia', 'Zimbabwe', 'Western Sahara'
|
|
161
|
+
}
|
|
162
|
+
south_asia_countries = {
|
|
163
|
+
'Afghanistan', 'Bangladesh', 'Bhutan', 'India', 'Maldives', 'Nepal', 'Pakistan', 'Sri Lanka'
|
|
164
|
+
}
|
|
165
|
+
se_asia_countries = {
|
|
166
|
+
'Brunei', 'Cambodia', 'Indonesia', 'Laos', 'Lao PDR', 'Malaysia', 'Myanmar',
|
|
167
|
+
'Philippines', 'Singapore', 'Thailand', 'Timor-Leste', 'Vietnam', 'Viet Nam'
|
|
168
|
+
}
|
|
169
|
+
latam_carib_countries = {
|
|
170
|
+
# Latin America (Mexico, Central, South America) + Caribbean
|
|
171
|
+
'Mexico',
|
|
172
|
+
'Belize', 'Costa Rica', 'El Salvador', 'Guatemala', 'Honduras', 'Nicaragua', 'Panama',
|
|
173
|
+
'Argentina', 'Bolivia', 'Brazil', 'Chile', 'Colombia', 'Ecuador', 'Guyana',
|
|
174
|
+
'Paraguay', 'Peru', 'Suriname', 'Uruguay', 'Venezuela',
|
|
175
|
+
'Antigua and Barbuda', 'Bahamas', 'Barbados', 'Cuba', 'Dominica', 'Dominican Republic',
|
|
176
|
+
'Grenada', 'Haiti', 'Jamaica', 'Saint Kitts and Nevis', 'Saint Lucia',
|
|
177
|
+
'Saint Vincent and the Grenadines', 'Trinidad and Tobago',
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
# Normalize some common aliases for matching
|
|
181
|
+
_alias = {
|
|
182
|
+
'United States of America': 'United States',
|
|
183
|
+
'Czech Republic': 'Czechia',
|
|
184
|
+
'Viet Nam': 'Vietnam',
|
|
185
|
+
'Lao PDR': 'Laos',
|
|
186
|
+
'Ivory Coast': "Côte d’Ivoire",
|
|
187
|
+
'Congo, Democratic Republic of the': 'Democratic Republic of the Congo',
|
|
188
|
+
'Congo, Republic of the': 'Republic of the Congo',
|
|
189
|
+
}
|
|
190
|
+
country_norm = _alias.get(country, country) if country else None
|
|
191
|
+
|
|
192
|
+
in_africa = (country_norm in africa_countries) if country_norm else (-25 <= center_lon <= 80 and -55 <= center_lat <= 45)
|
|
193
|
+
in_south_asia = (country_norm in south_asia_countries) if country_norm else (50 <= center_lon <= 100 and 0 <= center_lat <= 35)
|
|
194
|
+
in_se_asia = (country_norm in se_asia_countries) if country_norm else (90 <= center_lon <= 150 and -10 <= center_lat <= 25)
|
|
195
|
+
in_latam_carib = (country_norm in latam_carib_countries) if country_norm else (-110 <= center_lon <= -30 and -60 <= center_lat <= 30)
|
|
196
|
+
|
|
197
|
+
# Building base source
|
|
198
|
+
building_source = 'OpenStreetMap'
|
|
199
|
+
|
|
200
|
+
# Building complementary source
|
|
201
|
+
building_complementary_source = 'None'
|
|
202
|
+
if is_england:
|
|
203
|
+
building_complementary_source = 'England 1m DSM - DTM'
|
|
204
|
+
elif is_netherlands:
|
|
205
|
+
building_complementary_source = 'Netherlands 0.5m DSM - DTM'
|
|
206
|
+
elif is_usa or is_australia or is_europe:
|
|
207
|
+
building_complementary_source = 'Microsoft Building Footprints'
|
|
208
|
+
elif in_africa or in_south_asia or in_se_asia or in_latam_carib:
|
|
209
|
+
building_complementary_source = 'Open Building 2.5D Temporal'
|
|
210
|
+
|
|
211
|
+
# Land cover source
|
|
212
|
+
if is_usa:
|
|
213
|
+
land_cover_source = 'Urbanwatch'
|
|
214
|
+
elif is_japan:
|
|
215
|
+
land_cover_source = 'OpenEarthMapJapan'
|
|
216
|
+
else:
|
|
217
|
+
land_cover_source = 'OpenStreetMap'
|
|
218
|
+
|
|
219
|
+
# Canopy height source
|
|
220
|
+
canopy_height_source = 'High Resolution 1m Global Canopy Height Maps'
|
|
221
|
+
|
|
222
|
+
# DEM source
|
|
223
|
+
if is_usa:
|
|
224
|
+
dem_source = 'USGS 3DEP 1m'
|
|
225
|
+
elif is_england:
|
|
226
|
+
dem_source = 'England 1m DTM'
|
|
227
|
+
elif is_australia:
|
|
228
|
+
dem_source = 'AUSTRALIA 5M DEM'
|
|
229
|
+
elif is_france:
|
|
230
|
+
dem_source = 'DEM France 1m'
|
|
231
|
+
elif is_netherlands:
|
|
232
|
+
dem_source = 'Netherlands 0.5m DTM'
|
|
233
|
+
else:
|
|
234
|
+
dem_source = 'FABDEM'
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
'building_source': building_source,
|
|
238
|
+
'building_complementary_source': building_complementary_source,
|
|
239
|
+
'land_cover_source': land_cover_source,
|
|
240
|
+
'canopy_height_source': canopy_height_source,
|
|
241
|
+
'dem_source': dem_source,
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def get_voxcity(rectangle_vertices, meshsize, building_source=None, land_cover_source=None, canopy_height_source=None, dem_source=None, building_complementary_source=None, building_gdf=None, terrain_gdf=None, **kwargs):
|
|
246
|
+
"""
|
|
247
|
+
Generate a VoxCity model with automatic or custom data source selection.
|
|
248
|
+
|
|
249
|
+
This function supports both auto mode and custom mode:
|
|
250
|
+
- Auto mode: When sources are not specified (None), they are automatically selected based on location
|
|
251
|
+
- Custom mode: When sources are explicitly specified, they are used as-is
|
|
252
|
+
- Hybrid mode: Specify some sources and auto-select others
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
rectangle_vertices: List of (lon, lat) tuples defining the area of interest
|
|
256
|
+
meshsize: Grid resolution in meters (required)
|
|
257
|
+
building_source: Building base source (default: auto-selected based on location)
|
|
258
|
+
land_cover_source: Land cover source (default: auto-selected based on location)
|
|
259
|
+
canopy_height_source: Canopy height source (default: auto-selected based on location)
|
|
260
|
+
dem_source: Digital elevation model source (default: auto-selected based on location)
|
|
261
|
+
building_complementary_source: Building complementary source (default: auto-selected based on location)
|
|
262
|
+
building_gdf: Optional pre-loaded building GeoDataFrame
|
|
263
|
+
terrain_gdf: Optional pre-loaded terrain GeoDataFrame
|
|
264
|
+
**kwargs: Additional options for building, land cover, canopy, DEM, visualization, and I/O.
|
|
265
|
+
I/O options include:
|
|
266
|
+
- output_dir: Directory for intermediate/downloaded data (default: "output")
|
|
267
|
+
- save_path: Full file path to save the VoxCity object (overrides output_dir default)
|
|
268
|
+
- save_voxcity_data / save_voxctiy_data: bool flag to enable saving (default: True)
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
VoxCity object containing the generated 3D city model
|
|
272
|
+
"""
|
|
273
|
+
|
|
274
|
+
# Check if building_complementary_source was provided via kwargs (for backward compatibility)
|
|
275
|
+
if building_complementary_source is None and 'building_complementary_source' in kwargs:
|
|
276
|
+
building_complementary_source = kwargs.pop('building_complementary_source')
|
|
277
|
+
|
|
278
|
+
# Determine if we need to auto-select any sources
|
|
279
|
+
sources_to_select = []
|
|
280
|
+
if building_source is None:
|
|
281
|
+
sources_to_select.append('building_source')
|
|
282
|
+
if land_cover_source is None:
|
|
283
|
+
sources_to_select.append('land_cover_source')
|
|
284
|
+
if canopy_height_source is None:
|
|
285
|
+
sources_to_select.append('canopy_height_source')
|
|
286
|
+
if dem_source is None:
|
|
287
|
+
sources_to_select.append('dem_source')
|
|
288
|
+
if building_complementary_source is None:
|
|
289
|
+
sources_to_select.append('building_complementary_source')
|
|
290
|
+
|
|
291
|
+
# Auto-select missing sources if needed
|
|
292
|
+
if sources_to_select:
|
|
293
|
+
_logger.info("Auto-selecting data sources for: %s", ", ".join(sources_to_select))
|
|
294
|
+
auto_sources = auto_select_data_sources(rectangle_vertices)
|
|
295
|
+
|
|
296
|
+
# Check Earth Engine availability for auto-selected sources
|
|
297
|
+
ee_available = True
|
|
298
|
+
try:
|
|
299
|
+
from ..downloader.gee import initialize_earth_engine
|
|
300
|
+
initialize_earth_engine()
|
|
301
|
+
except Exception:
|
|
302
|
+
ee_available = False
|
|
303
|
+
|
|
304
|
+
if not ee_available:
|
|
305
|
+
# Downgrade EE-dependent sources
|
|
306
|
+
if auto_sources['land_cover_source'] not in ('OpenStreetMap', 'OpenEarthMapJapan'):
|
|
307
|
+
auto_sources['land_cover_source'] = 'OpenStreetMap'
|
|
308
|
+
auto_sources['canopy_height_source'] = 'Static'
|
|
309
|
+
auto_sources['dem_source'] = 'Flat'
|
|
310
|
+
ee_dependent_comp = {
|
|
311
|
+
'Open Building 2.5D Temporal',
|
|
312
|
+
'England 1m DSM - DTM',
|
|
313
|
+
'Netherlands 0.5m DSM - DTM',
|
|
314
|
+
}
|
|
315
|
+
if auto_sources.get('building_complementary_source') in ee_dependent_comp:
|
|
316
|
+
auto_sources['building_complementary_source'] = 'Microsoft Building Footprints'
|
|
317
|
+
|
|
318
|
+
# Apply auto-selected sources only where not specified
|
|
319
|
+
if building_source is None:
|
|
320
|
+
building_source = auto_sources['building_source']
|
|
321
|
+
if land_cover_source is None:
|
|
322
|
+
land_cover_source = auto_sources['land_cover_source']
|
|
323
|
+
if canopy_height_source is None:
|
|
324
|
+
canopy_height_source = auto_sources['canopy_height_source']
|
|
325
|
+
if dem_source is None:
|
|
326
|
+
dem_source = auto_sources['dem_source']
|
|
327
|
+
if building_complementary_source is None:
|
|
328
|
+
building_complementary_source = auto_sources.get('building_complementary_source', 'None')
|
|
329
|
+
|
|
330
|
+
# Auto-set complement height if not provided
|
|
331
|
+
if 'building_complement_height' not in kwargs:
|
|
332
|
+
kwargs['building_complement_height'] = 10
|
|
333
|
+
|
|
334
|
+
# Ensure building_complementary_source is passed through kwargs
|
|
335
|
+
if building_complementary_source is not None:
|
|
336
|
+
kwargs['building_complementary_source'] = building_complementary_source
|
|
337
|
+
|
|
338
|
+
# Default DEM interpolation to True unless explicitly provided
|
|
339
|
+
if 'dem_interpolation' not in kwargs:
|
|
340
|
+
kwargs['dem_interpolation'] = True
|
|
341
|
+
|
|
342
|
+
# Ensure default complement height even if all sources are user-specified
|
|
343
|
+
if 'building_complement_height' not in kwargs:
|
|
344
|
+
kwargs['building_complement_height'] = 10
|
|
345
|
+
|
|
346
|
+
# Log selected data sources (always)
|
|
347
|
+
try:
|
|
348
|
+
_logger.info("Selected data sources:")
|
|
349
|
+
b_base_url = _url_for_source(building_source)
|
|
350
|
+
_logger.info("- Buildings(base)=%s%s", building_source, f" | {b_base_url}" if b_base_url else "")
|
|
351
|
+
b_comp_url = _url_for_source(building_complementary_source)
|
|
352
|
+
_logger.info("- Buildings(comp)=%s%s", building_complementary_source, f" | {b_comp_url}" if b_comp_url else "")
|
|
353
|
+
lc_url = _url_for_source(land_cover_source)
|
|
354
|
+
_logger.info("- LandCover=%s%s", land_cover_source, f" | {lc_url}" if lc_url else "")
|
|
355
|
+
canopy_url = _url_for_source(canopy_height_source)
|
|
356
|
+
_logger.info("- Canopy=%s%s", canopy_height_source, f" | {canopy_url}" if canopy_url else "")
|
|
357
|
+
dem_url = _url_for_source(dem_source)
|
|
358
|
+
_logger.info("- DEM=%s%s", dem_source, f" | {dem_url}" if dem_url else "")
|
|
359
|
+
_logger.info("- ComplementHeight=%s", kwargs.get('building_complement_height'))
|
|
360
|
+
except Exception:
|
|
361
|
+
pass
|
|
362
|
+
|
|
363
|
+
output_dir = kwargs.get("output_dir", "output")
|
|
364
|
+
# Group incoming kwargs into structured options for consistency
|
|
365
|
+
land_cover_keys = {
|
|
366
|
+
# examples: source-specific options (placeholders kept broad for back-compat)
|
|
367
|
+
"land_cover_path", "land_cover_resample", "land_cover_classes",
|
|
368
|
+
}
|
|
369
|
+
building_keys = {
|
|
370
|
+
"overlapping_footprint", "gdf_comp", "geotiff_path_comp",
|
|
371
|
+
"complement_building_footprints", "complement_height", "floor_height",
|
|
372
|
+
"building_complementary_source", "building_complement_height",
|
|
373
|
+
"building_complementary_path", "gba_clip", "gba_download_dir",
|
|
374
|
+
}
|
|
375
|
+
canopy_keys = {
|
|
376
|
+
"min_canopy_height", "trunk_height_ratio", "static_tree_height",
|
|
377
|
+
}
|
|
378
|
+
dem_keys = {
|
|
379
|
+
"flat_dem",
|
|
380
|
+
}
|
|
381
|
+
visualize_keys = {"gridvis", "mapvis"}
|
|
382
|
+
io_keys = {"save_voxcity_data", "save_voxctiy_data", "save_data_path", "save_path"}
|
|
383
|
+
|
|
384
|
+
land_cover_options = {k: v for k, v in kwargs.items() if k in land_cover_keys}
|
|
385
|
+
building_options = {k: v for k, v in kwargs.items() if k in building_keys}
|
|
386
|
+
canopy_options = {k: v for k, v in kwargs.items() if k in canopy_keys}
|
|
387
|
+
dem_options = {k: v for k, v in kwargs.items() if k in dem_keys}
|
|
388
|
+
# Auto-set flat DEM when dem_source is None/empty and user didn't specify
|
|
389
|
+
if (dem_source in (None, "", "None")) and ("flat_dem" not in dem_options):
|
|
390
|
+
dem_options["flat_dem"] = True
|
|
391
|
+
visualize_options = {k: v for k, v in kwargs.items() if k in visualize_keys}
|
|
392
|
+
io_options = {k: v for k, v in kwargs.items() if k in io_keys}
|
|
393
|
+
|
|
394
|
+
cfg = PipelineConfig(
|
|
395
|
+
rectangle_vertices=rectangle_vertices,
|
|
396
|
+
meshsize=float(meshsize),
|
|
397
|
+
building_source=building_source,
|
|
398
|
+
land_cover_source=land_cover_source,
|
|
399
|
+
canopy_height_source=canopy_height_source,
|
|
400
|
+
dem_source=dem_source,
|
|
401
|
+
output_dir=output_dir,
|
|
402
|
+
trunk_height_ratio=kwargs.get("trunk_height_ratio"),
|
|
403
|
+
static_tree_height=kwargs.get("static_tree_height"),
|
|
404
|
+
remove_perimeter_object=kwargs.get("remove_perimeter_object"),
|
|
405
|
+
mapvis=bool(kwargs.get("mapvis", False)),
|
|
406
|
+
gridvis=bool(kwargs.get("gridvis", True)),
|
|
407
|
+
land_cover_options=land_cover_options,
|
|
408
|
+
building_options=building_options,
|
|
409
|
+
canopy_options=canopy_options,
|
|
410
|
+
dem_options=dem_options,
|
|
411
|
+
io_options=io_options,
|
|
412
|
+
visualize_options=visualize_options,
|
|
413
|
+
)
|
|
414
|
+
city = VoxCityPipeline(meshsize=cfg.meshsize, rectangle_vertices=cfg.rectangle_vertices).run(cfg, building_gdf=building_gdf, terrain_gdf=terrain_gdf, **{k: v for k, v in kwargs.items() if k != 'output_dir'})
|
|
415
|
+
|
|
416
|
+
# Optional shape normalization (pad/crop) to a target (x, y, z)
|
|
417
|
+
target_voxel_shape = kwargs.get("target_voxel_shape", None)
|
|
418
|
+
if target_voxel_shape is not None:
|
|
419
|
+
try:
|
|
420
|
+
from ..utils.shape import normalize_voxcity_shape # late import to avoid cycles
|
|
421
|
+
align_xy = kwargs.get("pad_align_xy", "center")
|
|
422
|
+
allow_crop_xy = bool(kwargs.get("allow_crop_xy", True))
|
|
423
|
+
allow_crop_z = bool(kwargs.get("allow_crop_z", False))
|
|
424
|
+
pad_values = kwargs.get("pad_values", None)
|
|
425
|
+
city = normalize_voxcity_shape(
|
|
426
|
+
city,
|
|
427
|
+
tuple(target_voxel_shape),
|
|
428
|
+
align_xy=align_xy,
|
|
429
|
+
pad_values=pad_values,
|
|
430
|
+
allow_crop_xy=allow_crop_xy,
|
|
431
|
+
allow_crop_z=allow_crop_z,
|
|
432
|
+
)
|
|
433
|
+
try:
|
|
434
|
+
_logger.info("Applied target voxel shape %s -> final voxel shape %s", tuple(target_voxel_shape), tuple(city.voxels.classes.shape))
|
|
435
|
+
except Exception:
|
|
436
|
+
pass
|
|
437
|
+
except Exception as e:
|
|
438
|
+
try:
|
|
439
|
+
_logger.warning("Shape normalization skipped due to error: %s", str(e))
|
|
440
|
+
except Exception:
|
|
441
|
+
pass
|
|
442
|
+
|
|
443
|
+
# Backwards compatible save flag: prefer correct key, fallback to legacy misspelling
|
|
444
|
+
_save_flag = io_options.get("save_voxcity_data", kwargs.get("save_voxcity_data", kwargs.get("save_voxctiy_data", True)))
|
|
445
|
+
if _save_flag:
|
|
446
|
+
# Prefer explicit save_path if provided; fall back to legacy save_data_path; else default
|
|
447
|
+
save_path = (
|
|
448
|
+
io_options.get("save_path")
|
|
449
|
+
or kwargs.get("save_path")
|
|
450
|
+
or io_options.get("save_data_path")
|
|
451
|
+
or kwargs.get("save_data_path")
|
|
452
|
+
or f"{output_dir}/voxcity.pkl"
|
|
453
|
+
)
|
|
454
|
+
save_voxcity(save_path, city)
|
|
455
|
+
|
|
456
|
+
# Attach selected sources (final resolved) to extras for downstream consumers
|
|
457
|
+
try:
|
|
458
|
+
city.extras['selected_sources'] = {
|
|
459
|
+
'building_source': building_source,
|
|
460
|
+
'building_complementary_source': building_complementary_source or 'None',
|
|
461
|
+
'land_cover_source': land_cover_source,
|
|
462
|
+
'canopy_height_source': canopy_height_source,
|
|
463
|
+
'dem_source': dem_source,
|
|
464
|
+
'building_complement_height': kwargs.get('building_complement_height'),
|
|
465
|
+
}
|
|
466
|
+
except Exception:
|
|
467
|
+
pass
|
|
468
|
+
|
|
469
|
+
return city
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def get_voxcity_CityGML(rectangle_vertices, land_cover_source, canopy_height_source, meshsize, url_citygml=None, citygml_path=None, **kwargs):
|
|
473
|
+
output_dir = kwargs.get("output_dir", "output")
|
|
474
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
475
|
+
kwargs.pop('output_dir', None)
|
|
476
|
+
|
|
477
|
+
ssl_verify = kwargs.pop('ssl_verify', kwargs.pop('verify', True))
|
|
478
|
+
ca_bundle = kwargs.pop('ca_bundle', None)
|
|
479
|
+
timeout = kwargs.pop('timeout', 60)
|
|
480
|
+
|
|
481
|
+
building_gdf, terrain_gdf, vegetation_gdf = load_buid_dem_veg_from_citygml(
|
|
482
|
+
url=url_citygml,
|
|
483
|
+
citygml_path=citygml_path,
|
|
484
|
+
base_dir=output_dir,
|
|
485
|
+
rectangle_vertices=rectangle_vertices,
|
|
486
|
+
ssl_verify=ssl_verify,
|
|
487
|
+
ca_bundle=ca_bundle,
|
|
488
|
+
timeout=timeout
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
try:
|
|
492
|
+
import geopandas as gpd # noqa: F401
|
|
493
|
+
if building_gdf is not None:
|
|
494
|
+
if building_gdf.crs is None:
|
|
495
|
+
building_gdf = building_gdf.set_crs(epsg=4326)
|
|
496
|
+
elif getattr(building_gdf.crs, 'to_epsg', lambda: None)() != 4326 and building_gdf.crs != "EPSG:4326":
|
|
497
|
+
building_gdf = building_gdf.to_crs(epsg=4326)
|
|
498
|
+
if terrain_gdf is not None:
|
|
499
|
+
if terrain_gdf.crs is None:
|
|
500
|
+
terrain_gdf = terrain_gdf.set_crs(epsg=4326)
|
|
501
|
+
elif getattr(terrain_gdf.crs, 'to_epsg', lambda: None)() != 4326 and terrain_gdf.crs != "EPSG:4326":
|
|
502
|
+
terrain_gdf = terrain_gdf.to_crs(epsg=4326)
|
|
503
|
+
if vegetation_gdf is not None:
|
|
504
|
+
if vegetation_gdf.crs is None:
|
|
505
|
+
vegetation_gdf = vegetation_gdf.set_crs(epsg=4326)
|
|
506
|
+
elif getattr(vegetation_gdf.crs, 'to_epsg', lambda: None)() != 4326 and vegetation_gdf.crs != "EPSG:4326":
|
|
507
|
+
vegetation_gdf = vegetation_gdf.to_crs(epsg=4326)
|
|
508
|
+
except Exception:
|
|
509
|
+
pass
|
|
510
|
+
|
|
511
|
+
land_cover_grid = get_land_cover_grid(rectangle_vertices, meshsize, land_cover_source, output_dir, **kwargs)
|
|
512
|
+
|
|
513
|
+
print("Creating building height grid")
|
|
514
|
+
building_complementary_source = kwargs.get("building_complementary_source")
|
|
515
|
+
gdf_comp = None
|
|
516
|
+
geotiff_path_comp = None
|
|
517
|
+
complement_building_footprints = kwargs.get("complement_building_footprints")
|
|
518
|
+
if complement_building_footprints is None and (building_complementary_source not in (None, "None")):
|
|
519
|
+
complement_building_footprints = True
|
|
520
|
+
|
|
521
|
+
if (building_complementary_source is not None) and (building_complementary_source != "None"):
|
|
522
|
+
floor_height = kwargs.get("floor_height", 3.0)
|
|
523
|
+
if building_complementary_source == 'Microsoft Building Footprints':
|
|
524
|
+
gdf_comp = get_mbfp_gdf(kwargs.get("output_dir", "output"), rectangle_vertices)
|
|
525
|
+
elif building_complementary_source == 'OpenStreetMap':
|
|
526
|
+
gdf_comp = load_gdf_from_openstreetmap(rectangle_vertices, floor_height=floor_height)
|
|
527
|
+
elif building_complementary_source == 'EUBUCCO v0.1':
|
|
528
|
+
gdf_comp = load_gdf_from_eubucco(rectangle_vertices, kwargs.get("output_dir", "output"))
|
|
529
|
+
elif building_complementary_source == 'Overture':
|
|
530
|
+
gdf_comp = load_gdf_from_overture(rectangle_vertices, floor_height=floor_height)
|
|
531
|
+
elif building_complementary_source in ("GBA", "Global Building Atlas"):
|
|
532
|
+
clip_gba = kwargs.get("gba_clip", False)
|
|
533
|
+
gba_download_dir = kwargs.get("gba_download_dir")
|
|
534
|
+
gdf_comp = load_gdf_from_gba(rectangle_vertices, download_dir=gba_download_dir, clip_to_rectangle=clip_gba)
|
|
535
|
+
elif building_complementary_source == 'Local file':
|
|
536
|
+
comp_path = kwargs.get("building_complementary_path")
|
|
537
|
+
if comp_path is not None:
|
|
538
|
+
_, extension = os.path.splitext(comp_path)
|
|
539
|
+
if extension == ".gpkg":
|
|
540
|
+
gdf_comp = get_gdf_from_gpkg(comp_path, rectangle_vertices)
|
|
541
|
+
if gdf_comp is not None:
|
|
542
|
+
try:
|
|
543
|
+
if gdf_comp.crs is None:
|
|
544
|
+
gdf_comp = gdf_comp.set_crs(epsg=4326)
|
|
545
|
+
elif getattr(gdf_comp.crs, 'to_epsg', lambda: None)() != 4326 and gdf_comp.crs != "EPSG:4326":
|
|
546
|
+
gdf_comp = gdf_comp.to_crs(epsg=4326)
|
|
547
|
+
except Exception:
|
|
548
|
+
pass
|
|
549
|
+
elif building_complementary_source == "Open Building 2.5D Temporal":
|
|
550
|
+
roi = get_roi(rectangle_vertices)
|
|
551
|
+
os.makedirs(kwargs.get("output_dir", "output"), exist_ok=True)
|
|
552
|
+
geotiff_path_comp = os.path.join(kwargs.get("output_dir", "output"), "building_height.tif")
|
|
553
|
+
save_geotiff_open_buildings_temporal(roi, geotiff_path_comp)
|
|
554
|
+
elif building_complementary_source in ["England 1m DSM - DTM", "Netherlands 0.5m DSM - DTM"]:
|
|
555
|
+
roi = get_roi(rectangle_vertices)
|
|
556
|
+
os.makedirs(kwargs.get("output_dir", "output"), exist_ok=True)
|
|
557
|
+
geotiff_path_comp = os.path.join(kwargs.get("output_dir", "output"), "building_height.tif")
|
|
558
|
+
save_geotiff_dsm_minus_dtm(roi, geotiff_path_comp, meshsize, building_complementary_source)
|
|
559
|
+
|
|
560
|
+
_allowed_building_kwargs = {
|
|
561
|
+
"overlapping_footprint",
|
|
562
|
+
"gdf_comp",
|
|
563
|
+
"geotiff_path_comp",
|
|
564
|
+
"complement_building_footprints",
|
|
565
|
+
"complement_height",
|
|
566
|
+
}
|
|
567
|
+
_building_kwargs = {k: v for k, v in kwargs.items() if k in _allowed_building_kwargs}
|
|
568
|
+
if gdf_comp is not None:
|
|
569
|
+
_building_kwargs["gdf_comp"] = gdf_comp
|
|
570
|
+
if geotiff_path_comp is not None:
|
|
571
|
+
_building_kwargs["geotiff_path_comp"] = geotiff_path_comp
|
|
572
|
+
if complement_building_footprints is not None:
|
|
573
|
+
_building_kwargs["complement_building_footprints"] = complement_building_footprints
|
|
574
|
+
|
|
575
|
+
comp_height_user = kwargs.get("building_complement_height")
|
|
576
|
+
if comp_height_user is not None:
|
|
577
|
+
_building_kwargs["complement_height"] = comp_height_user
|
|
578
|
+
if _building_kwargs.get("complement_building_footprints") and ("complement_height" not in _building_kwargs):
|
|
579
|
+
_building_kwargs["complement_height"] = 10.0
|
|
580
|
+
|
|
581
|
+
building_height_grid, building_min_height_grid, building_id_grid, filtered_buildings = create_building_height_grid_from_gdf_polygon(
|
|
582
|
+
building_gdf, meshsize, rectangle_vertices, **_building_kwargs
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
grid_vis = kwargs.get("gridvis", True)
|
|
586
|
+
if grid_vis:
|
|
587
|
+
building_height_grid_nan = building_height_grid.copy()
|
|
588
|
+
building_height_grid_nan[building_height_grid_nan == 0] = np.nan
|
|
589
|
+
visualize_numerical_grid(np.flipud(building_height_grid_nan), meshsize, "building height (m)", cmap='viridis', label='Value')
|
|
590
|
+
|
|
591
|
+
if canopy_height_source == "Static":
|
|
592
|
+
canopy_height_grid_comp = np.zeros_like(land_cover_grid, dtype=float)
|
|
593
|
+
static_tree_height = kwargs.get("static_tree_height", 10.0)
|
|
594
|
+
_classes = get_land_cover_classes(land_cover_source)
|
|
595
|
+
_class_to_int = {name: i for i, name in enumerate(_classes.values())}
|
|
596
|
+
_tree_labels = ["Tree", "Trees", "Tree Canopy"]
|
|
597
|
+
_tree_indices = [_class_to_int[label] for label in _tree_labels if label in _class_to_int]
|
|
598
|
+
tree_mask = np.isin(land_cover_grid, _tree_indices) if _tree_indices else np.zeros_like(land_cover_grid, dtype=bool)
|
|
599
|
+
canopy_height_grid_comp[tree_mask] = static_tree_height
|
|
600
|
+
trunk_height_ratio = kwargs.get("trunk_height_ratio")
|
|
601
|
+
if trunk_height_ratio is None:
|
|
602
|
+
trunk_height_ratio = 11.76 / 19.98
|
|
603
|
+
canopy_bottom_height_grid_comp = canopy_height_grid_comp * float(trunk_height_ratio)
|
|
604
|
+
else:
|
|
605
|
+
from .grids import get_canopy_height_grid
|
|
606
|
+
canopy_height_grid_comp, canopy_bottom_height_grid_comp = get_canopy_height_grid(rectangle_vertices, meshsize, canopy_height_source, output_dir, **kwargs)
|
|
607
|
+
|
|
608
|
+
if vegetation_gdf is not None:
|
|
609
|
+
canopy_height_grid = create_vegetation_height_grid_from_gdf_polygon(vegetation_gdf, meshsize, rectangle_vertices)
|
|
610
|
+
trunk_height_ratio = kwargs.get("trunk_height_ratio")
|
|
611
|
+
if trunk_height_ratio is None:
|
|
612
|
+
trunk_height_ratio = 11.76 / 19.98
|
|
613
|
+
canopy_bottom_height_grid = canopy_height_grid * float(trunk_height_ratio)
|
|
614
|
+
else:
|
|
615
|
+
canopy_height_grid = np.zeros_like(building_height_grid)
|
|
616
|
+
canopy_bottom_height_grid = np.zeros_like(building_height_grid)
|
|
617
|
+
|
|
618
|
+
mask = (canopy_height_grid == 0) & (canopy_height_grid_comp != 0)
|
|
619
|
+
canopy_height_grid[mask] = canopy_height_grid_comp[mask]
|
|
620
|
+
mask_b = (canopy_bottom_height_grid == 0) & (canopy_bottom_height_grid_comp != 0)
|
|
621
|
+
canopy_bottom_height_grid[mask_b] = canopy_bottom_height_grid_comp[mask_b]
|
|
622
|
+
canopy_bottom_height_grid = np.minimum(canopy_bottom_height_grid, canopy_height_grid)
|
|
623
|
+
|
|
624
|
+
if kwargs.pop('flat_dem', None):
|
|
625
|
+
dem_grid = np.zeros_like(land_cover_grid)
|
|
626
|
+
else:
|
|
627
|
+
print("Creating Digital Elevation Model (DEM) grid")
|
|
628
|
+
dem_grid = create_dem_grid_from_gdf_polygon(terrain_gdf, meshsize, rectangle_vertices)
|
|
629
|
+
grid_vis = kwargs.get("gridvis", True)
|
|
630
|
+
if grid_vis:
|
|
631
|
+
visualize_numerical_grid(np.flipud(dem_grid), meshsize, title='Digital Elevation Model', cmap='terrain', label='Elevation (m)')
|
|
632
|
+
|
|
633
|
+
min_canopy_height = kwargs.get("min_canopy_height")
|
|
634
|
+
if min_canopy_height is not None:
|
|
635
|
+
canopy_height_grid[canopy_height_grid < kwargs["min_canopy_height"]] = 0
|
|
636
|
+
canopy_bottom_height_grid[canopy_height_grid == 0] = 0
|
|
637
|
+
|
|
638
|
+
remove_perimeter_object = kwargs.get("remove_perimeter_object")
|
|
639
|
+
if (remove_perimeter_object is not None) and (remove_perimeter_object > 0):
|
|
640
|
+
print("apply perimeter removal")
|
|
641
|
+
w_peri = int(remove_perimeter_object * building_height_grid.shape[0] + 0.5)
|
|
642
|
+
h_peri = int(remove_perimeter_object * building_height_grid.shape[1] + 0.5)
|
|
643
|
+
|
|
644
|
+
canopy_height_grid[:w_peri, :] = canopy_height_grid[-w_peri:, :] = canopy_height_grid[:, :h_peri] = canopy_height_grid[:, -h_peri:] = 0
|
|
645
|
+
canopy_bottom_height_grid[:w_peri, :] = canopy_bottom_height_grid[-w_peri:, :] = canopy_bottom_height_grid[:, :h_peri] = canopy_bottom_height_grid[:, -h_peri:] = 0
|
|
646
|
+
|
|
647
|
+
ids1 = np.unique(building_id_grid[:w_peri, :][building_id_grid[:w_peri, :] > 0])
|
|
648
|
+
ids2 = np.unique(building_id_grid[-w_peri:, :][building_id_grid[-w_peri:, :] > 0])
|
|
649
|
+
ids3 = np.unique(building_id_grid[:, :h_peri][building_id_grid[:, :h_peri] > 0])
|
|
650
|
+
ids4 = np.unique(building_id_grid[:, -h_peri:][building_id_grid[:, -h_peri:] > 0])
|
|
651
|
+
remove_ids = np.concatenate((ids1, ids2, ids3, ids4))
|
|
652
|
+
|
|
653
|
+
for remove_id in remove_ids:
|
|
654
|
+
positions = np.where(building_id_grid == remove_id)
|
|
655
|
+
building_height_grid[positions] = 0
|
|
656
|
+
building_min_height_grid[positions] = [[] for _ in range(len(building_min_height_grid[positions]))]
|
|
657
|
+
|
|
658
|
+
grid_vis = kwargs.get("gridvis", True)
|
|
659
|
+
if grid_vis:
|
|
660
|
+
building_height_grid_nan = building_height_grid.copy()
|
|
661
|
+
building_height_grid_nan[building_height_grid_nan == 0] = np.nan
|
|
662
|
+
visualize_numerical_grid(
|
|
663
|
+
np.flipud(building_height_grid_nan),
|
|
664
|
+
meshsize,
|
|
665
|
+
"building height (m)",
|
|
666
|
+
cmap='viridis',
|
|
667
|
+
label='Value'
|
|
668
|
+
)
|
|
669
|
+
canopy_height_grid_nan = canopy_height_grid.copy()
|
|
670
|
+
canopy_height_grid_nan[canopy_height_grid_nan == 0] = np.nan
|
|
671
|
+
visualize_numerical_grid(
|
|
672
|
+
np.flipud(canopy_height_grid_nan),
|
|
673
|
+
meshsize,
|
|
674
|
+
"Tree canopy height (m)",
|
|
675
|
+
cmap='Greens',
|
|
676
|
+
label='Tree canopy height (m)'
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
from .voxelizer import Voxelizer
|
|
680
|
+
voxelizer = Voxelizer(
|
|
681
|
+
voxel_size=meshsize,
|
|
682
|
+
land_cover_source=land_cover_source,
|
|
683
|
+
trunk_height_ratio=kwargs.get("trunk_height_ratio"),
|
|
684
|
+
)
|
|
685
|
+
voxcity_grid = voxelizer.generate_combined(
|
|
686
|
+
building_height_grid_ori=building_height_grid,
|
|
687
|
+
building_min_height_grid_ori=building_min_height_grid,
|
|
688
|
+
building_id_grid_ori=building_id_grid,
|
|
689
|
+
land_cover_grid_ori=land_cover_grid,
|
|
690
|
+
dem_grid_ori=dem_grid,
|
|
691
|
+
tree_grid_ori=canopy_height_grid,
|
|
692
|
+
canopy_bottom_height_grid_ori=locals().get("canopy_bottom_height_grid"),
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
from .pipeline import VoxCityPipeline as _Pipeline
|
|
696
|
+
pipeline = _Pipeline(meshsize=meshsize, rectangle_vertices=rectangle_vertices)
|
|
697
|
+
city = pipeline.assemble_voxcity(
|
|
698
|
+
voxcity_grid=voxcity_grid,
|
|
699
|
+
building_height_grid=building_height_grid,
|
|
700
|
+
building_min_height_grid=building_min_height_grid,
|
|
701
|
+
building_id_grid=building_id_grid,
|
|
702
|
+
land_cover_grid=land_cover_grid,
|
|
703
|
+
dem_grid=dem_grid,
|
|
704
|
+
canopy_height_top=canopy_height_grid,
|
|
705
|
+
canopy_height_bottom=locals().get("canopy_bottom_height_grid"),
|
|
706
|
+
extras={"building_gdf": building_gdf},
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
# Backwards compatible save flag: prefer correct key, fallback to legacy misspelling
|
|
710
|
+
_save_flag = kwargs.get("save_voxcity_data", kwargs.get("save_voxctiy_data", True))
|
|
711
|
+
if _save_flag:
|
|
712
|
+
save_path = (
|
|
713
|
+
kwargs.get("save_path")
|
|
714
|
+
or kwargs.get("save_data_path")
|
|
715
|
+
or f"{output_dir}/voxcity.pkl"
|
|
716
|
+
)
|
|
717
|
+
save_voxcity(save_path, city)
|
|
718
|
+
|
|
719
|
+
return city
|
|
720
|
+
|
|
721
|
+
|