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.
Files changed (42) hide show
  1. voxcity/__init__.py +14 -14
  2. voxcity/exporter/__init__.py +12 -12
  3. voxcity/exporter/cityles.py +633 -633
  4. voxcity/exporter/envimet.py +733 -728
  5. voxcity/exporter/magicavoxel.py +333 -333
  6. voxcity/exporter/netcdf.py +238 -238
  7. voxcity/exporter/obj.py +1480 -1480
  8. voxcity/generator/__init__.py +47 -44
  9. voxcity/generator/api.py +721 -675
  10. voxcity/generator/grids.py +381 -379
  11. voxcity/generator/io.py +94 -94
  12. voxcity/generator/pipeline.py +282 -282
  13. voxcity/generator/update.py +429 -0
  14. voxcity/generator/voxelizer.py +18 -6
  15. voxcity/geoprocessor/__init__.py +75 -75
  16. voxcity/geoprocessor/draw.py +1488 -1219
  17. voxcity/geoprocessor/merge_utils.py +91 -91
  18. voxcity/geoprocessor/mesh.py +806 -806
  19. voxcity/geoprocessor/network.py +708 -708
  20. voxcity/geoprocessor/raster/buildings.py +435 -428
  21. voxcity/geoprocessor/raster/export.py +93 -93
  22. voxcity/geoprocessor/raster/landcover.py +5 -2
  23. voxcity/geoprocessor/utils.py +824 -824
  24. voxcity/models.py +113 -113
  25. voxcity/simulator/solar/__init__.py +66 -43
  26. voxcity/simulator/solar/integration.py +336 -336
  27. voxcity/simulator/solar/sky.py +668 -0
  28. voxcity/simulator/solar/temporal.py +792 -434
  29. voxcity/utils/__init__.py +11 -0
  30. voxcity/utils/classes.py +194 -0
  31. voxcity/utils/lc.py +80 -39
  32. voxcity/utils/shape.py +230 -0
  33. voxcity/visualizer/__init__.py +24 -24
  34. voxcity/visualizer/builder.py +43 -43
  35. voxcity/visualizer/grids.py +141 -141
  36. voxcity/visualizer/maps.py +187 -187
  37. voxcity/visualizer/renderer.py +1145 -928
  38. {voxcity-0.7.0.dist-info → voxcity-1.0.2.dist-info}/METADATA +90 -49
  39. {voxcity-0.7.0.dist-info → voxcity-1.0.2.dist-info}/RECORD +42 -38
  40. {voxcity-0.7.0.dist-info → voxcity-1.0.2.dist-info}/WHEEL +0 -0
  41. {voxcity-0.7.0.dist-info → voxcity-1.0.2.dist-info}/licenses/AUTHORS.rst +0 -0
  42. {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
- 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
+ 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
+