voxcity 0.6.26__py3-none-any.whl → 1.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- voxcity/__init__.py +10 -4
- voxcity/downloader/__init__.py +2 -1
- voxcity/downloader/gba.py +210 -0
- voxcity/downloader/gee.py +5 -1
- voxcity/downloader/mbfp.py +1 -1
- voxcity/downloader/oemj.py +80 -8
- voxcity/downloader/utils.py +73 -73
- voxcity/errors.py +30 -0
- voxcity/exporter/__init__.py +9 -1
- voxcity/exporter/cityles.py +129 -34
- voxcity/exporter/envimet.py +51 -26
- voxcity/exporter/magicavoxel.py +42 -5
- voxcity/exporter/netcdf.py +27 -0
- voxcity/exporter/obj.py +103 -28
- voxcity/generator/__init__.py +47 -0
- voxcity/generator/api.py +721 -0
- voxcity/generator/grids.py +381 -0
- voxcity/generator/io.py +94 -0
- voxcity/generator/pipeline.py +282 -0
- voxcity/generator/update.py +429 -0
- voxcity/generator/voxelizer.py +392 -0
- voxcity/geoprocessor/__init__.py +75 -6
- voxcity/geoprocessor/conversion.py +153 -0
- voxcity/geoprocessor/draw.py +1488 -1169
- voxcity/geoprocessor/heights.py +199 -0
- voxcity/geoprocessor/io.py +101 -0
- voxcity/geoprocessor/merge_utils.py +91 -0
- voxcity/geoprocessor/mesh.py +26 -10
- voxcity/geoprocessor/network.py +35 -6
- voxcity/geoprocessor/overlap.py +84 -0
- voxcity/geoprocessor/raster/__init__.py +82 -0
- voxcity/geoprocessor/raster/buildings.py +435 -0
- voxcity/geoprocessor/raster/canopy.py +258 -0
- voxcity/geoprocessor/raster/core.py +150 -0
- voxcity/geoprocessor/raster/export.py +93 -0
- voxcity/geoprocessor/raster/landcover.py +159 -0
- voxcity/geoprocessor/raster/raster.py +110 -0
- voxcity/geoprocessor/selection.py +85 -0
- voxcity/geoprocessor/utils.py +824 -820
- voxcity/models.py +113 -0
- voxcity/simulator/common/__init__.py +22 -0
- voxcity/simulator/common/geometry.py +98 -0
- voxcity/simulator/common/raytracing.py +450 -0
- voxcity/simulator/solar/__init__.py +66 -0
- voxcity/simulator/solar/integration.py +336 -0
- voxcity/simulator/solar/kernels.py +62 -0
- voxcity/simulator/solar/radiation.py +648 -0
- voxcity/simulator/solar/sky.py +668 -0
- voxcity/simulator/solar/temporal.py +792 -0
- voxcity/simulator/view.py +36 -2286
- voxcity/simulator/visibility/__init__.py +29 -0
- voxcity/simulator/visibility/landmark.py +392 -0
- voxcity/simulator/visibility/view.py +508 -0
- voxcity/utils/__init__.py +11 -0
- voxcity/utils/classes.py +194 -0
- voxcity/utils/lc.py +80 -39
- voxcity/utils/logging.py +61 -0
- voxcity/utils/orientation.py +51 -0
- voxcity/utils/shape.py +230 -0
- voxcity/utils/weather/__init__.py +26 -0
- voxcity/utils/weather/epw.py +146 -0
- voxcity/utils/weather/files.py +36 -0
- voxcity/utils/weather/onebuilding.py +486 -0
- voxcity/visualizer/__init__.py +24 -0
- voxcity/visualizer/builder.py +43 -0
- voxcity/visualizer/grids.py +141 -0
- voxcity/visualizer/maps.py +187 -0
- voxcity/visualizer/palette.py +228 -0
- voxcity/visualizer/renderer.py +1145 -0
- {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/METADATA +162 -48
- voxcity-1.0.2.dist-info/RECORD +81 -0
- voxcity/generator.py +0 -1302
- voxcity/geoprocessor/grid.py +0 -1739
- voxcity/geoprocessor/polygon.py +0 -1344
- voxcity/simulator/solar.py +0 -2339
- voxcity/utils/visualization.py +0 -2849
- voxcity/utils/weather.py +0 -1038
- voxcity-0.6.26.dist-info/RECORD +0 -38
- {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/WHEEL +0 -0
- {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/licenses/AUTHORS.rst +0 -0
- {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/licenses/LICENSE +0 -0
voxcity/exporter/cityles.py
CHANGED
|
@@ -14,25 +14,26 @@ Notes:
|
|
|
14
14
|
import os
|
|
15
15
|
import numpy as np
|
|
16
16
|
from pathlib import Path
|
|
17
|
+
from ..models import VoxCity
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
# VoxCity standard land cover classes after conversion
|
|
20
|
-
# Based on convert_land_cover function output
|
|
21
|
+
# Based on convert_land_cover function output (1-based indices)
|
|
21
22
|
VOXCITY_STANDARD_CLASSES = {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
23
|
+
1: 'Bareland',
|
|
24
|
+
2: 'Rangeland',
|
|
25
|
+
3: 'Shrub',
|
|
26
|
+
4: 'Agriculture land',
|
|
27
|
+
5: 'Tree',
|
|
28
|
+
6: 'Moss and lichen',
|
|
29
|
+
7: 'Wet land',
|
|
30
|
+
8: 'Mangrove',
|
|
31
|
+
9: 'Water',
|
|
32
|
+
10: 'Snow and ice',
|
|
33
|
+
11: 'Developed space',
|
|
34
|
+
12: 'Road',
|
|
35
|
+
13: 'Building',
|
|
36
|
+
14: 'No Data'
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
## Source-specific class name to CityLES land use mappings
|
|
@@ -189,6 +190,27 @@ def _build_index_to_cityles_map(land_cover_source):
|
|
|
189
190
|
return index_to_code, class_names
|
|
190
191
|
|
|
191
192
|
|
|
193
|
+
def _resolve_under_tree_code(under_tree_class_name, under_tree_cityles_code, land_cover_source):
|
|
194
|
+
"""Resolve the CityLES land-use code used under tree canopy.
|
|
195
|
+
|
|
196
|
+
Priority:
|
|
197
|
+
1) Explicit numeric code if provided
|
|
198
|
+
2) Class name using the source-specific mapping
|
|
199
|
+
3) Class name using the standard (OSM) mapping
|
|
200
|
+
4) Default to 9 (Bare Land)
|
|
201
|
+
"""
|
|
202
|
+
if under_tree_cityles_code is not None:
|
|
203
|
+
try:
|
|
204
|
+
return int(under_tree_cityles_code)
|
|
205
|
+
except Exception:
|
|
206
|
+
pass
|
|
207
|
+
name_to_code = _get_source_name_mapping(land_cover_source)
|
|
208
|
+
code = name_to_code.get(under_tree_class_name)
|
|
209
|
+
if code is None:
|
|
210
|
+
code = OSM_CLASS_TO_CITYLES.get(under_tree_class_name, 9)
|
|
211
|
+
return code
|
|
212
|
+
|
|
213
|
+
|
|
192
214
|
def export_topog(building_height_grid, building_id_grid, output_path,
|
|
193
215
|
building_material='default', cityles_landuse_grid=None):
|
|
194
216
|
"""
|
|
@@ -239,7 +261,9 @@ def export_topog(building_height_grid, building_id_grid, output_path,
|
|
|
239
261
|
f.write(f"{i_1based} {j_1based} {height:.1f} {material_code_cell} 0.0 0.0 102\n")
|
|
240
262
|
|
|
241
263
|
|
|
242
|
-
def export_landuse(land_cover_grid, output_path, land_cover_source=None
|
|
264
|
+
def export_landuse(land_cover_grid, output_path, land_cover_source=None,
|
|
265
|
+
canopy_height_grid=None, building_height_grid=None,
|
|
266
|
+
under_tree_class_name='Bareland', under_tree_cityles_code=None):
|
|
243
267
|
"""
|
|
244
268
|
Export landuse.txt file for CityLES
|
|
245
269
|
|
|
@@ -251,6 +275,17 @@ def export_landuse(land_cover_grid, output_path, land_cover_source=None):
|
|
|
251
275
|
Output directory path
|
|
252
276
|
land_cover_source : str, optional
|
|
253
277
|
Source of land cover data
|
|
278
|
+
canopy_height_grid : numpy.ndarray, optional
|
|
279
|
+
2D array of canopy heights; if provided, cells with canopy (>0) will be
|
|
280
|
+
assigned the ground class under the canopy instead of a tree class.
|
|
281
|
+
building_height_grid : numpy.ndarray, optional
|
|
282
|
+
2D array of building heights; if provided, canopy overrides will not be
|
|
283
|
+
applied where buildings exist (height > 0).
|
|
284
|
+
under_tree_class_name : str, optional
|
|
285
|
+
Name of ground land-cover class to use under tree canopy. Defaults to 'Bareland'.
|
|
286
|
+
under_tree_cityles_code : int, optional
|
|
287
|
+
Explicit CityLES land-use code to use under canopy; if provided it takes
|
|
288
|
+
precedence over under_tree_class_name.
|
|
254
289
|
"""
|
|
255
290
|
filename = output_path / 'landuse.txt'
|
|
256
291
|
|
|
@@ -261,7 +296,12 @@ def export_landuse(land_cover_grid, output_path, land_cover_source=None):
|
|
|
261
296
|
|
|
262
297
|
print(f"Land cover source: {land_cover_source} (raw indices)")
|
|
263
298
|
|
|
264
|
-
#
|
|
299
|
+
# Resolve the CityLES code to use under tree canopy
|
|
300
|
+
under_tree_code = _resolve_under_tree_code(
|
|
301
|
+
under_tree_class_name, under_tree_cityles_code, land_cover_source
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# Create mapping statistics: per raw index, count per resulting CityLES code
|
|
265
305
|
mapping_stats = {}
|
|
266
306
|
# Prepare grid to return
|
|
267
307
|
cityles_landuse_grid = np.zeros((ny, nx), dtype=int)
|
|
@@ -272,24 +312,33 @@ def export_landuse(land_cover_grid, output_path, land_cover_source=None):
|
|
|
272
312
|
for i in range(nx):
|
|
273
313
|
idx = int(land_cover_grid[j, i])
|
|
274
314
|
cityles_code = index_to_code.get(idx, 4)
|
|
315
|
+
|
|
316
|
+
# If a canopy grid is provided, override tree canopy cells to the
|
|
317
|
+
# specified ground class, optionally skipping where buildings exist.
|
|
318
|
+
if canopy_height_grid is not None:
|
|
319
|
+
has_canopy = float(canopy_height_grid[j, i]) > 0.0
|
|
320
|
+
has_building = False
|
|
321
|
+
if building_height_grid is not None:
|
|
322
|
+
has_building = float(building_height_grid[j, i]) > 0.0
|
|
323
|
+
if has_canopy and not has_building:
|
|
324
|
+
cityles_code = under_tree_code
|
|
275
325
|
f.write(f"{cityles_code}\n")
|
|
276
326
|
|
|
277
327
|
cityles_landuse_grid[j, i] = cityles_code
|
|
278
328
|
|
|
279
329
|
# Track mapping statistics
|
|
280
330
|
if idx not in mapping_stats:
|
|
281
|
-
mapping_stats[idx] = {
|
|
282
|
-
mapping_stats[idx][
|
|
331
|
+
mapping_stats[idx] = {}
|
|
332
|
+
mapping_stats[idx][cityles_code] = mapping_stats[idx].get(cityles_code, 0) + 1
|
|
283
333
|
|
|
284
334
|
# Print mapping summary
|
|
285
335
|
print("\nLand cover mapping summary (by source class):")
|
|
286
336
|
total = ny * nx
|
|
287
337
|
for idx in sorted(mapping_stats.keys()):
|
|
288
|
-
stats = mapping_stats[idx]
|
|
289
|
-
percentage = (stats['count'] / total) * 100
|
|
290
338
|
class_name = class_names[idx] if 0 <= idx < len(class_names) else 'Unknown'
|
|
291
|
-
|
|
292
|
-
|
|
339
|
+
for code, count in sorted(mapping_stats[idx].items()):
|
|
340
|
+
percentage = (count / total) * 100
|
|
341
|
+
print(f" {idx}: {class_name} -> CityLES {code}: {count} cells ({percentage:.1f}%)")
|
|
293
342
|
|
|
294
343
|
return cityles_landuse_grid
|
|
295
344
|
|
|
@@ -322,7 +371,7 @@ def export_dem(dem_grid, output_path):
|
|
|
322
371
|
f.write(f"{i_1based} {j_1based} {elevation:.1f}\n")
|
|
323
372
|
|
|
324
373
|
|
|
325
|
-
def export_vmap(canopy_height_grid, output_path,
|
|
374
|
+
def export_vmap(canopy_height_grid, output_path, trunk_height_ratio=0.3, tree_type='default', building_height_grid=None, canopy_bottom_height_grid=None):
|
|
326
375
|
"""
|
|
327
376
|
Export vmap.txt file for CityLES
|
|
328
377
|
|
|
@@ -332,7 +381,7 @@ def export_vmap(canopy_height_grid, output_path, tree_base_ratio=0.3, tree_type=
|
|
|
332
381
|
2D array of canopy heights
|
|
333
382
|
output_path : Path
|
|
334
383
|
Output directory path
|
|
335
|
-
|
|
384
|
+
trunk_height_ratio : float
|
|
336
385
|
Ratio of tree base height to total canopy height
|
|
337
386
|
tree_type : str
|
|
338
387
|
Tree type for mapping
|
|
@@ -366,7 +415,7 @@ def export_vmap(canopy_height_grid, output_path, tree_base_ratio=0.3, tree_type=
|
|
|
366
415
|
if canopy_bottom_height_grid is not None:
|
|
367
416
|
lower_height = float(np.clip(canopy_bottom_height_grid[j, i], 0.0, total_height))
|
|
368
417
|
else:
|
|
369
|
-
lower_height = total_height *
|
|
418
|
+
lower_height = total_height * trunk_height_ratio
|
|
370
419
|
upper_height = total_height
|
|
371
420
|
# Format: i j lower_height upper_height tree_type
|
|
372
421
|
f.write(f"{i_1based} {j_1based} {lower_height:.1f} {upper_height:.1f} {tree_code}\n")
|
|
@@ -412,11 +461,16 @@ def export_lonlat(rectangle_vertices, grid_shape, output_path):
|
|
|
412
461
|
f.write(f"{i_1based} {j_1based} {lon:.7f} {lat:.8f}\n")
|
|
413
462
|
|
|
414
463
|
|
|
415
|
-
def export_cityles(
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
464
|
+
def export_cityles(city: VoxCity,
|
|
465
|
+
output_directory: str = "output/cityles",
|
|
466
|
+
building_material: str = 'default',
|
|
467
|
+
tree_type: str = 'default',
|
|
468
|
+
trunk_height_ratio: float = 0.3,
|
|
469
|
+
canopy_bottom_height_grid=None,
|
|
470
|
+
under_tree_class_name: str = 'Bareland',
|
|
471
|
+
under_tree_cityles_code=None,
|
|
472
|
+
land_cover_source: str | None = None,
|
|
473
|
+
**kwargs):
|
|
420
474
|
"""
|
|
421
475
|
Export VoxCity data to CityLES format
|
|
422
476
|
|
|
@@ -444,7 +498,7 @@ def export_cityles(building_height_grid, building_id_grid, canopy_height_grid,
|
|
|
444
498
|
Building material type for mapping
|
|
445
499
|
tree_type : str
|
|
446
500
|
Tree type for mapping
|
|
447
|
-
|
|
501
|
+
trunk_height_ratio : float
|
|
448
502
|
Ratio of tree base height to total canopy height
|
|
449
503
|
**kwargs : dict
|
|
450
504
|
Additional parameters (for compatibility)
|
|
@@ -457,11 +511,29 @@ def export_cityles(building_height_grid, building_id_grid, canopy_height_grid,
|
|
|
457
511
|
output_path = create_cityles_directories(output_directory)
|
|
458
512
|
|
|
459
513
|
print(f"Exporting CityLES files to: {output_path}")
|
|
514
|
+
# Resolve data from VoxCity
|
|
515
|
+
building_height_grid = city.buildings.heights
|
|
516
|
+
building_id_grid = city.buildings.ids if city.buildings.ids is not None else np.zeros_like(building_height_grid, dtype=int)
|
|
517
|
+
canopy_height_grid = city.tree_canopy.top if city.tree_canopy is not None else np.zeros_like(city.land_cover.classes, dtype=float)
|
|
518
|
+
land_cover_grid = city.land_cover.classes
|
|
519
|
+
dem_grid = city.dem.elevation
|
|
520
|
+
meshsize = float(city.voxels.meta.meshsize)
|
|
521
|
+
rectangle_vertices = city.extras.get("rectangle_vertices") or [(0.0, 0.0)] * 4
|
|
522
|
+
land_cover_source = land_cover_source or city.extras.get("land_cover_source", "Standard")
|
|
523
|
+
|
|
460
524
|
print(f"Land cover source: {land_cover_source}")
|
|
461
525
|
|
|
462
526
|
# Export individual files
|
|
463
527
|
print("\nExporting landuse.txt...")
|
|
464
|
-
cityles_landuse_grid = export_landuse(
|
|
528
|
+
cityles_landuse_grid = export_landuse(
|
|
529
|
+
land_cover_grid,
|
|
530
|
+
output_path,
|
|
531
|
+
land_cover_source,
|
|
532
|
+
canopy_height_grid=canopy_height_grid,
|
|
533
|
+
building_height_grid=building_height_grid,
|
|
534
|
+
under_tree_class_name=under_tree_class_name,
|
|
535
|
+
under_tree_cityles_code=under_tree_cityles_code,
|
|
536
|
+
)
|
|
465
537
|
|
|
466
538
|
print("\nExporting topog.txt...")
|
|
467
539
|
export_topog(
|
|
@@ -476,7 +548,7 @@ def export_cityles(building_height_grid, building_id_grid, canopy_height_grid,
|
|
|
476
548
|
export_dem(dem_grid, output_path)
|
|
477
549
|
|
|
478
550
|
print("\nExporting vmap.txt...")
|
|
479
|
-
export_vmap(canopy_height_grid, output_path,
|
|
551
|
+
export_vmap(canopy_height_grid, output_path, trunk_height_ratio, tree_type, building_height_grid=building_height_grid, canopy_bottom_height_grid=canopy_bottom_height_grid)
|
|
480
552
|
|
|
481
553
|
print("\nExporting lonlat.txt...")
|
|
482
554
|
export_lonlat(rectangle_vertices, building_height_grid.shape, output_path)
|
|
@@ -497,6 +569,11 @@ def export_cityles(building_height_grid, building_id_grid, canopy_height_grid,
|
|
|
497
569
|
# Trees count after removing overlaps with buildings
|
|
498
570
|
trees_count = int(np.sum(np.where(building_height_grid > 0, 0.0, canopy_height_grid) > 0))
|
|
499
571
|
f.write(f"Trees: {trees_count}\n")
|
|
572
|
+
# Under-tree land-use selection
|
|
573
|
+
under_tree_code = _resolve_under_tree_code(
|
|
574
|
+
under_tree_class_name, under_tree_cityles_code, land_cover_source
|
|
575
|
+
)
|
|
576
|
+
f.write(f"Under-tree land use: {under_tree_class_name} (CityLES {under_tree_code})\n")
|
|
500
577
|
|
|
501
578
|
# Add land use value ranges
|
|
502
579
|
f.write(f"\nLand cover value range: {land_cover_grid.min()} - {land_cover_grid.max()}\n")
|
|
@@ -507,6 +584,24 @@ def export_cityles(building_height_grid, building_id_grid, canopy_height_grid,
|
|
|
507
584
|
return str(output_path)
|
|
508
585
|
|
|
509
586
|
|
|
587
|
+
class CityLesExporter:
|
|
588
|
+
"""Exporter adapter to write a VoxCity model to CityLES text files."""
|
|
589
|
+
|
|
590
|
+
def export(self, obj, output_directory: str, base_filename: str, **kwargs):
|
|
591
|
+
if not isinstance(obj, VoxCity):
|
|
592
|
+
raise TypeError("CityLesExporter expects a VoxCity instance")
|
|
593
|
+
city: VoxCity = obj
|
|
594
|
+
# CityLES writes multiple files; use output_directory/base_filename as folder/name
|
|
595
|
+
out_dir = os.path.join(output_directory, base_filename)
|
|
596
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
597
|
+
export_cityles(
|
|
598
|
+
city,
|
|
599
|
+
output_directory=out_dir,
|
|
600
|
+
**kwargs,
|
|
601
|
+
)
|
|
602
|
+
return out_dir
|
|
603
|
+
|
|
604
|
+
|
|
510
605
|
# Helper function to apply VoxCity's convert_land_cover if needed
|
|
511
606
|
def ensure_converted_land_cover(land_cover_grid, land_cover_source):
|
|
512
607
|
"""
|
voxcity/exporter/envimet.py
CHANGED
|
@@ -27,9 +27,10 @@ import os
|
|
|
27
27
|
import numpy as np
|
|
28
28
|
import datetime
|
|
29
29
|
|
|
30
|
-
from ..geoprocessor.
|
|
30
|
+
from ..geoprocessor.raster import apply_operation, translate_array, group_and_label_cells, process_grid
|
|
31
31
|
from ..geoprocessor.utils import get_city_country_name_from_rectangle, get_timezone_info
|
|
32
32
|
from ..utils.lc import convert_land_cover
|
|
33
|
+
from ..models import VoxCity
|
|
33
34
|
|
|
34
35
|
def array_to_string(arr):
|
|
35
36
|
"""Convert a 2D numpy array to a string representation with comma-separated values.
|
|
@@ -136,21 +137,26 @@ def prepare_grids(building_height_grid_ori, building_id_grid_ori, canopy_height_
|
|
|
136
137
|
building_height_grid = apply_operation(building_height_grid, meshsize)
|
|
137
138
|
|
|
138
139
|
# Convert land cover if needed based on source
|
|
139
|
-
if
|
|
140
|
-
|
|
140
|
+
if land_cover_source == 'OpenStreetMap':
|
|
141
|
+
# OpenStreetMap uses Standard classification, just shift to 1-based
|
|
142
|
+
land_cover_grid_converted = land_cover_grid_ori + 1
|
|
141
143
|
else:
|
|
144
|
+
# All other sources need remapping to standard indices
|
|
142
145
|
land_cover_grid_converted = convert_land_cover(land_cover_grid_ori, land_cover_source=land_cover_source)
|
|
143
146
|
|
|
144
|
-
land_cover_grid = np.flipud(land_cover_grid_converted).copy()
|
|
147
|
+
land_cover_grid = np.flipud(land_cover_grid_converted).copy()
|
|
145
148
|
|
|
146
149
|
# Dictionary mapping land cover types to vegetation codes
|
|
150
|
+
# Standard 1-based indices: 1=Bareland, 2=Rangeland, 3=Shrub, 4=Agriculture, 5=Tree,
|
|
151
|
+
# 6=Moss/lichen, 7=Wetland, 8=Mangrove, 9=Water, 10=Snow,
|
|
152
|
+
# 11=Developed, 12=Road, 13=Building, 14=NoData
|
|
147
153
|
veg_translation_dict = {
|
|
148
154
|
1: '', # Bareland
|
|
149
155
|
2: '0200XX', # Rangeland
|
|
150
156
|
3: '0200H1', # Shrub
|
|
151
|
-
4: '0200XX', #
|
|
152
|
-
5: '
|
|
153
|
-
6: '', #
|
|
157
|
+
4: '0200XX', # Agriculture land
|
|
158
|
+
5: '', # Tree (handled separately as 3D vegetation)
|
|
159
|
+
6: '0200XX', # Moss and lichen
|
|
154
160
|
7: '0200XX', # Wet land
|
|
155
161
|
8: '' # Mangroves
|
|
156
162
|
}
|
|
@@ -161,9 +167,9 @@ def prepare_grids(building_height_grid_ori, building_id_grid_ori, canopy_height_
|
|
|
161
167
|
1: '000000', # Bareland
|
|
162
168
|
2: '000000', # Rangeland
|
|
163
169
|
3: '000000', # Shrub
|
|
164
|
-
4: '000000', #
|
|
165
|
-
5: '000000', #
|
|
166
|
-
6: '000000', #
|
|
170
|
+
4: '000000', # Agriculture land
|
|
171
|
+
5: '000000', # Tree
|
|
172
|
+
6: '000000', # Moss and lichen
|
|
167
173
|
7: '0200WW', # Wet land
|
|
168
174
|
8: '0200WW', # Mangroves
|
|
169
175
|
9: '0200WW', # Water
|
|
@@ -478,25 +484,18 @@ def save_file(content, output_file_path):
|
|
|
478
484
|
with open(output_file_path, 'w', encoding='utf-8') as file:
|
|
479
485
|
file.write(content)
|
|
480
486
|
|
|
481
|
-
def export_inx(
|
|
487
|
+
def export_inx(city: VoxCity, output_directory: str, file_basename: str = 'voxcity', land_cover_source: str | None = None, **kwargs):
|
|
482
488
|
"""Export model data to ENVI-met INX file format.
|
|
483
489
|
|
|
484
490
|
This is the main function for exporting voxel city data to ENVI-met format.
|
|
485
491
|
It coordinates the entire export process from grid preparation to file saving.
|
|
486
492
|
|
|
487
493
|
Args:
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
meshsize (float): Size of mesh cells in meters
|
|
494
|
-
land_cover_source (str): Source of land cover data
|
|
495
|
-
rectangle_vertices (list): Vertices defining model area
|
|
496
|
-
**kwargs: Additional keyword arguments:
|
|
497
|
-
- output_directory (str): Directory to save output
|
|
498
|
-
- file_basename (str): Base filename for output
|
|
499
|
-
- Other args passed to create_xml_content()
|
|
494
|
+
city (VoxCity): VoxCity instance to export
|
|
495
|
+
output_directory (str): Directory to save output
|
|
496
|
+
file_basename (str): Base filename (without extension)
|
|
497
|
+
land_cover_source (str | None): Optional override for land cover source; defaults to city.extras
|
|
498
|
+
**kwargs: Additional keyword arguments passed to create_xml_content()
|
|
500
499
|
|
|
501
500
|
Notes:
|
|
502
501
|
- Creates output directory if it doesn't exist
|
|
@@ -504,20 +503,46 @@ def export_inx(building_height_grid_ori, building_id_grid_ori, canopy_height_gri
|
|
|
504
503
|
- Generates complete INX file with all required data
|
|
505
504
|
- Uses standardized file naming convention
|
|
506
505
|
"""
|
|
506
|
+
# Resolve inputs from VoxCity
|
|
507
|
+
meshsize = float(city.voxels.meta.meshsize)
|
|
508
|
+
rectangle_vertices = city.extras.get("rectangle_vertices") or [(0.0, 0.0)] * 4
|
|
509
|
+
lc_source = land_cover_source or city.extras.get("land_cover_source", "Standard")
|
|
510
|
+
|
|
507
511
|
# Prepare grids
|
|
508
512
|
building_height_grid_inx, building_id_grid, land_cover_veg_grid_inx, land_cover_mat_grid_inx, canopy_height_grid_inx, dem_grid_inx = prepare_grids(
|
|
509
|
-
|
|
513
|
+
city.buildings.heights.copy(),
|
|
514
|
+
(city.buildings.ids if city.buildings.ids is not None else np.zeros_like(city.buildings.heights, dtype=int)).copy(),
|
|
515
|
+
(city.tree_canopy.top if city.tree_canopy is not None else np.zeros_like(city.land_cover.classes, dtype=float)).copy(),
|
|
516
|
+
city.land_cover.classes.copy(),
|
|
517
|
+
city.dem.elevation.copy(),
|
|
518
|
+
meshsize,
|
|
519
|
+
lc_source)
|
|
510
520
|
|
|
511
521
|
# Create XML content
|
|
512
522
|
xml_content = create_xml_content(building_height_grid_inx, building_id_grid, land_cover_veg_grid_inx, land_cover_mat_grid_inx, canopy_height_grid_inx, dem_grid_inx, meshsize, rectangle_vertices, **kwargs)
|
|
513
523
|
|
|
514
524
|
# Save the output
|
|
515
|
-
output_dir =
|
|
525
|
+
output_dir = output_directory or 'output'
|
|
516
526
|
os.makedirs(output_dir, exist_ok=True)
|
|
517
|
-
file_basename = kwargs.get("file_basename", 'voxcity')
|
|
518
527
|
output_file_path = os.path.join(output_dir, f"{file_basename}.INX")
|
|
519
528
|
save_file(xml_content, output_file_path)
|
|
520
529
|
|
|
530
|
+
|
|
531
|
+
class EnvimetExporter:
|
|
532
|
+
"""Exporter adapter to write a VoxCity model to ENVI-met INX format."""
|
|
533
|
+
|
|
534
|
+
def export(self, obj, output_directory: str, base_filename: str, **kwargs):
|
|
535
|
+
if not isinstance(obj, VoxCity):
|
|
536
|
+
raise TypeError("EnvimetExporter expects a VoxCity instance")
|
|
537
|
+
city: VoxCity = obj
|
|
538
|
+
export_inx(
|
|
539
|
+
city,
|
|
540
|
+
output_directory=output_directory,
|
|
541
|
+
file_basename=base_filename,
|
|
542
|
+
**kwargs,
|
|
543
|
+
)
|
|
544
|
+
return os.path.join(output_directory, f"{base_filename}.INX")
|
|
545
|
+
|
|
521
546
|
def generate_edb_file(**kwargs):
|
|
522
547
|
"""Generate ENVI-met database file for 3D plants.
|
|
523
548
|
|
voxcity/exporter/magicavoxel.py
CHANGED
|
@@ -23,7 +23,7 @@ import numpy as np
|
|
|
23
23
|
from pyvox.models import Vox
|
|
24
24
|
from pyvox.writer import VoxWriter
|
|
25
25
|
import os
|
|
26
|
-
from ..
|
|
26
|
+
from ..visualizer import get_voxel_color_map
|
|
27
27
|
|
|
28
28
|
def convert_colormap_and_array(original_map, original_array):
|
|
29
29
|
"""
|
|
@@ -270,14 +270,14 @@ def export_magicavoxel_vox(array, output_dir, base_filename='chunk', voxel_color
|
|
|
270
270
|
4. Progress reporting
|
|
271
271
|
|
|
272
272
|
Args:
|
|
273
|
-
array (numpy.ndarray): 3D array containing voxel data.
|
|
274
|
-
|
|
273
|
+
array (numpy.ndarray | VoxCity): 3D array containing voxel data or a VoxCity instance.
|
|
274
|
+
When a VoxCity is provided, its voxel classes are exported.
|
|
275
275
|
output_dir (str): Directory to save the .vox files.
|
|
276
276
|
Will be created if it doesn't exist.
|
|
277
277
|
base_filename (str, optional): Base name for the output files.
|
|
278
278
|
Defaults to 'chunk'. Used when model is split into multiple files.
|
|
279
279
|
voxel_color_map (dict, optional): Dictionary mapping indices to RGB color values.
|
|
280
|
-
If None, uses default color map from
|
|
280
|
+
If None, uses default color map from visualizer.
|
|
281
281
|
Each value should be a list of [R,G,B] values (0-255).
|
|
282
282
|
|
|
283
283
|
Note:
|
|
@@ -285,6 +285,14 @@ def export_magicavoxel_vox(array, output_dir, base_filename='chunk', voxel_color
|
|
|
285
285
|
- Color mapping is optimized and made sequential
|
|
286
286
|
- Progress information is printed to stdout
|
|
287
287
|
"""
|
|
288
|
+
# Accept VoxCity instance as first argument
|
|
289
|
+
try:
|
|
290
|
+
from ..models import VoxCity as _VoxCity
|
|
291
|
+
if isinstance(array, _VoxCity):
|
|
292
|
+
array = array.voxels.classes
|
|
293
|
+
except Exception:
|
|
294
|
+
pass
|
|
295
|
+
|
|
288
296
|
# Use default color map if none provided
|
|
289
297
|
if voxel_color_map is None:
|
|
290
298
|
voxel_color_map = get_voxel_color_map()
|
|
@@ -294,4 +302,33 @@ def export_magicavoxel_vox(array, output_dir, base_filename='chunk', voxel_color
|
|
|
294
302
|
|
|
295
303
|
# Export the model and print confirmation
|
|
296
304
|
value_mapping, palette = export_large_voxel_model(converted_array, converted_voxel_color_map, output_dir, base_filename=base_filename)
|
|
297
|
-
print(f"\tvox files was successfully exported in {output_dir}")
|
|
305
|
+
print(f"\tvox files was successfully exported in {output_dir}")
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class MagicaVoxelExporter:
|
|
309
|
+
"""Exporter adapter to write VoxCity voxels as MagicaVoxel .vox chunks.
|
|
310
|
+
|
|
311
|
+
Accepts either a VoxCity instance (uses `voxels.classes`) or a raw 3D numpy array.
|
|
312
|
+
"""
|
|
313
|
+
|
|
314
|
+
def export(self, obj, output_directory: str, base_filename: str, **kwargs):
|
|
315
|
+
import numpy as _np
|
|
316
|
+
os.makedirs(output_directory, exist_ok=True)
|
|
317
|
+
try:
|
|
318
|
+
from ..models import VoxCity as _VoxCity
|
|
319
|
+
if isinstance(obj, _VoxCity):
|
|
320
|
+
export_magicavoxel_vox(
|
|
321
|
+
obj.voxels.classes,
|
|
322
|
+
output_directory,
|
|
323
|
+
base_filename,
|
|
324
|
+
voxel_color_map=kwargs.get("voxel_color_map"),
|
|
325
|
+
)
|
|
326
|
+
return output_directory
|
|
327
|
+
except Exception:
|
|
328
|
+
pass
|
|
329
|
+
|
|
330
|
+
if isinstance(obj, _np.ndarray) and obj.ndim == 3:
|
|
331
|
+
export_magicavoxel_vox(obj, output_directory, base_filename, voxel_color_map=kwargs.get("voxel_color_map"))
|
|
332
|
+
return output_directory
|
|
333
|
+
|
|
334
|
+
raise TypeError("MagicaVoxelExporter.export expects VoxCity or a 3D numpy array")
|
voxcity/exporter/netcdf.py
CHANGED
|
@@ -38,6 +38,7 @@ except Exception: # pragma: no cover - optional dependency
|
|
|
38
38
|
__all__ = [
|
|
39
39
|
"voxel_to_xarray_dataset",
|
|
40
40
|
"save_voxel_netcdf",
|
|
41
|
+
"NetCDFExporter",
|
|
41
42
|
]
|
|
42
43
|
|
|
43
44
|
|
|
@@ -209,3 +210,29 @@ def save_voxel_netcdf(
|
|
|
209
210
|
return str(path)
|
|
210
211
|
|
|
211
212
|
|
|
213
|
+
class NetCDFExporter:
|
|
214
|
+
"""Exporter adapter to write a VoxCity voxel grid to NetCDF."""
|
|
215
|
+
|
|
216
|
+
def export(self, obj, output_directory: str, base_filename: str, **kwargs):
|
|
217
|
+
from ..models import VoxCity
|
|
218
|
+
path = Path(output_directory) / f"{base_filename}.nc"
|
|
219
|
+
if not isinstance(obj, VoxCity):
|
|
220
|
+
raise TypeError("NetCDFExporter expects a VoxCity instance")
|
|
221
|
+
rect = obj.extras.get("rectangle_vertices")
|
|
222
|
+
# Merge default attrs with user-provided extras
|
|
223
|
+
user_extra = kwargs.get("extra_attrs") or {}
|
|
224
|
+
attrs = {
|
|
225
|
+
"land_cover_source": obj.extras.get("land_cover_source", ""),
|
|
226
|
+
"building_source": obj.extras.get("building_source", ""),
|
|
227
|
+
"dem_source": obj.extras.get("dem_source", ""),
|
|
228
|
+
}
|
|
229
|
+
attrs.update(user_extra)
|
|
230
|
+
return save_voxel_netcdf(
|
|
231
|
+
voxcity_grid=obj.voxels.classes,
|
|
232
|
+
output_path=path,
|
|
233
|
+
voxel_size_m=obj.voxels.meta.meshsize,
|
|
234
|
+
rectangle_vertices=rect,
|
|
235
|
+
extra_attrs=attrs,
|
|
236
|
+
engine=kwargs.get("engine"),
|
|
237
|
+
)
|
|
238
|
+
|