voxcity 0.7.0__py3-none-any.whl → 1.0.13__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.
Files changed (81) hide show
  1. voxcity/__init__.py +14 -14
  2. voxcity/downloader/ocean.py +559 -0
  3. voxcity/exporter/__init__.py +12 -12
  4. voxcity/exporter/cityles.py +633 -633
  5. voxcity/exporter/envimet.py +733 -728
  6. voxcity/exporter/magicavoxel.py +333 -333
  7. voxcity/exporter/netcdf.py +238 -238
  8. voxcity/exporter/obj.py +1480 -1480
  9. voxcity/generator/__init__.py +47 -44
  10. voxcity/generator/api.py +727 -675
  11. voxcity/generator/grids.py +394 -379
  12. voxcity/generator/io.py +94 -94
  13. voxcity/generator/pipeline.py +582 -282
  14. voxcity/generator/update.py +429 -0
  15. voxcity/generator/voxelizer.py +18 -6
  16. voxcity/geoprocessor/__init__.py +75 -75
  17. voxcity/geoprocessor/draw.py +1494 -1219
  18. voxcity/geoprocessor/merge_utils.py +91 -91
  19. voxcity/geoprocessor/mesh.py +806 -806
  20. voxcity/geoprocessor/network.py +708 -708
  21. voxcity/geoprocessor/raster/__init__.py +2 -0
  22. voxcity/geoprocessor/raster/buildings.py +435 -428
  23. voxcity/geoprocessor/raster/core.py +31 -0
  24. voxcity/geoprocessor/raster/export.py +93 -93
  25. voxcity/geoprocessor/raster/landcover.py +178 -51
  26. voxcity/geoprocessor/raster/raster.py +1 -1
  27. voxcity/geoprocessor/utils.py +824 -824
  28. voxcity/models.py +115 -113
  29. voxcity/simulator/solar/__init__.py +66 -43
  30. voxcity/simulator/solar/integration.py +336 -336
  31. voxcity/simulator/solar/sky.py +668 -0
  32. voxcity/simulator/solar/temporal.py +792 -434
  33. voxcity/simulator_gpu/__init__.py +115 -0
  34. voxcity/simulator_gpu/common/__init__.py +9 -0
  35. voxcity/simulator_gpu/common/geometry.py +11 -0
  36. voxcity/simulator_gpu/core.py +322 -0
  37. voxcity/simulator_gpu/domain.py +262 -0
  38. voxcity/simulator_gpu/environment.yml +11 -0
  39. voxcity/simulator_gpu/init_taichi.py +154 -0
  40. voxcity/simulator_gpu/integration.py +15 -0
  41. voxcity/simulator_gpu/kernels.py +56 -0
  42. voxcity/simulator_gpu/radiation.py +28 -0
  43. voxcity/simulator_gpu/raytracing.py +623 -0
  44. voxcity/simulator_gpu/sky.py +9 -0
  45. voxcity/simulator_gpu/solar/__init__.py +178 -0
  46. voxcity/simulator_gpu/solar/core.py +66 -0
  47. voxcity/simulator_gpu/solar/csf.py +1249 -0
  48. voxcity/simulator_gpu/solar/domain.py +561 -0
  49. voxcity/simulator_gpu/solar/epw.py +421 -0
  50. voxcity/simulator_gpu/solar/integration.py +2953 -0
  51. voxcity/simulator_gpu/solar/radiation.py +3019 -0
  52. voxcity/simulator_gpu/solar/raytracing.py +686 -0
  53. voxcity/simulator_gpu/solar/reflection.py +533 -0
  54. voxcity/simulator_gpu/solar/sky.py +907 -0
  55. voxcity/simulator_gpu/solar/solar.py +337 -0
  56. voxcity/simulator_gpu/solar/svf.py +446 -0
  57. voxcity/simulator_gpu/solar/volumetric.py +1151 -0
  58. voxcity/simulator_gpu/solar/voxcity.py +2953 -0
  59. voxcity/simulator_gpu/temporal.py +13 -0
  60. voxcity/simulator_gpu/utils.py +25 -0
  61. voxcity/simulator_gpu/view.py +32 -0
  62. voxcity/simulator_gpu/visibility/__init__.py +109 -0
  63. voxcity/simulator_gpu/visibility/geometry.py +278 -0
  64. voxcity/simulator_gpu/visibility/integration.py +808 -0
  65. voxcity/simulator_gpu/visibility/landmark.py +753 -0
  66. voxcity/simulator_gpu/visibility/view.py +944 -0
  67. voxcity/utils/__init__.py +11 -0
  68. voxcity/utils/classes.py +194 -0
  69. voxcity/utils/lc.py +80 -39
  70. voxcity/utils/shape.py +230 -0
  71. voxcity/visualizer/__init__.py +24 -24
  72. voxcity/visualizer/builder.py +43 -43
  73. voxcity/visualizer/grids.py +141 -141
  74. voxcity/visualizer/maps.py +187 -187
  75. voxcity/visualizer/renderer.py +1146 -928
  76. {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/METADATA +56 -52
  77. voxcity-1.0.13.dist-info/RECORD +116 -0
  78. voxcity-0.7.0.dist-info/RECORD +0 -77
  79. {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/WHEEL +0 -0
  80. {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/licenses/AUTHORS.rst +0 -0
  81. {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/licenses/LICENSE +0 -0
voxcity/generator/api.py CHANGED
@@ -1,675 +1,727 @@
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
- Returns:
267
- VoxCity object containing the generated 3D city model
268
- """
269
-
270
- # Check if building_complementary_source was provided via kwargs (for backward compatibility)
271
- if building_complementary_source is None and 'building_complementary_source' in kwargs:
272
- building_complementary_source = kwargs.pop('building_complementary_source')
273
-
274
- # Determine if we need to auto-select any sources
275
- sources_to_select = []
276
- if building_source is None:
277
- sources_to_select.append('building_source')
278
- if land_cover_source is None:
279
- sources_to_select.append('land_cover_source')
280
- if canopy_height_source is None:
281
- sources_to_select.append('canopy_height_source')
282
- if dem_source is None:
283
- sources_to_select.append('dem_source')
284
- if building_complementary_source is None:
285
- sources_to_select.append('building_complementary_source')
286
-
287
- # Auto-select missing sources if needed
288
- if sources_to_select:
289
- _logger.info("Auto-selecting data sources for: %s", ", ".join(sources_to_select))
290
- auto_sources = auto_select_data_sources(rectangle_vertices)
291
-
292
- # Check Earth Engine availability for auto-selected sources
293
- ee_available = True
294
- try:
295
- from ..downloader.gee import initialize_earth_engine
296
- initialize_earth_engine()
297
- except Exception:
298
- ee_available = False
299
-
300
- if not ee_available:
301
- # Downgrade EE-dependent sources
302
- if auto_sources['land_cover_source'] not in ('OpenStreetMap', 'OpenEarthMapJapan'):
303
- auto_sources['land_cover_source'] = 'OpenStreetMap'
304
- auto_sources['canopy_height_source'] = 'Static'
305
- auto_sources['dem_source'] = 'Flat'
306
- ee_dependent_comp = {
307
- 'Open Building 2.5D Temporal',
308
- 'England 1m DSM - DTM',
309
- 'Netherlands 0.5m DSM - DTM',
310
- }
311
- if auto_sources.get('building_complementary_source') in ee_dependent_comp:
312
- auto_sources['building_complementary_source'] = 'Microsoft Building Footprints'
313
-
314
- # Apply auto-selected sources only where not specified
315
- if building_source is None:
316
- building_source = auto_sources['building_source']
317
- if land_cover_source is None:
318
- land_cover_source = auto_sources['land_cover_source']
319
- if canopy_height_source is None:
320
- canopy_height_source = auto_sources['canopy_height_source']
321
- if dem_source is None:
322
- dem_source = auto_sources['dem_source']
323
- if building_complementary_source is None:
324
- building_complementary_source = auto_sources.get('building_complementary_source', 'None')
325
-
326
- # Auto-set complement height if not provided
327
- if 'building_complement_height' not in kwargs:
328
- kwargs['building_complement_height'] = 10
329
-
330
- # Log selected sources
331
- try:
332
- _logger.info("Selected data sources:")
333
- b_base_url = _url_for_source(building_source)
334
- _logger.info("- Buildings(base)=%s%s", building_source, f" | {b_base_url}" if b_base_url else "")
335
- b_comp_url = _url_for_source(building_complementary_source)
336
- _logger.info("- Buildings(comp)=%s%s", building_complementary_source, f" | {b_comp_url}" if b_comp_url else "")
337
- lc_url = _url_for_source(land_cover_source)
338
- _logger.info("- LandCover=%s%s", land_cover_source, f" | {lc_url}" if lc_url else "")
339
- canopy_url = _url_for_source(canopy_height_source)
340
- _logger.info("- Canopy=%s%s", canopy_height_source, f" | {canopy_url}" if canopy_url else "")
341
- dem_url = _url_for_source(dem_source)
342
- _logger.info("- DEM=%s%s", dem_source, f" | {dem_url}" if dem_url else "")
343
- _logger.info("- ComplementHeight=%s", kwargs.get('building_complement_height'))
344
- except Exception:
345
- pass
346
-
347
- # Ensure building_complementary_source is passed through kwargs
348
- if building_complementary_source is not None:
349
- kwargs['building_complementary_source'] = building_complementary_source
350
-
351
- # Default DEM interpolation to True unless explicitly provided
352
- if 'dem_interpolation' not in kwargs:
353
- kwargs['dem_interpolation'] = True
354
-
355
- output_dir = kwargs.get("output_dir", "output")
356
- # Group incoming kwargs into structured options for consistency
357
- land_cover_keys = {
358
- # examples: source-specific options (placeholders kept broad for back-compat)
359
- "land_cover_path", "land_cover_resample", "land_cover_classes",
360
- }
361
- building_keys = {
362
- "overlapping_footprint", "gdf_comp", "geotiff_path_comp",
363
- "complement_building_footprints", "complement_height", "floor_height",
364
- "building_complementary_source", "building_complement_height",
365
- "building_complementary_path", "gba_clip", "gba_download_dir",
366
- }
367
- canopy_keys = {
368
- "min_canopy_height", "trunk_height_ratio", "static_tree_height",
369
- }
370
- dem_keys = {
371
- "flat_dem",
372
- }
373
- visualize_keys = {"gridvis", "mapvis"}
374
- io_keys = {"save_voxcity_data", "save_voxctiy_data", "save_data_path"}
375
-
376
- land_cover_options = {k: v for k, v in kwargs.items() if k in land_cover_keys}
377
- building_options = {k: v for k, v in kwargs.items() if k in building_keys}
378
- canopy_options = {k: v for k, v in kwargs.items() if k in canopy_keys}
379
- dem_options = {k: v for k, v in kwargs.items() if k in dem_keys}
380
- # Auto-set flat DEM when dem_source is None/empty and user didn't specify
381
- if (dem_source in (None, "", "None")) and ("flat_dem" not in dem_options):
382
- dem_options["flat_dem"] = True
383
- visualize_options = {k: v for k, v in kwargs.items() if k in visualize_keys}
384
- io_options = {k: v for k, v in kwargs.items() if k in io_keys}
385
-
386
- cfg = PipelineConfig(
387
- rectangle_vertices=rectangle_vertices,
388
- meshsize=float(meshsize),
389
- building_source=building_source,
390
- land_cover_source=land_cover_source,
391
- canopy_height_source=canopy_height_source,
392
- dem_source=dem_source,
393
- output_dir=output_dir,
394
- trunk_height_ratio=kwargs.get("trunk_height_ratio"),
395
- static_tree_height=kwargs.get("static_tree_height"),
396
- remove_perimeter_object=kwargs.get("remove_perimeter_object"),
397
- mapvis=bool(kwargs.get("mapvis", False)),
398
- gridvis=bool(kwargs.get("gridvis", True)),
399
- land_cover_options=land_cover_options,
400
- building_options=building_options,
401
- canopy_options=canopy_options,
402
- dem_options=dem_options,
403
- io_options=io_options,
404
- visualize_options=visualize_options,
405
- )
406
- 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'})
407
-
408
- # Backwards compatible save flag: prefer correct key, fallback to legacy misspelling
409
- _save_flag = io_options.get("save_voxcity_data", kwargs.get("save_voxcity_data", kwargs.get("save_voxctiy_data", True)))
410
- if _save_flag:
411
- save_path = io_options.get("save_data_path", kwargs.get("save_data_path", f"{output_dir}/voxcity.pkl"))
412
- save_voxcity(save_path, city)
413
-
414
- # Attach selected sources (final resolved) to extras for downstream consumers
415
- try:
416
- city.extras['selected_sources'] = {
417
- 'building_source': building_source,
418
- 'building_complementary_source': building_complementary_source or 'None',
419
- 'land_cover_source': land_cover_source,
420
- 'canopy_height_source': canopy_height_source,
421
- 'dem_source': dem_source,
422
- 'building_complement_height': kwargs.get('building_complement_height'),
423
- }
424
- except Exception:
425
- pass
426
-
427
- return city
428
-
429
-
430
- def get_voxcity_CityGML(rectangle_vertices, land_cover_source, canopy_height_source, meshsize, url_citygml=None, citygml_path=None, **kwargs):
431
- output_dir = kwargs.get("output_dir", "output")
432
- os.makedirs(output_dir, exist_ok=True)
433
- kwargs.pop('output_dir', None)
434
-
435
- ssl_verify = kwargs.pop('ssl_verify', kwargs.pop('verify', True))
436
- ca_bundle = kwargs.pop('ca_bundle', None)
437
- timeout = kwargs.pop('timeout', 60)
438
-
439
- building_gdf, terrain_gdf, vegetation_gdf = load_buid_dem_veg_from_citygml(
440
- url=url_citygml,
441
- citygml_path=citygml_path,
442
- base_dir=output_dir,
443
- rectangle_vertices=rectangle_vertices,
444
- ssl_verify=ssl_verify,
445
- ca_bundle=ca_bundle,
446
- timeout=timeout
447
- )
448
-
449
- try:
450
- import geopandas as gpd # noqa: F401
451
- if building_gdf is not None:
452
- if building_gdf.crs is None:
453
- building_gdf = building_gdf.set_crs(epsg=4326)
454
- elif getattr(building_gdf.crs, 'to_epsg', lambda: None)() != 4326 and building_gdf.crs != "EPSG:4326":
455
- building_gdf = building_gdf.to_crs(epsg=4326)
456
- if terrain_gdf is not None:
457
- if terrain_gdf.crs is None:
458
- terrain_gdf = terrain_gdf.set_crs(epsg=4326)
459
- elif getattr(terrain_gdf.crs, 'to_epsg', lambda: None)() != 4326 and terrain_gdf.crs != "EPSG:4326":
460
- terrain_gdf = terrain_gdf.to_crs(epsg=4326)
461
- if vegetation_gdf is not None:
462
- if vegetation_gdf.crs is None:
463
- vegetation_gdf = vegetation_gdf.set_crs(epsg=4326)
464
- elif getattr(vegetation_gdf.crs, 'to_epsg', lambda: None)() != 4326 and vegetation_gdf.crs != "EPSG:4326":
465
- vegetation_gdf = vegetation_gdf.to_crs(epsg=4326)
466
- except Exception:
467
- pass
468
-
469
- land_cover_grid = get_land_cover_grid(rectangle_vertices, meshsize, land_cover_source, output_dir, **kwargs)
470
-
471
- print("Creating building height grid")
472
- building_complementary_source = kwargs.get("building_complementary_source")
473
- gdf_comp = None
474
- geotiff_path_comp = None
475
- complement_building_footprints = kwargs.get("complement_building_footprints")
476
- if complement_building_footprints is None and (building_complementary_source not in (None, "None")):
477
- complement_building_footprints = True
478
-
479
- if (building_complementary_source is not None) and (building_complementary_source != "None"):
480
- floor_height = kwargs.get("floor_height", 3.0)
481
- if building_complementary_source == 'Microsoft Building Footprints':
482
- gdf_comp = get_mbfp_gdf(kwargs.get("output_dir", "output"), rectangle_vertices)
483
- elif building_complementary_source == 'OpenStreetMap':
484
- gdf_comp = load_gdf_from_openstreetmap(rectangle_vertices, floor_height=floor_height)
485
- elif building_complementary_source == 'EUBUCCO v0.1':
486
- gdf_comp = load_gdf_from_eubucco(rectangle_vertices, kwargs.get("output_dir", "output"))
487
- elif building_complementary_source == 'Overture':
488
- gdf_comp = load_gdf_from_overture(rectangle_vertices, floor_height=floor_height)
489
- elif building_complementary_source in ("GBA", "Global Building Atlas"):
490
- clip_gba = kwargs.get("gba_clip", False)
491
- gba_download_dir = kwargs.get("gba_download_dir")
492
- gdf_comp = load_gdf_from_gba(rectangle_vertices, download_dir=gba_download_dir, clip_to_rectangle=clip_gba)
493
- elif building_complementary_source == 'Local file':
494
- comp_path = kwargs.get("building_complementary_path")
495
- if comp_path is not None:
496
- _, extension = os.path.splitext(comp_path)
497
- if extension == ".gpkg":
498
- gdf_comp = get_gdf_from_gpkg(comp_path, rectangle_vertices)
499
- if gdf_comp is not None:
500
- try:
501
- if gdf_comp.crs is None:
502
- gdf_comp = gdf_comp.set_crs(epsg=4326)
503
- elif getattr(gdf_comp.crs, 'to_epsg', lambda: None)() != 4326 and gdf_comp.crs != "EPSG:4326":
504
- gdf_comp = gdf_comp.to_crs(epsg=4326)
505
- except Exception:
506
- pass
507
- elif building_complementary_source == "Open Building 2.5D Temporal":
508
- roi = get_roi(rectangle_vertices)
509
- os.makedirs(kwargs.get("output_dir", "output"), exist_ok=True)
510
- geotiff_path_comp = os.path.join(kwargs.get("output_dir", "output"), "building_height.tif")
511
- save_geotiff_open_buildings_temporal(roi, geotiff_path_comp)
512
- elif building_complementary_source in ["England 1m DSM - DTM", "Netherlands 0.5m DSM - DTM"]:
513
- roi = get_roi(rectangle_vertices)
514
- os.makedirs(kwargs.get("output_dir", "output"), exist_ok=True)
515
- geotiff_path_comp = os.path.join(kwargs.get("output_dir", "output"), "building_height.tif")
516
- save_geotiff_dsm_minus_dtm(roi, geotiff_path_comp, meshsize, building_complementary_source)
517
-
518
- _allowed_building_kwargs = {
519
- "overlapping_footprint",
520
- "gdf_comp",
521
- "geotiff_path_comp",
522
- "complement_building_footprints",
523
- "complement_height",
524
- }
525
- _building_kwargs = {k: v for k, v in kwargs.items() if k in _allowed_building_kwargs}
526
- if gdf_comp is not None:
527
- _building_kwargs["gdf_comp"] = gdf_comp
528
- if geotiff_path_comp is not None:
529
- _building_kwargs["geotiff_path_comp"] = geotiff_path_comp
530
- if complement_building_footprints is not None:
531
- _building_kwargs["complement_building_footprints"] = complement_building_footprints
532
-
533
- comp_height_user = kwargs.get("building_complement_height")
534
- if comp_height_user is not None:
535
- _building_kwargs["complement_height"] = comp_height_user
536
- if _building_kwargs.get("complement_building_footprints") and ("complement_height" not in _building_kwargs):
537
- _building_kwargs["complement_height"] = 10.0
538
-
539
- building_height_grid, building_min_height_grid, building_id_grid, filtered_buildings = create_building_height_grid_from_gdf_polygon(
540
- building_gdf, meshsize, rectangle_vertices, **_building_kwargs
541
- )
542
-
543
- grid_vis = kwargs.get("gridvis", True)
544
- if grid_vis:
545
- building_height_grid_nan = building_height_grid.copy()
546
- building_height_grid_nan[building_height_grid_nan == 0] = np.nan
547
- visualize_numerical_grid(np.flipud(building_height_grid_nan), meshsize, "building height (m)", cmap='viridis', label='Value')
548
-
549
- if canopy_height_source == "Static":
550
- canopy_height_grid_comp = np.zeros_like(land_cover_grid, dtype=float)
551
- static_tree_height = kwargs.get("static_tree_height", 10.0)
552
- _classes = get_land_cover_classes(land_cover_source)
553
- _class_to_int = {name: i for i, name in enumerate(_classes.values())}
554
- _tree_labels = ["Tree", "Trees", "Tree Canopy"]
555
- _tree_indices = [_class_to_int[label] for label in _tree_labels if label in _class_to_int]
556
- tree_mask = np.isin(land_cover_grid, _tree_indices) if _tree_indices else np.zeros_like(land_cover_grid, dtype=bool)
557
- canopy_height_grid_comp[tree_mask] = static_tree_height
558
- trunk_height_ratio = kwargs.get("trunk_height_ratio")
559
- if trunk_height_ratio is None:
560
- trunk_height_ratio = 11.76 / 19.98
561
- canopy_bottom_height_grid_comp = canopy_height_grid_comp * float(trunk_height_ratio)
562
- else:
563
- from .grids import get_canopy_height_grid
564
- canopy_height_grid_comp, canopy_bottom_height_grid_comp = get_canopy_height_grid(rectangle_vertices, meshsize, canopy_height_source, output_dir, **kwargs)
565
-
566
- if vegetation_gdf is not None:
567
- canopy_height_grid = create_vegetation_height_grid_from_gdf_polygon(vegetation_gdf, meshsize, rectangle_vertices)
568
- trunk_height_ratio = kwargs.get("trunk_height_ratio")
569
- if trunk_height_ratio is None:
570
- trunk_height_ratio = 11.76 / 19.98
571
- canopy_bottom_height_grid = canopy_height_grid * float(trunk_height_ratio)
572
- else:
573
- canopy_height_grid = np.zeros_like(building_height_grid)
574
- canopy_bottom_height_grid = np.zeros_like(building_height_grid)
575
-
576
- mask = (canopy_height_grid == 0) & (canopy_height_grid_comp != 0)
577
- canopy_height_grid[mask] = canopy_height_grid_comp[mask]
578
- mask_b = (canopy_bottom_height_grid == 0) & (canopy_bottom_height_grid_comp != 0)
579
- canopy_bottom_height_grid[mask_b] = canopy_bottom_height_grid_comp[mask_b]
580
- canopy_bottom_height_grid = np.minimum(canopy_bottom_height_grid, canopy_height_grid)
581
-
582
- if kwargs.pop('flat_dem', None):
583
- dem_grid = np.zeros_like(land_cover_grid)
584
- else:
585
- print("Creating Digital Elevation Model (DEM) grid")
586
- dem_grid = create_dem_grid_from_gdf_polygon(terrain_gdf, meshsize, rectangle_vertices)
587
- grid_vis = kwargs.get("gridvis", True)
588
- if grid_vis:
589
- visualize_numerical_grid(np.flipud(dem_grid), meshsize, title='Digital Elevation Model', cmap='terrain', label='Elevation (m)')
590
-
591
- min_canopy_height = kwargs.get("min_canopy_height")
592
- if min_canopy_height is not None:
593
- canopy_height_grid[canopy_height_grid < kwargs["min_canopy_height"]] = 0
594
- canopy_bottom_height_grid[canopy_height_grid == 0] = 0
595
-
596
- remove_perimeter_object = kwargs.get("remove_perimeter_object")
597
- if (remove_perimeter_object is not None) and (remove_perimeter_object > 0):
598
- print("apply perimeter removal")
599
- w_peri = int(remove_perimeter_object * building_height_grid.shape[0] + 0.5)
600
- h_peri = int(remove_perimeter_object * building_height_grid.shape[1] + 0.5)
601
-
602
- canopy_height_grid[:w_peri, :] = canopy_height_grid[-w_peri:, :] = canopy_height_grid[:, :h_peri] = canopy_height_grid[:, -h_peri:] = 0
603
- 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
604
-
605
- ids1 = np.unique(building_id_grid[:w_peri, :][building_id_grid[:w_peri, :] > 0])
606
- ids2 = np.unique(building_id_grid[-w_peri:, :][building_id_grid[-w_peri:, :] > 0])
607
- ids3 = np.unique(building_id_grid[:, :h_peri][building_id_grid[:, :h_peri] > 0])
608
- ids4 = np.unique(building_id_grid[:, -h_peri:][building_id_grid[:, -h_peri:] > 0])
609
- remove_ids = np.concatenate((ids1, ids2, ids3, ids4))
610
-
611
- for remove_id in remove_ids:
612
- positions = np.where(building_id_grid == remove_id)
613
- building_height_grid[positions] = 0
614
- building_min_height_grid[positions] = [[] for _ in range(len(building_min_height_grid[positions]))]
615
-
616
- grid_vis = kwargs.get("gridvis", True)
617
- if grid_vis:
618
- building_height_grid_nan = building_height_grid.copy()
619
- building_height_grid_nan[building_height_grid_nan == 0] = np.nan
620
- visualize_numerical_grid(
621
- np.flipud(building_height_grid_nan),
622
- meshsize,
623
- "building height (m)",
624
- cmap='viridis',
625
- label='Value'
626
- )
627
- canopy_height_grid_nan = canopy_height_grid.copy()
628
- canopy_height_grid_nan[canopy_height_grid_nan == 0] = np.nan
629
- visualize_numerical_grid(
630
- np.flipud(canopy_height_grid_nan),
631
- meshsize,
632
- "Tree canopy height (m)",
633
- cmap='Greens',
634
- label='Tree canopy height (m)'
635
- )
636
-
637
- from .voxelizer import Voxelizer
638
- voxelizer = Voxelizer(
639
- voxel_size=meshsize,
640
- land_cover_source=land_cover_source,
641
- trunk_height_ratio=kwargs.get("trunk_height_ratio"),
642
- )
643
- voxcity_grid = voxelizer.generate_combined(
644
- building_height_grid_ori=building_height_grid,
645
- building_min_height_grid_ori=building_min_height_grid,
646
- building_id_grid_ori=building_id_grid,
647
- land_cover_grid_ori=land_cover_grid,
648
- dem_grid_ori=dem_grid,
649
- tree_grid_ori=canopy_height_grid,
650
- canopy_bottom_height_grid_ori=locals().get("canopy_bottom_height_grid"),
651
- )
652
-
653
- from .pipeline import VoxCityPipeline as _Pipeline
654
- pipeline = _Pipeline(meshsize=meshsize, rectangle_vertices=rectangle_vertices)
655
- city = pipeline.assemble_voxcity(
656
- voxcity_grid=voxcity_grid,
657
- building_height_grid=building_height_grid,
658
- building_min_height_grid=building_min_height_grid,
659
- building_id_grid=building_id_grid,
660
- land_cover_grid=land_cover_grid,
661
- dem_grid=dem_grid,
662
- canopy_height_top=canopy_height_grid,
663
- canopy_height_bottom=locals().get("canopy_bottom_height_grid"),
664
- extras={"building_gdf": building_gdf},
665
- )
666
-
667
- # Backwards compatible save flag: prefer correct key, fallback to legacy misspelling
668
- _save_flag = kwargs.get("save_voxcity_data", kwargs.get("save_voxctiy_data", True))
669
- if _save_flag:
670
- save_path = kwargs.get("save_data_path", f"{output_dir}/voxcity.pkl")
671
- save_voxcity(save_path, city)
672
-
673
- return city
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
+ Performance options include:
266
+ - parallel_download: bool, if True downloads run concurrently (default: False)
267
+ I/O options include:
268
+ - output_dir: Directory for intermediate/downloaded data (default: "output")
269
+ - save_path: Full file path to save the VoxCity object (overrides output_dir default)
270
+ - save_voxcity_data / save_voxctiy_data: bool flag to enable saving (default: True)
271
+
272
+ Returns:
273
+ VoxCity object containing the generated 3D city model
274
+ """
275
+
276
+ # Check if building_complementary_source was provided via kwargs (for backward compatibility)
277
+ if building_complementary_source is None and 'building_complementary_source' in kwargs:
278
+ building_complementary_source = kwargs.pop('building_complementary_source')
279
+
280
+ # Determine if we need to auto-select any sources
281
+ sources_to_select = []
282
+ if building_source is None:
283
+ sources_to_select.append('building_source')
284
+ if land_cover_source is None:
285
+ sources_to_select.append('land_cover_source')
286
+ if canopy_height_source is None:
287
+ sources_to_select.append('canopy_height_source')
288
+ if dem_source is None:
289
+ sources_to_select.append('dem_source')
290
+ if building_complementary_source is None:
291
+ sources_to_select.append('building_complementary_source')
292
+
293
+ # Auto-select missing sources if needed
294
+ if sources_to_select:
295
+ _logger.info("Auto-selecting data sources for: %s", ", ".join(sources_to_select))
296
+ auto_sources = auto_select_data_sources(rectangle_vertices)
297
+
298
+ # Check Earth Engine availability for auto-selected sources
299
+ ee_available = True
300
+ try:
301
+ from ..downloader.gee import initialize_earth_engine
302
+ initialize_earth_engine()
303
+ except Exception:
304
+ ee_available = False
305
+
306
+ if not ee_available:
307
+ # Downgrade EE-dependent sources
308
+ if auto_sources['land_cover_source'] not in ('OpenStreetMap', 'OpenEarthMapJapan'):
309
+ auto_sources['land_cover_source'] = 'OpenStreetMap'
310
+ auto_sources['canopy_height_source'] = 'Static'
311
+ auto_sources['dem_source'] = 'Flat'
312
+ ee_dependent_comp = {
313
+ 'Open Building 2.5D Temporal',
314
+ 'England 1m DSM - DTM',
315
+ 'Netherlands 0.5m DSM - DTM',
316
+ }
317
+ if auto_sources.get('building_complementary_source') in ee_dependent_comp:
318
+ auto_sources['building_complementary_source'] = 'Microsoft Building Footprints'
319
+
320
+ # Apply auto-selected sources only where not specified
321
+ if building_source is None:
322
+ building_source = auto_sources['building_source']
323
+ if land_cover_source is None:
324
+ land_cover_source = auto_sources['land_cover_source']
325
+ if canopy_height_source is None:
326
+ canopy_height_source = auto_sources['canopy_height_source']
327
+ if dem_source is None:
328
+ dem_source = auto_sources['dem_source']
329
+ if building_complementary_source is None:
330
+ building_complementary_source = auto_sources.get('building_complementary_source', 'None')
331
+
332
+ # Auto-set complement height if not provided
333
+ if 'building_complement_height' not in kwargs:
334
+ kwargs['building_complement_height'] = 10
335
+
336
+ # Ensure building_complementary_source is passed through kwargs
337
+ if building_complementary_source is not None:
338
+ kwargs['building_complementary_source'] = building_complementary_source
339
+
340
+ # Default DEM interpolation to True unless explicitly provided
341
+ if 'dem_interpolation' not in kwargs:
342
+ kwargs['dem_interpolation'] = True
343
+
344
+ # Ensure default complement height even if all sources are user-specified
345
+ if 'building_complement_height' not in kwargs:
346
+ kwargs['building_complement_height'] = 10
347
+
348
+ # Log selected data sources (always)
349
+ try:
350
+ _logger.info("Selected data sources:")
351
+ b_base_url = _url_for_source(building_source)
352
+ _logger.info("- Buildings(base)=%s%s", building_source, f" | {b_base_url}" if b_base_url else "")
353
+ b_comp_url = _url_for_source(building_complementary_source)
354
+ _logger.info("- Buildings(comp)=%s%s", building_complementary_source, f" | {b_comp_url}" if b_comp_url else "")
355
+ lc_url = _url_for_source(land_cover_source)
356
+ _logger.info("- LandCover=%s%s", land_cover_source, f" | {lc_url}" if lc_url else "")
357
+ canopy_url = _url_for_source(canopy_height_source)
358
+ _logger.info("- Canopy=%s%s", canopy_height_source, f" | {canopy_url}" if canopy_url else "")
359
+ dem_url = _url_for_source(dem_source)
360
+ _logger.info("- DEM=%s%s", dem_source, f" | {dem_url}" if dem_url else "")
361
+ _logger.info("- ComplementHeight=%s", kwargs.get('building_complement_height'))
362
+ except Exception:
363
+ pass
364
+
365
+ output_dir = kwargs.get("output_dir", "output")
366
+ # Group incoming kwargs into structured options for consistency
367
+ land_cover_keys = {
368
+ # examples: source-specific options (placeholders kept broad for back-compat)
369
+ "land_cover_path", "land_cover_resample", "land_cover_classes",
370
+ }
371
+ building_keys = {
372
+ "overlapping_footprint", "gdf_comp", "geotiff_path_comp",
373
+ "complement_building_footprints", "complement_height", "floor_height",
374
+ "building_complementary_source", "building_complement_height",
375
+ "building_complementary_path", "gba_clip", "gba_download_dir",
376
+ }
377
+ canopy_keys = {
378
+ "min_canopy_height", "trunk_height_ratio", "static_tree_height",
379
+ }
380
+ dem_keys = {
381
+ "flat_dem",
382
+ }
383
+ visualize_keys = {"gridvis", "mapvis"}
384
+ io_keys = {"save_voxcity_data", "save_voxctiy_data", "save_data_path", "save_path"}
385
+
386
+ land_cover_options = {k: v for k, v in kwargs.items() if k in land_cover_keys}
387
+ building_options = {k: v for k, v in kwargs.items() if k in building_keys}
388
+ canopy_options = {k: v for k, v in kwargs.items() if k in canopy_keys}
389
+ dem_options = {k: v for k, v in kwargs.items() if k in dem_keys}
390
+ # Auto-set flat DEM when dem_source is None/empty and user didn't specify
391
+ if (dem_source in (None, "", "None")) and ("flat_dem" not in dem_options):
392
+ dem_options["flat_dem"] = True
393
+ visualize_options = {k: v for k, v in kwargs.items() if k in visualize_keys}
394
+ io_options = {k: v for k, v in kwargs.items() if k in io_keys}
395
+
396
+ # Parallel download mode
397
+ parallel_download = kwargs.get("parallel_download", False)
398
+
399
+ cfg = PipelineConfig(
400
+ rectangle_vertices=rectangle_vertices,
401
+ meshsize=float(meshsize),
402
+ building_source=building_source,
403
+ land_cover_source=land_cover_source,
404
+ canopy_height_source=canopy_height_source,
405
+ dem_source=dem_source,
406
+ output_dir=output_dir,
407
+ trunk_height_ratio=kwargs.get("trunk_height_ratio"),
408
+ static_tree_height=kwargs.get("static_tree_height"),
409
+ remove_perimeter_object=kwargs.get("remove_perimeter_object"),
410
+ mapvis=bool(kwargs.get("mapvis", False)),
411
+ gridvis=bool(kwargs.get("gridvis", True)),
412
+ parallel_download=parallel_download,
413
+ land_cover_options=land_cover_options,
414
+ building_options=building_options,
415
+ canopy_options=canopy_options,
416
+ dem_options=dem_options,
417
+ io_options=io_options,
418
+ visualize_options=visualize_options,
419
+ )
420
+ 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'})
421
+
422
+ # Optional shape normalization (pad/crop) to a target (x, y, z)
423
+ target_voxel_shape = kwargs.get("target_voxel_shape", None)
424
+ if target_voxel_shape is not None:
425
+ try:
426
+ from ..utils.shape import normalize_voxcity_shape # late import to avoid cycles
427
+ align_xy = kwargs.get("pad_align_xy", "center")
428
+ allow_crop_xy = bool(kwargs.get("allow_crop_xy", True))
429
+ allow_crop_z = bool(kwargs.get("allow_crop_z", False))
430
+ pad_values = kwargs.get("pad_values", None)
431
+ city = normalize_voxcity_shape(
432
+ city,
433
+ tuple(target_voxel_shape),
434
+ align_xy=align_xy,
435
+ pad_values=pad_values,
436
+ allow_crop_xy=allow_crop_xy,
437
+ allow_crop_z=allow_crop_z,
438
+ )
439
+ try:
440
+ _logger.info("Applied target voxel shape %s -> final voxel shape %s", tuple(target_voxel_shape), tuple(city.voxels.classes.shape))
441
+ except Exception:
442
+ pass
443
+ except Exception as e:
444
+ try:
445
+ _logger.warning("Shape normalization skipped due to error: %s", str(e))
446
+ except Exception:
447
+ pass
448
+
449
+ # Backwards compatible save flag: prefer correct key, fallback to legacy misspelling
450
+ _save_flag = io_options.get("save_voxcity_data", kwargs.get("save_voxcity_data", kwargs.get("save_voxctiy_data", True)))
451
+ if _save_flag:
452
+ # Prefer explicit save_path if provided; fall back to legacy save_data_path; else default
453
+ save_path = (
454
+ io_options.get("save_path")
455
+ or kwargs.get("save_path")
456
+ or io_options.get("save_data_path")
457
+ or kwargs.get("save_data_path")
458
+ or f"{output_dir}/voxcity.pkl"
459
+ )
460
+ save_voxcity(save_path, city)
461
+
462
+ # Attach selected sources (final resolved) to extras for downstream consumers
463
+ try:
464
+ city.extras['selected_sources'] = {
465
+ 'building_source': building_source,
466
+ 'building_complementary_source': building_complementary_source or 'None',
467
+ 'land_cover_source': land_cover_source,
468
+ 'canopy_height_source': canopy_height_source,
469
+ 'dem_source': dem_source,
470
+ 'building_complement_height': kwargs.get('building_complement_height'),
471
+ }
472
+ except Exception:
473
+ pass
474
+
475
+ return city
476
+
477
+
478
+ def get_voxcity_CityGML(rectangle_vertices, land_cover_source, canopy_height_source, meshsize, url_citygml=None, citygml_path=None, **kwargs):
479
+ output_dir = kwargs.get("output_dir", "output")
480
+ os.makedirs(output_dir, exist_ok=True)
481
+ kwargs.pop('output_dir', None)
482
+
483
+ ssl_verify = kwargs.pop('ssl_verify', kwargs.pop('verify', True))
484
+ ca_bundle = kwargs.pop('ca_bundle', None)
485
+ timeout = kwargs.pop('timeout', 60)
486
+
487
+ building_gdf, terrain_gdf, vegetation_gdf = load_buid_dem_veg_from_citygml(
488
+ url=url_citygml,
489
+ citygml_path=citygml_path,
490
+ base_dir=output_dir,
491
+ rectangle_vertices=rectangle_vertices,
492
+ ssl_verify=ssl_verify,
493
+ ca_bundle=ca_bundle,
494
+ timeout=timeout
495
+ )
496
+
497
+ try:
498
+ import geopandas as gpd # noqa: F401
499
+ if building_gdf is not None:
500
+ if building_gdf.crs is None:
501
+ building_gdf = building_gdf.set_crs(epsg=4326)
502
+ elif getattr(building_gdf.crs, 'to_epsg', lambda: None)() != 4326 and building_gdf.crs != "EPSG:4326":
503
+ building_gdf = building_gdf.to_crs(epsg=4326)
504
+ if terrain_gdf is not None:
505
+ if terrain_gdf.crs is None:
506
+ terrain_gdf = terrain_gdf.set_crs(epsg=4326)
507
+ elif getattr(terrain_gdf.crs, 'to_epsg', lambda: None)() != 4326 and terrain_gdf.crs != "EPSG:4326":
508
+ terrain_gdf = terrain_gdf.to_crs(epsg=4326)
509
+ if vegetation_gdf is not None:
510
+ if vegetation_gdf.crs is None:
511
+ vegetation_gdf = vegetation_gdf.set_crs(epsg=4326)
512
+ elif getattr(vegetation_gdf.crs, 'to_epsg', lambda: None)() != 4326 and vegetation_gdf.crs != "EPSG:4326":
513
+ vegetation_gdf = vegetation_gdf.to_crs(epsg=4326)
514
+ except Exception:
515
+ pass
516
+
517
+ land_cover_grid = get_land_cover_grid(rectangle_vertices, meshsize, land_cover_source, output_dir, **kwargs)
518
+
519
+ print("Creating building height grid")
520
+ building_complementary_source = kwargs.get("building_complementary_source")
521
+ gdf_comp = None
522
+ geotiff_path_comp = None
523
+ complement_building_footprints = kwargs.get("complement_building_footprints")
524
+ if complement_building_footprints is None and (building_complementary_source not in (None, "None")):
525
+ complement_building_footprints = True
526
+
527
+ if (building_complementary_source is not None) and (building_complementary_source != "None"):
528
+ floor_height = kwargs.get("floor_height", 3.0)
529
+ if building_complementary_source == 'Microsoft Building Footprints':
530
+ gdf_comp = get_mbfp_gdf(kwargs.get("output_dir", "output"), rectangle_vertices)
531
+ elif building_complementary_source == 'OpenStreetMap':
532
+ gdf_comp = load_gdf_from_openstreetmap(rectangle_vertices, floor_height=floor_height)
533
+ elif building_complementary_source == 'EUBUCCO v0.1':
534
+ gdf_comp = load_gdf_from_eubucco(rectangle_vertices, kwargs.get("output_dir", "output"))
535
+ elif building_complementary_source == 'Overture':
536
+ gdf_comp = load_gdf_from_overture(rectangle_vertices, floor_height=floor_height)
537
+ elif building_complementary_source in ("GBA", "Global Building Atlas"):
538
+ clip_gba = kwargs.get("gba_clip", False)
539
+ gba_download_dir = kwargs.get("gba_download_dir")
540
+ gdf_comp = load_gdf_from_gba(rectangle_vertices, download_dir=gba_download_dir, clip_to_rectangle=clip_gba)
541
+ elif building_complementary_source == 'Local file':
542
+ comp_path = kwargs.get("building_complementary_path")
543
+ if comp_path is not None:
544
+ _, extension = os.path.splitext(comp_path)
545
+ if extension == ".gpkg":
546
+ gdf_comp = get_gdf_from_gpkg(comp_path, rectangle_vertices)
547
+ if gdf_comp is not None:
548
+ try:
549
+ if gdf_comp.crs is None:
550
+ gdf_comp = gdf_comp.set_crs(epsg=4326)
551
+ elif getattr(gdf_comp.crs, 'to_epsg', lambda: None)() != 4326 and gdf_comp.crs != "EPSG:4326":
552
+ gdf_comp = gdf_comp.to_crs(epsg=4326)
553
+ except Exception:
554
+ pass
555
+ elif building_complementary_source == "Open Building 2.5D Temporal":
556
+ roi = get_roi(rectangle_vertices)
557
+ os.makedirs(kwargs.get("output_dir", "output"), exist_ok=True)
558
+ geotiff_path_comp = os.path.join(kwargs.get("output_dir", "output"), "building_height.tif")
559
+ save_geotiff_open_buildings_temporal(roi, geotiff_path_comp)
560
+ elif building_complementary_source in ["England 1m DSM - DTM", "Netherlands 0.5m DSM - DTM"]:
561
+ roi = get_roi(rectangle_vertices)
562
+ os.makedirs(kwargs.get("output_dir", "output"), exist_ok=True)
563
+ geotiff_path_comp = os.path.join(kwargs.get("output_dir", "output"), "building_height.tif")
564
+ save_geotiff_dsm_minus_dtm(roi, geotiff_path_comp, meshsize, building_complementary_source)
565
+
566
+ _allowed_building_kwargs = {
567
+ "overlapping_footprint",
568
+ "gdf_comp",
569
+ "geotiff_path_comp",
570
+ "complement_building_footprints",
571
+ "complement_height",
572
+ }
573
+ _building_kwargs = {k: v for k, v in kwargs.items() if k in _allowed_building_kwargs}
574
+ if gdf_comp is not None:
575
+ _building_kwargs["gdf_comp"] = gdf_comp
576
+ if geotiff_path_comp is not None:
577
+ _building_kwargs["geotiff_path_comp"] = geotiff_path_comp
578
+ if complement_building_footprints is not None:
579
+ _building_kwargs["complement_building_footprints"] = complement_building_footprints
580
+
581
+ comp_height_user = kwargs.get("building_complement_height")
582
+ if comp_height_user is not None:
583
+ _building_kwargs["complement_height"] = comp_height_user
584
+ if _building_kwargs.get("complement_building_footprints") and ("complement_height" not in _building_kwargs):
585
+ _building_kwargs["complement_height"] = 10.0
586
+
587
+ building_height_grid, building_min_height_grid, building_id_grid, filtered_buildings = create_building_height_grid_from_gdf_polygon(
588
+ building_gdf, meshsize, rectangle_vertices, **_building_kwargs
589
+ )
590
+
591
+ grid_vis = kwargs.get("gridvis", True)
592
+ if grid_vis:
593
+ building_height_grid_nan = building_height_grid.copy()
594
+ building_height_grid_nan[building_height_grid_nan == 0] = np.nan
595
+ visualize_numerical_grid(np.flipud(building_height_grid_nan), meshsize, "building height (m)", cmap='viridis', label='Value')
596
+
597
+ if canopy_height_source == "Static":
598
+ canopy_height_grid_comp = np.zeros_like(land_cover_grid, dtype=float)
599
+ static_tree_height = kwargs.get("static_tree_height", 10.0)
600
+ _classes = get_land_cover_classes(land_cover_source)
601
+ _class_to_int = {name: i for i, name in enumerate(_classes.values())}
602
+ _tree_labels = ["Tree", "Trees", "Tree Canopy"]
603
+ _tree_indices = [_class_to_int[label] for label in _tree_labels if label in _class_to_int]
604
+ tree_mask = np.isin(land_cover_grid, _tree_indices) if _tree_indices else np.zeros_like(land_cover_grid, dtype=bool)
605
+ canopy_height_grid_comp[tree_mask] = static_tree_height
606
+ trunk_height_ratio = kwargs.get("trunk_height_ratio")
607
+ if trunk_height_ratio is None:
608
+ trunk_height_ratio = 11.76 / 19.98
609
+ canopy_bottom_height_grid_comp = canopy_height_grid_comp * float(trunk_height_ratio)
610
+ else:
611
+ from .grids import get_canopy_height_grid
612
+ canopy_height_grid_comp, canopy_bottom_height_grid_comp = get_canopy_height_grid(rectangle_vertices, meshsize, canopy_height_source, output_dir, **kwargs)
613
+
614
+ if vegetation_gdf is not None:
615
+ canopy_height_grid = create_vegetation_height_grid_from_gdf_polygon(vegetation_gdf, meshsize, rectangle_vertices)
616
+ trunk_height_ratio = kwargs.get("trunk_height_ratio")
617
+ if trunk_height_ratio is None:
618
+ trunk_height_ratio = 11.76 / 19.98
619
+ canopy_bottom_height_grid = canopy_height_grid * float(trunk_height_ratio)
620
+ else:
621
+ canopy_height_grid = np.zeros_like(building_height_grid)
622
+ canopy_bottom_height_grid = np.zeros_like(building_height_grid)
623
+
624
+ mask = (canopy_height_grid == 0) & (canopy_height_grid_comp != 0)
625
+ canopy_height_grid[mask] = canopy_height_grid_comp[mask]
626
+ mask_b = (canopy_bottom_height_grid == 0) & (canopy_bottom_height_grid_comp != 0)
627
+ canopy_bottom_height_grid[mask_b] = canopy_bottom_height_grid_comp[mask_b]
628
+ canopy_bottom_height_grid = np.minimum(canopy_bottom_height_grid, canopy_height_grid)
629
+
630
+ if kwargs.pop('flat_dem', None):
631
+ dem_grid = np.zeros_like(land_cover_grid)
632
+ else:
633
+ print("Creating Digital Elevation Model (DEM) grid")
634
+ dem_grid = create_dem_grid_from_gdf_polygon(terrain_gdf, meshsize, rectangle_vertices)
635
+ grid_vis = kwargs.get("gridvis", True)
636
+ if grid_vis:
637
+ visualize_numerical_grid(np.flipud(dem_grid), meshsize, title='Digital Elevation Model', cmap='terrain', label='Elevation (m)')
638
+
639
+ min_canopy_height = kwargs.get("min_canopy_height")
640
+ if min_canopy_height is not None:
641
+ canopy_height_grid[canopy_height_grid < kwargs["min_canopy_height"]] = 0
642
+ canopy_bottom_height_grid[canopy_height_grid == 0] = 0
643
+
644
+ remove_perimeter_object = kwargs.get("remove_perimeter_object")
645
+ if (remove_perimeter_object is not None) and (remove_perimeter_object > 0):
646
+ print("apply perimeter removal")
647
+ w_peri = int(remove_perimeter_object * building_height_grid.shape[0] + 0.5)
648
+ h_peri = int(remove_perimeter_object * building_height_grid.shape[1] + 0.5)
649
+
650
+ canopy_height_grid[:w_peri, :] = canopy_height_grid[-w_peri:, :] = canopy_height_grid[:, :h_peri] = canopy_height_grid[:, -h_peri:] = 0
651
+ 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
652
+
653
+ ids1 = np.unique(building_id_grid[:w_peri, :][building_id_grid[:w_peri, :] > 0])
654
+ ids2 = np.unique(building_id_grid[-w_peri:, :][building_id_grid[-w_peri:, :] > 0])
655
+ ids3 = np.unique(building_id_grid[:, :h_peri][building_id_grid[:, :h_peri] > 0])
656
+ ids4 = np.unique(building_id_grid[:, -h_peri:][building_id_grid[:, -h_peri:] > 0])
657
+ remove_ids = np.concatenate((ids1, ids2, ids3, ids4))
658
+
659
+ for remove_id in remove_ids:
660
+ positions = np.where(building_id_grid == remove_id)
661
+ building_height_grid[positions] = 0
662
+ building_min_height_grid[positions] = [[] for _ in range(len(building_min_height_grid[positions]))]
663
+
664
+ grid_vis = kwargs.get("gridvis", True)
665
+ if grid_vis:
666
+ building_height_grid_nan = building_height_grid.copy()
667
+ building_height_grid_nan[building_height_grid_nan == 0] = np.nan
668
+ visualize_numerical_grid(
669
+ np.flipud(building_height_grid_nan),
670
+ meshsize,
671
+ "building height (m)",
672
+ cmap='viridis',
673
+ label='Value'
674
+ )
675
+ canopy_height_grid_nan = canopy_height_grid.copy()
676
+ canopy_height_grid_nan[canopy_height_grid_nan == 0] = np.nan
677
+ visualize_numerical_grid(
678
+ np.flipud(canopy_height_grid_nan),
679
+ meshsize,
680
+ "Tree canopy height (m)",
681
+ cmap='Greens',
682
+ label='Tree canopy height (m)'
683
+ )
684
+
685
+ from .voxelizer import Voxelizer
686
+ voxelizer = Voxelizer(
687
+ voxel_size=meshsize,
688
+ land_cover_source=land_cover_source,
689
+ trunk_height_ratio=kwargs.get("trunk_height_ratio"),
690
+ )
691
+ voxcity_grid = voxelizer.generate_combined(
692
+ building_height_grid_ori=building_height_grid,
693
+ building_min_height_grid_ori=building_min_height_grid,
694
+ building_id_grid_ori=building_id_grid,
695
+ land_cover_grid_ori=land_cover_grid,
696
+ dem_grid_ori=dem_grid,
697
+ tree_grid_ori=canopy_height_grid,
698
+ canopy_bottom_height_grid_ori=locals().get("canopy_bottom_height_grid"),
699
+ )
700
+
701
+ from .pipeline import VoxCityPipeline as _Pipeline
702
+ pipeline = _Pipeline(meshsize=meshsize, rectangle_vertices=rectangle_vertices)
703
+ city = pipeline.assemble_voxcity(
704
+ voxcity_grid=voxcity_grid,
705
+ building_height_grid=building_height_grid,
706
+ building_min_height_grid=building_min_height_grid,
707
+ building_id_grid=building_id_grid,
708
+ land_cover_grid=land_cover_grid,
709
+ dem_grid=dem_grid,
710
+ canopy_height_top=canopy_height_grid,
711
+ canopy_height_bottom=locals().get("canopy_bottom_height_grid"),
712
+ extras={"building_gdf": building_gdf},
713
+ )
714
+
715
+ # Backwards compatible save flag: prefer correct key, fallback to legacy misspelling
716
+ _save_flag = kwargs.get("save_voxcity_data", kwargs.get("save_voxctiy_data", True))
717
+ if _save_flag:
718
+ save_path = (
719
+ kwargs.get("save_path")
720
+ or kwargs.get("save_data_path")
721
+ or f"{output_dir}/voxcity.pkl"
722
+ )
723
+ save_voxcity(save_path, city)
724
+
725
+ return city
726
+
727
+