voxcity 0.6.23__tar.gz → 0.6.25__tar.gz
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.
Potentially problematic release.
This version of voxcity might be problematic. Click here for more details.
- {voxcity-0.6.23 → voxcity-0.6.25}/PKG-INFO +1 -1
- {voxcity-0.6.23 → voxcity-0.6.25}/pyproject.toml +1 -1
- {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/exporter/cityles.py +95 -76
- {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/utils/visualization.py +317 -231
- {voxcity-0.6.23 → voxcity-0.6.25}/AUTHORS.rst +0 -0
- {voxcity-0.6.23 → voxcity-0.6.25}/LICENSE +0 -0
- {voxcity-0.6.23 → voxcity-0.6.25}/README.md +0 -0
- {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/__init__.py +0 -0
- {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/downloader/__init__.py +0 -0
- {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/downloader/citygml.py +0 -0
- {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/downloader/eubucco.py +0 -0
- {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/downloader/gee.py +0 -0
- {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/downloader/mbfp.py +0 -0
- {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/downloader/oemj.py +0 -0
- {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/downloader/osm.py +0 -0
- {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/downloader/overture.py +0 -0
- {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/downloader/utils.py +0 -0
- {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/exporter/__init__.py +0 -0
- {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/exporter/envimet.py +0 -0
- {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/exporter/magicavoxel.py +0 -0
- {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/exporter/netcdf.py +0 -0
- {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/exporter/obj.py +0 -0
- {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/generator.py +0 -0
- {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/geoprocessor/__init__.py +0 -0
- {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/geoprocessor/draw.py +0 -0
- {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/geoprocessor/grid.py +0 -0
- {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/geoprocessor/mesh.py +0 -0
- {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/geoprocessor/network.py +0 -0
- {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/geoprocessor/polygon.py +0 -0
- {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/geoprocessor/utils.py +0 -0
- {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/simulator/__init__.py +0 -0
- {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/simulator/solar.py +0 -0
- {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/simulator/utils.py +0 -0
- {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/simulator/view.py +0 -0
- {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/utils/__init__.py +0 -0
- {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/utils/lc.py +0 -0
- {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/utils/material.py +0 -0
- {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/utils/weather.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "voxcity"
|
|
3
|
-
version = "0.6.
|
|
3
|
+
version = "0.6.25"
|
|
4
4
|
description = "voxcity is an easy and one-stop tool to output 3d city models for microclimate simulation by integrating multiple geospatial open-data"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -36,107 +36,126 @@ VOXCITY_STANDARD_CLASSES = {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
## Source-specific class name to CityLES land use mappings
|
|
39
|
-
# CityLES land use codes
|
|
40
|
-
#
|
|
41
|
-
|
|
42
|
-
#
|
|
39
|
+
# CityLES land use codes (updated to match provided definitions):
|
|
40
|
+
# 1: High reflective ASPHALT, 2: High reflective ASPHALT without AH,
|
|
41
|
+
# 3: CONCRETE (proxy of jari), 4: CONCRETE building,
|
|
42
|
+
# 5: Slate roof (Ordinal wooden house), 6: PADDY,
|
|
43
|
+
# 7: Dryland Cropland and Pasture, 8: Barren or Sparsely Vegetated,
|
|
44
|
+
# 9: WATER, 10: Grassland, 11: CONCRETE (proxy of block),
|
|
45
|
+
# 12: ASPHALT without AH, 13: ASPHALT,
|
|
46
|
+
# 14-17: Deciduous Broadleaf Forest
|
|
47
|
+
|
|
48
|
+
# OpenStreetMap / Standard (mapped to updated CityLES landuse codes)
|
|
43
49
|
OSM_CLASS_TO_CITYLES = {
|
|
44
|
-
'Bareland':
|
|
45
|
-
'Rangeland':
|
|
46
|
-
'Shrub':
|
|
47
|
-
'Moss and lichen':
|
|
48
|
-
'Agriculture land':
|
|
49
|
-
'Tree':
|
|
50
|
-
'Wet land':
|
|
51
|
-
'Mangroves':
|
|
52
|
-
'Water':
|
|
53
|
-
'Snow and ice':
|
|
54
|
-
'Developed space':
|
|
55
|
-
'Road':
|
|
56
|
-
'Building':
|
|
57
|
-
'No Data':
|
|
50
|
+
'Bareland': 8,
|
|
51
|
+
'Rangeland': 10,
|
|
52
|
+
'Shrub': 10,
|
|
53
|
+
'Moss and lichen': 10,
|
|
54
|
+
'Agriculture land': 7,
|
|
55
|
+
'Tree': 14,
|
|
56
|
+
'Wet land': 6,
|
|
57
|
+
'Mangroves': 14,
|
|
58
|
+
'Water': 9,
|
|
59
|
+
'Snow and ice': 8,
|
|
60
|
+
'Developed space': 4,
|
|
61
|
+
'Road': 13,
|
|
62
|
+
'Building': 4,
|
|
63
|
+
'No Data': 10
|
|
58
64
|
}
|
|
59
65
|
|
|
60
66
|
# Urbanwatch
|
|
61
67
|
URBANWATCH_CLASS_TO_CITYLES = {
|
|
62
|
-
'Building':
|
|
63
|
-
'Road':
|
|
64
|
-
'Parking Lot':
|
|
65
|
-
'Tree Canopy':
|
|
66
|
-
'Grass/Shrub':
|
|
67
|
-
'Agriculture':
|
|
68
|
-
'Water':
|
|
69
|
-
'Barren':
|
|
70
|
-
'Unknown':
|
|
71
|
-
'Sea':
|
|
68
|
+
'Building': 4,
|
|
69
|
+
'Road': 13,
|
|
70
|
+
'Parking Lot': 13,
|
|
71
|
+
'Tree Canopy': 14,
|
|
72
|
+
'Grass/Shrub': 10,
|
|
73
|
+
'Agriculture': 7,
|
|
74
|
+
'Water': 9,
|
|
75
|
+
'Barren': 8,
|
|
76
|
+
'Unknown': 10,
|
|
77
|
+
'Sea': 9
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
# OpenEarthMapJapan
|
|
75
81
|
OEMJ_CLASS_TO_CITYLES = {
|
|
76
|
-
'Bareland':
|
|
77
|
-
'Rangeland':
|
|
78
|
-
'Developed space':
|
|
79
|
-
'Road':
|
|
80
|
-
'Tree':
|
|
81
|
-
'Water':
|
|
82
|
-
'Agriculture land':
|
|
83
|
-
'Building':
|
|
82
|
+
'Bareland': 8,
|
|
83
|
+
'Rangeland': 10,
|
|
84
|
+
'Developed space': 4,
|
|
85
|
+
'Road': 13,
|
|
86
|
+
'Tree': 14,
|
|
87
|
+
'Water': 9,
|
|
88
|
+
'Agriculture land': 7,
|
|
89
|
+
'Building': 4
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
# ESA WorldCover
|
|
87
93
|
ESA_CLASS_TO_CITYLES = {
|
|
88
|
-
'Trees':
|
|
89
|
-
'Shrubland':
|
|
90
|
-
'Grassland':
|
|
91
|
-
'Cropland':
|
|
92
|
-
'Built-up':
|
|
93
|
-
'Barren / sparse vegetation':
|
|
94
|
-
'Snow and ice':
|
|
95
|
-
'Open water':
|
|
96
|
-
'Herbaceous wetland':
|
|
97
|
-
'Mangroves':
|
|
98
|
-
'Moss and lichen':
|
|
94
|
+
'Trees': 14,
|
|
95
|
+
'Shrubland': 10,
|
|
96
|
+
'Grassland': 10,
|
|
97
|
+
'Cropland': 7,
|
|
98
|
+
'Built-up': 4,
|
|
99
|
+
'Barren / sparse vegetation': 8,
|
|
100
|
+
'Snow and ice': 8,
|
|
101
|
+
'Open water': 9,
|
|
102
|
+
'Herbaceous wetland': 6,
|
|
103
|
+
'Mangroves': 14,
|
|
104
|
+
'Moss and lichen': 10
|
|
99
105
|
}
|
|
100
106
|
|
|
101
107
|
# ESRI 10m Annual Land Cover
|
|
102
108
|
ESRI_CLASS_TO_CITYLES = {
|
|
103
|
-
'No Data':
|
|
104
|
-
'Water':
|
|
105
|
-
'Trees':
|
|
106
|
-
'Grass':
|
|
107
|
-
'Flooded Vegetation':
|
|
108
|
-
'Crops':
|
|
109
|
-
'Scrub/Shrub':
|
|
110
|
-
'Built Area':
|
|
111
|
-
'Bare Ground':
|
|
112
|
-
'Snow/Ice':
|
|
113
|
-
'Clouds':
|
|
109
|
+
'No Data': 10,
|
|
110
|
+
'Water': 9,
|
|
111
|
+
'Trees': 14,
|
|
112
|
+
'Grass': 10,
|
|
113
|
+
'Flooded Vegetation': 6,
|
|
114
|
+
'Crops': 7,
|
|
115
|
+
'Scrub/Shrub': 10,
|
|
116
|
+
'Built Area': 4,
|
|
117
|
+
'Bare Ground': 8,
|
|
118
|
+
'Snow/Ice': 8,
|
|
119
|
+
'Clouds': 10
|
|
114
120
|
}
|
|
115
121
|
|
|
116
122
|
# Dynamic World V1
|
|
117
123
|
DYNAMIC_WORLD_CLASS_TO_CITYLES = {
|
|
118
|
-
'Water':
|
|
119
|
-
'Trees':
|
|
120
|
-
'Grass':
|
|
121
|
-
'Flooded Vegetation':
|
|
122
|
-
'Crops':
|
|
123
|
-
'Shrub and Scrub':
|
|
124
|
-
'Built':
|
|
125
|
-
'Bare':
|
|
126
|
-
'Snow and Ice':
|
|
124
|
+
'Water': 9,
|
|
125
|
+
'Trees': 14,
|
|
126
|
+
'Grass': 10,
|
|
127
|
+
'Flooded Vegetation': 6,
|
|
128
|
+
'Crops': 7,
|
|
129
|
+
'Shrub and Scrub': 10,
|
|
130
|
+
'Built': 4,
|
|
131
|
+
'Bare': 8,
|
|
132
|
+
'Snow and Ice': 8
|
|
127
133
|
}
|
|
128
134
|
|
|
129
|
-
# Building material mapping based on corrected
|
|
135
|
+
# Building material mapping based on corrected definitions (101-117)
|
|
130
136
|
BUILDING_MATERIAL_MAPPING = {
|
|
131
|
-
'building':
|
|
132
|
-
'concrete':
|
|
133
|
-
'residential':
|
|
134
|
-
'wooden':
|
|
135
|
-
'commercial':
|
|
136
|
-
'industrial':
|
|
137
|
-
'default':
|
|
137
|
+
'building': 104, # CONCRETE building
|
|
138
|
+
'concrete': 104, # CONCRETE building
|
|
139
|
+
'residential': 105, # Slate roof (Ordinal wooden house)
|
|
140
|
+
'wooden': 105, # Slate roof (Ordinal wooden house)
|
|
141
|
+
'commercial': 104, # CONCRETE building
|
|
142
|
+
'industrial': 104, # CONCRETE building
|
|
143
|
+
'default': 104 # Default to CONCRETE building
|
|
138
144
|
}
|
|
139
145
|
|
|
146
|
+
# Helper to convert landuse code (1-17) to building material code (101-117)
|
|
147
|
+
def landuse_to_building_material_code(landuse_code: int) -> int:
|
|
148
|
+
"""Map landuse code to building-material code with required adjustments.
|
|
149
|
+
|
|
150
|
+
The general rule is 100 + landuse_code, except for ASPHALT classes where
|
|
151
|
+
landuse 12 (ASPHALT without AH) maps to 113 and landuse 13 (ASPHALT) maps to 112.
|
|
152
|
+
"""
|
|
153
|
+
# if landuse_code == 12:
|
|
154
|
+
# return 113
|
|
155
|
+
# if landuse_code == 13:
|
|
156
|
+
# return 112
|
|
157
|
+
return 100 + int(landuse_code)
|
|
158
|
+
|
|
140
159
|
# Tree type mapping for vmap.txt
|
|
141
160
|
TREE_TYPE_MAPPING = {
|
|
142
161
|
'deciduous': 101, # Leaf
|
|
@@ -229,7 +248,7 @@ def export_topog(building_height_grid, building_id_grid, output_path,
|
|
|
229
248
|
# Decide material code per cell
|
|
230
249
|
if cityles_landuse_grid is not None:
|
|
231
250
|
cell_lu = int(cityles_landuse_grid[j, i])
|
|
232
|
-
material_code_cell = cell_lu
|
|
251
|
+
material_code_cell = landuse_to_building_material_code(cell_lu)
|
|
233
252
|
else:
|
|
234
253
|
if height > 0:
|
|
235
254
|
material_code_cell = material_code
|
|
@@ -2385,188 +2385,23 @@ def _rgb_tuple_to_plotly_color(rgb_tuple):
|
|
|
2385
2385
|
except Exception:
|
|
2386
2386
|
return "rgb(128,128,128)"
|
|
2387
2387
|
|
|
2388
|
-
|
|
2389
|
-
def visualize_building_sim_results_plotly(
|
|
2390
|
-
voxel_array,
|
|
2391
|
-
meshsize,
|
|
2392
|
-
building_sim_mesh,
|
|
2393
|
-
**kwargs
|
|
2394
|
-
):
|
|
2388
|
+
def _mpl_cmap_to_plotly_colorscale(cmap_name, n=256):
|
|
2395
2389
|
"""
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
This function reuses visualize_voxcity_plotly to render voxel cubes and adds a
|
|
2399
|
-
Plotly Mesh3d trace for a provided building simulation mesh (e.g., SVF, temperature).
|
|
2400
|
-
|
|
2401
|
-
Parameters (kwargs)
|
|
2402
|
-
-------------------
|
|
2403
|
-
classes : list[int] or None
|
|
2404
|
-
Classes to render for voxel cubes. Default: all non-zero classes present.
|
|
2405
|
-
voxel_color_map : str or dict
|
|
2406
|
-
Scheme name understood by get_voxel_color_map or explicit mapping {class_id: [R,G,B]}.
|
|
2407
|
-
downsample : int or None
|
|
2408
|
-
Stride for voxel cubes. 1 means no downsampling.
|
|
2409
|
-
cubes_opacity : float
|
|
2410
|
-
Opacity for voxel cubes (default 0.95 for buildings, 0.6 for others via function's per-class logic; here default 0.9).
|
|
2411
|
-
title : str
|
|
2412
|
-
Figure title.
|
|
2413
|
-
width, height : int
|
|
2414
|
-
Figure size.
|
|
2415
|
-
value_name : str
|
|
2416
|
-
Metadata field name on building_sim_mesh storing per-face values (default 'svf_values').
|
|
2417
|
-
colormap : str
|
|
2418
|
-
Matplotlib colormap name for the simulation values (default 'viridis').
|
|
2419
|
-
vmin, vmax : float or None
|
|
2420
|
-
Value range for color mapping. If None, computed from finite values.
|
|
2421
|
-
nan_color : str
|
|
2422
|
-
Color name for NaN values (default 'gray').
|
|
2423
|
-
building_opacity : float
|
|
2424
|
-
Opacity for the simulation mesh (default 1.0).
|
|
2425
|
-
shaded : bool
|
|
2426
|
-
If True, apply lighting-based shading to the simulation mesh. Default False (unlit colors).
|
|
2427
|
-
render_voxel_buildings : bool
|
|
2428
|
-
If True, also render voxel buildings (-3) from voxcity_grid. Default False (hide voxel buildings
|
|
2429
|
-
so only simulation mesh buildings are visible).
|
|
2430
|
-
show : bool, return_fig : bool
|
|
2431
|
-
Standard display controls.
|
|
2390
|
+
Convert a matplotlib colormap name to a Plotly colorscale list.
|
|
2432
2391
|
"""
|
|
2433
|
-
|
|
2434
|
-
voxel_color_map = kwargs.get('voxel_color_map', 'default')
|
|
2435
|
-
downsample = kwargs.get('downsample')
|
|
2436
|
-
title = kwargs.get('title', None)
|
|
2437
|
-
width = kwargs.get('width', 1000)
|
|
2438
|
-
height = kwargs.get('height', 800)
|
|
2439
|
-
cubes_opacity = kwargs.get('cubes_opacity', 0.9)
|
|
2440
|
-
show = kwargs.get('show', True)
|
|
2441
|
-
return_fig = kwargs.get('return_fig', False)
|
|
2442
|
-
render_voxel_buildings = kwargs.get('render_voxel_buildings', False)
|
|
2443
|
-
|
|
2444
|
-
# Determine classes for voxel cubes and exclude buildings (-3) by default
|
|
2445
|
-
if classes is None:
|
|
2446
|
-
classes_all = np.unique(voxel_array[voxel_array != 0]).tolist()
|
|
2447
|
-
else:
|
|
2448
|
-
classes_all = list(classes)
|
|
2449
|
-
classes_cubes = classes_all if render_voxel_buildings else [c for c in classes_all if int(c) != -3]
|
|
2450
|
-
|
|
2451
|
-
# Render voxel cubes background (or blank scene if nothing to render)
|
|
2452
|
-
if len(classes_cubes) > 0:
|
|
2453
|
-
fig = visualize_voxcity_plotly(
|
|
2454
|
-
voxel_array,
|
|
2455
|
-
meshsize,
|
|
2456
|
-
classes=classes_cubes,
|
|
2457
|
-
voxel_color_map=voxel_color_map,
|
|
2458
|
-
opacity=cubes_opacity,
|
|
2459
|
-
downsample=downsample,
|
|
2460
|
-
title=title or "Building Simulation (Plotly)",
|
|
2461
|
-
width=width,
|
|
2462
|
-
height=height,
|
|
2463
|
-
show=False,
|
|
2464
|
-
return_fig=True,
|
|
2465
|
-
)
|
|
2466
|
-
else:
|
|
2467
|
-
fig = go.Figure()
|
|
2468
|
-
fig.update_layout(
|
|
2469
|
-
title=title or "Building Simulation (Plotly)",
|
|
2470
|
-
width=width,
|
|
2471
|
-
height=height,
|
|
2472
|
-
scene=dict(
|
|
2473
|
-
xaxis_title="X (m)",
|
|
2474
|
-
yaxis_title="Y (m)",
|
|
2475
|
-
zaxis_title="Z (m)",
|
|
2476
|
-
aspectmode="data",
|
|
2477
|
-
camera=dict(eye=dict(x=1.6, y=1.6, z=1.0)),
|
|
2478
|
-
)
|
|
2479
|
-
)
|
|
2480
|
-
|
|
2481
|
-
# Nothing to overlay
|
|
2482
|
-
if building_sim_mesh is None or getattr(building_sim_mesh, 'vertices', None) is None:
|
|
2483
|
-
if show:
|
|
2484
|
-
fig.show()
|
|
2485
|
-
if return_fig:
|
|
2486
|
-
return fig
|
|
2487
|
-
return None
|
|
2488
|
-
|
|
2489
|
-
# Extract geometry
|
|
2490
|
-
V = np.asarray(building_sim_mesh.vertices)
|
|
2491
|
-
F = np.asarray(building_sim_mesh.faces)
|
|
2492
|
-
values = None
|
|
2493
|
-
value_name = kwargs.get('value_name', 'svf_values')
|
|
2494
|
-
if hasattr(building_sim_mesh, 'metadata') and isinstance(building_sim_mesh.metadata, dict):
|
|
2495
|
-
values = building_sim_mesh.metadata.get(value_name)
|
|
2496
|
-
if values is not None:
|
|
2497
|
-
values = np.asarray(values)
|
|
2498
|
-
|
|
2499
|
-
# Compute per-face scalar to avoid color interpolation across edges
|
|
2500
|
-
face_vals = None
|
|
2501
|
-
if values is not None and values.size == len(F):
|
|
2502
|
-
# Already per-face
|
|
2503
|
-
face_vals = values.astype(float)
|
|
2504
|
-
elif values is not None and values.size == len(V):
|
|
2505
|
-
# Average the three vertex values per face
|
|
2506
|
-
vals_v = values.astype(float)
|
|
2507
|
-
face_vals = np.nanmean(vals_v[F], axis=1)
|
|
2508
|
-
|
|
2509
|
-
# Map to colors
|
|
2510
|
-
cmap_name = kwargs.get('colormap', 'viridis')
|
|
2511
|
-
vmin = kwargs.get('vmin')
|
|
2512
|
-
vmax = kwargs.get('vmax')
|
|
2513
|
-
nan_color = kwargs.get('nan_color', 'gray')
|
|
2514
|
-
building_opacity = kwargs.get('building_opacity', 1.0)
|
|
2515
|
-
|
|
2516
|
-
facecolor = None
|
|
2517
|
-
if face_vals is not None:
|
|
2518
|
-
# Compute range
|
|
2519
|
-
finite = np.isfinite(face_vals)
|
|
2520
|
-
if vmin is None:
|
|
2521
|
-
vmin = float(np.nanmin(face_vals[finite])) if np.any(finite) else 0.0
|
|
2522
|
-
if vmax is None:
|
|
2523
|
-
vmax = float(np.nanmax(face_vals[finite])) if np.any(finite) else 1.0
|
|
2524
|
-
norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
|
|
2392
|
+
try:
|
|
2525
2393
|
cmap = cm.get_cmap(cmap_name)
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
shaded = kwargs.get('shaded', False)
|
|
2537
|
-
if shaded:
|
|
2538
|
-
lighting = dict(ambient=0.35, diffuse=1.0, specular=0.4, roughness=0.5, fresnel=0.1)
|
|
2539
|
-
flat = False
|
|
2540
|
-
else:
|
|
2541
|
-
# Unlit: make colors independent of lighting
|
|
2542
|
-
lighting = dict(ambient=1.0, diffuse=0.0, specular=0.0, roughness=0.0, fresnel=0.0)
|
|
2543
|
-
flat = False
|
|
2544
|
-
cx = float((V[:,0].min() + V[:,0].max()) * 0.5)
|
|
2545
|
-
cy = float((V[:,1].min() + V[:,1].max()) * 0.5)
|
|
2546
|
-
cz = float((V[:,2].min() + V[:,2].max()) * 0.5)
|
|
2547
|
-
lx = cx + (V[:,0].max() - V[:,0].min() + meshsize) * 0.9
|
|
2548
|
-
ly = cy + (V[:,1].max() - V[:,1].min() + meshsize) * 0.6
|
|
2549
|
-
lz = cz + (V[:,2].max() - V[:,2].min() + meshsize) * 1.4
|
|
2550
|
-
|
|
2551
|
-
fig.add_trace(
|
|
2552
|
-
go.Mesh3d(
|
|
2553
|
-
x=V[:,0], y=V[:,1], z=V[:,2],
|
|
2554
|
-
i=F[:,0], j=F[:,1], k=F[:,2],
|
|
2555
|
-
facecolor=facecolor if facecolor is not None else None,
|
|
2556
|
-
color=None if facecolor is not None else 'rgb(200,200,200)',
|
|
2557
|
-
opacity=float(building_opacity),
|
|
2558
|
-
flatshading=flat,
|
|
2559
|
-
lighting=lighting,
|
|
2560
|
-
lightposition=dict(x=lx, y=ly, z=lz),
|
|
2561
|
-
name=value_name if facecolor is not None else 'building_mesh'
|
|
2562
|
-
)
|
|
2563
|
-
)
|
|
2394
|
+
except Exception:
|
|
2395
|
+
cmap = cm.get_cmap('viridis')
|
|
2396
|
+
if n < 2:
|
|
2397
|
+
n = 2
|
|
2398
|
+
scale = []
|
|
2399
|
+
for i in range(n):
|
|
2400
|
+
x = i / (n - 1)
|
|
2401
|
+
r, g, b, _ = cmap(x)
|
|
2402
|
+
scale.append([x, f"rgb({int(255*r)},{int(255*g)},{int(255*b)})"])
|
|
2403
|
+
return scale
|
|
2564
2404
|
|
|
2565
|
-
if show:
|
|
2566
|
-
fig.show()
|
|
2567
|
-
if return_fig:
|
|
2568
|
-
return fig
|
|
2569
|
-
return None
|
|
2570
2405
|
|
|
2571
2406
|
def visualize_voxcity_plotly(
|
|
2572
2407
|
voxel_array,
|
|
@@ -2581,55 +2416,123 @@ def visualize_voxcity_plotly(
|
|
|
2581
2416
|
height=800,
|
|
2582
2417
|
show=True,
|
|
2583
2418
|
return_fig=False,
|
|
2419
|
+
# Building simulation overlay
|
|
2420
|
+
building_sim_mesh=None,
|
|
2421
|
+
building_value_name='svf_values',
|
|
2422
|
+
building_colormap='viridis',
|
|
2423
|
+
building_vmin=None,
|
|
2424
|
+
building_vmax=None,
|
|
2425
|
+
building_nan_color='gray',
|
|
2426
|
+
building_opacity=1.0,
|
|
2427
|
+
building_shaded=False,
|
|
2428
|
+
render_voxel_buildings=False,
|
|
2429
|
+
# Ground simulation surface overlay
|
|
2430
|
+
ground_sim_grid=None,
|
|
2431
|
+
ground_dem_grid=None,
|
|
2432
|
+
ground_z_offset=None,
|
|
2433
|
+
ground_view_point_height=None,
|
|
2434
|
+
ground_colormap='viridis',
|
|
2435
|
+
ground_vmin=None,
|
|
2436
|
+
ground_vmax=None,
|
|
2437
|
+
sim_surface_opacity=0.95,
|
|
2438
|
+
ground_shaded=False,
|
|
2584
2439
|
):
|
|
2585
2440
|
"""
|
|
2586
|
-
Interactive 3D visualization
|
|
2587
|
-
|
|
2441
|
+
Interactive 3D visualization using Plotly Mesh3d which can render:
|
|
2442
|
+
- Voxel cubes (one Mesh3d trace per exposed face per class)
|
|
2443
|
+
- Optional building-surface simulation mesh overlay
|
|
2444
|
+
- Optional ground-level simulation surface overlay (triangulated)
|
|
2588
2445
|
|
|
2589
|
-
Parameters
|
|
2590
|
-
|
|
2591
|
-
|
|
2446
|
+
Parameters
|
|
2447
|
+
----------
|
|
2448
|
+
voxel_array : np.ndarray (nx, ny, nz)
|
|
2449
|
+
Voxel class grid. Required for voxel rendering; can be None if only overlays are shown.
|
|
2450
|
+
meshsize : float
|
|
2451
|
+
Cell size (m) for converting voxel indices to metric coordinates.
|
|
2452
|
+
classes : list[int] or None
|
|
2453
|
+
Classes to render for voxel cubes. Default: all non-zero present in the array.
|
|
2454
|
+
voxel_color_map : str or dict
|
|
2455
|
+
Scheme name understood by get_voxel_color_map, or mapping {class_id: [R,G,B]}.
|
|
2456
|
+
opacity : float
|
|
2457
|
+
Opacity for voxel cubes.
|
|
2458
|
+
max_dimension : int
|
|
2459
|
+
Target maximum dimension for auto-downsampling.
|
|
2460
|
+
downsample : int or None
|
|
2461
|
+
Explicit stride for voxels. If None, auto-downsample when needed.
|
|
2462
|
+
title, width, height, show, return_fig : standard display controls.
|
|
2463
|
+
|
|
2464
|
+
Building overlay
|
|
2465
|
+
----------------
|
|
2466
|
+
building_sim_mesh : trimesh.Trimesh or similar with .vertices, .faces
|
|
2467
|
+
building_value_name : str (default 'svf_values')
|
|
2468
|
+
building_colormap : str (default 'viridis')
|
|
2469
|
+
building_vmin, building_vmax : float or None
|
|
2470
|
+
building_nan_color : str (default 'gray')
|
|
2471
|
+
building_opacity : float (default 1.0)
|
|
2472
|
+
building_shaded : bool (default False)
|
|
2473
|
+
render_voxel_buildings : bool (default False; when False, exclude class -3 from voxel cubes)
|
|
2474
|
+
|
|
2475
|
+
Ground overlay
|
|
2476
|
+
--------------
|
|
2477
|
+
ground_sim_grid : 2D array of values
|
|
2478
|
+
ground_dem_grid : 2D array of ground elevations
|
|
2479
|
+
ground_z_offset or ground_view_point_height : float above DEM (default 1.5 m)
|
|
2480
|
+
ground_colormap : str (default 'viridis')
|
|
2481
|
+
ground_vmin, ground_vmax : float or None
|
|
2482
|
+
sim_surface_opacity : float (default 0.95)
|
|
2483
|
+
ground_shaded : bool (default False)
|
|
2592
2484
|
"""
|
|
2593
2485
|
if voxel_array is None or getattr(voxel_array, 'ndim', 0) != 3:
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2486
|
+
# Allow overlays without voxels
|
|
2487
|
+
if building_sim_mesh is None and (ground_sim_grid is None or ground_dem_grid is None):
|
|
2488
|
+
raise ValueError("voxel_array must be a 3D numpy array (nx, ny, nz) when no overlays are provided")
|
|
2489
|
+
vox = None
|
|
2490
|
+
else:
|
|
2491
|
+
vox = voxel_array
|
|
2597
2492
|
|
|
2598
2493
|
# Downsample for performance if requested or auto-needed
|
|
2599
2494
|
# Respect explicit downsample even when it is 1 (no auto-downsample)
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2495
|
+
stride = 1
|
|
2496
|
+
if vox is not None:
|
|
2497
|
+
if downsample is not None:
|
|
2498
|
+
stride = max(1, int(downsample))
|
|
2499
|
+
else:
|
|
2500
|
+
nx_tmp, ny_tmp, nz_tmp = vox.shape
|
|
2501
|
+
max_dim = max(nx_tmp, ny_tmp, nz_tmp)
|
|
2502
|
+
if max_dim > max_dimension:
|
|
2503
|
+
stride = int(np.ceil(max_dim / max_dimension))
|
|
2504
|
+
if stride > 1:
|
|
2505
|
+
vox = vox[::stride, ::stride, ::stride]
|
|
2506
|
+
|
|
2604
2507
|
nx, ny, nz = vox.shape
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2508
|
+
|
|
2509
|
+
# Coordinate of voxel centers in meters
|
|
2510
|
+
dx = meshsize * stride
|
|
2511
|
+
dy = meshsize * stride
|
|
2512
|
+
dz = meshsize * stride
|
|
2513
|
+
x = np.arange(nx, dtype=float) * dx
|
|
2514
|
+
y = np.arange(ny, dtype=float) * dy
|
|
2515
|
+
z = np.arange(nz, dtype=float) * dz
|
|
2516
|
+
|
|
2517
|
+
# Choose classes
|
|
2518
|
+
if classes is None:
|
|
2519
|
+
classes_all = np.unique(vox[vox != 0]).tolist()
|
|
2520
|
+
else:
|
|
2521
|
+
classes_all = list(classes)
|
|
2522
|
+
# Exclude building voxels (-3) only when a building overlay is provided and hiding is desired
|
|
2523
|
+
if building_sim_mesh is not None and getattr(building_sim_mesh, 'vertices', None) is not None:
|
|
2524
|
+
if render_voxel_buildings:
|
|
2525
|
+
classes_to_draw = classes_all
|
|
2526
|
+
else:
|
|
2527
|
+
classes_to_draw = [c for c in classes_all if int(c) != -3]
|
|
2528
|
+
else:
|
|
2529
|
+
classes_to_draw = classes_all
|
|
2530
|
+
|
|
2531
|
+
# Resolve color map: accept scheme name or explicit dict
|
|
2532
|
+
if isinstance(voxel_color_map, dict):
|
|
2533
|
+
vox_dict = voxel_color_map
|
|
2534
|
+
else:
|
|
2535
|
+
vox_dict = get_voxel_color_map(voxel_color_map)
|
|
2633
2536
|
|
|
2634
2537
|
def exposed_face_masks(occ):
|
|
2635
2538
|
# occ shape (nx, ny, nz)
|
|
@@ -2730,21 +2633,204 @@ def visualize_voxcity_plotly(
|
|
|
2730
2633
|
|
|
2731
2634
|
fig = go.Figure()
|
|
2732
2635
|
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2636
|
+
# Draw voxel cubes if available
|
|
2637
|
+
if vox is not None and classes_to_draw:
|
|
2638
|
+
for cls in classes_to_draw:
|
|
2639
|
+
if not np.any(vox == cls):
|
|
2640
|
+
continue
|
|
2641
|
+
occ = (vox == cls)
|
|
2642
|
+
posx, negx, posy, negy, posz, negz = exposed_face_masks(occ)
|
|
2643
|
+
color_rgb = vox_dict.get(int(cls), [128,128,128])
|
|
2644
|
+
add_faces(fig, posx, '+x', color_rgb)
|
|
2645
|
+
add_faces(fig, negx, '-x', color_rgb)
|
|
2646
|
+
add_faces(fig, posy, '+y', color_rgb)
|
|
2647
|
+
add_faces(fig, negy, '-y', color_rgb)
|
|
2648
|
+
add_faces(fig, posz, '+z', color_rgb)
|
|
2649
|
+
add_faces(fig, negz, '-z', color_rgb)
|
|
2650
|
+
|
|
2651
|
+
# Building simulation mesh overlay
|
|
2652
|
+
if building_sim_mesh is not None and getattr(building_sim_mesh, 'vertices', None) is not None:
|
|
2653
|
+
Vb = np.asarray(building_sim_mesh.vertices)
|
|
2654
|
+
Fb = np.asarray(building_sim_mesh.faces)
|
|
2655
|
+
|
|
2656
|
+
# Values can be stored in metadata under building_value_name
|
|
2657
|
+
values = None
|
|
2658
|
+
if hasattr(building_sim_mesh, 'metadata') and isinstance(building_sim_mesh.metadata, dict):
|
|
2659
|
+
values = building_sim_mesh.metadata.get(building_value_name)
|
|
2660
|
+
if values is not None:
|
|
2661
|
+
values = np.asarray(values)
|
|
2662
|
+
|
|
2663
|
+
face_vals = None
|
|
2664
|
+
if values is not None and values.size == len(Fb):
|
|
2665
|
+
face_vals = values.astype(float)
|
|
2666
|
+
elif values is not None and values.size == len(Vb):
|
|
2667
|
+
vals_v = values.astype(float)
|
|
2668
|
+
face_vals = np.nanmean(vals_v[Fb], axis=1)
|
|
2669
|
+
|
|
2670
|
+
facecolor = None
|
|
2671
|
+
if face_vals is not None:
|
|
2672
|
+
finite = np.isfinite(face_vals)
|
|
2673
|
+
vmin_b = building_vmin if building_vmin is not None else (float(np.nanmin(face_vals[finite])) if np.any(finite) else 0.0)
|
|
2674
|
+
vmax_b = building_vmax if building_vmax is not None else (float(np.nanmax(face_vals[finite])) if np.any(finite) else 1.0)
|
|
2675
|
+
norm = mcolors.Normalize(vmin=vmin_b, vmax=vmax_b)
|
|
2676
|
+
cmap = cm.get_cmap(building_colormap)
|
|
2677
|
+
colors_rgba = np.zeros((len(Fb), 4), dtype=float)
|
|
2678
|
+
colors_rgba[finite] = cmap(norm(face_vals[finite]))
|
|
2679
|
+
nan_rgba = np.array(mcolors.to_rgba(building_nan_color))
|
|
2680
|
+
colors_rgba[~finite] = nan_rgba
|
|
2681
|
+
facecolor = [f"rgb({int(255*c[0])},{int(255*c[1])},{int(255*c[2])})" for c in colors_rgba]
|
|
2682
|
+
|
|
2683
|
+
if building_shaded:
|
|
2684
|
+
lighting_b = dict(ambient=0.35, diffuse=1.0, specular=0.4, roughness=0.5, fresnel=0.1)
|
|
2685
|
+
flat_b = False
|
|
2686
|
+
else:
|
|
2687
|
+
lighting_b = dict(ambient=1.0, diffuse=0.0, specular=0.0, roughness=0.0, fresnel=0.0)
|
|
2688
|
+
flat_b = False
|
|
2689
|
+
|
|
2690
|
+
# Place a directional light near mesh center
|
|
2691
|
+
cx = float((Vb[:,0].min() + Vb[:,0].max()) * 0.5)
|
|
2692
|
+
cy = float((Vb[:,1].min() + Vb[:,1].max()) * 0.5)
|
|
2693
|
+
cz = float((Vb[:,2].min() + Vb[:,2].max()) * 0.5)
|
|
2694
|
+
lx = cx + (Vb[:,0].max() - Vb[:,0].min() + meshsize) * 0.9
|
|
2695
|
+
ly = cy + (Vb[:,1].max() - Vb[:,1].min() + meshsize) * 0.6
|
|
2696
|
+
lz = cz + (Vb[:,2].max() - Vb[:,2].min() + meshsize) * 1.4
|
|
2697
|
+
|
|
2698
|
+
fig.add_trace(
|
|
2699
|
+
go.Mesh3d(
|
|
2700
|
+
x=Vb[:,0], y=Vb[:,1], z=Vb[:,2],
|
|
2701
|
+
i=Fb[:,0], j=Fb[:,1], k=Fb[:,2],
|
|
2702
|
+
facecolor=facecolor if facecolor is not None else None,
|
|
2703
|
+
color=None if facecolor is not None else 'rgb(200,200,200)',
|
|
2704
|
+
opacity=float(building_opacity),
|
|
2705
|
+
flatshading=flat_b,
|
|
2706
|
+
lighting=lighting_b,
|
|
2707
|
+
lightposition=dict(x=lx, y=ly, z=lz),
|
|
2708
|
+
name=building_value_name if facecolor is not None else 'building_mesh'
|
|
2709
|
+
)
|
|
2710
|
+
)
|
|
2711
|
+
|
|
2712
|
+
# Colorbar for building overlay
|
|
2713
|
+
if face_vals is not None:
|
|
2714
|
+
colorscale_b = _mpl_cmap_to_plotly_colorscale(building_colormap)
|
|
2715
|
+
fig.add_trace(
|
|
2716
|
+
go.Scatter3d(
|
|
2717
|
+
x=[None], y=[None], z=[None],
|
|
2718
|
+
mode='markers',
|
|
2719
|
+
marker=dict(
|
|
2720
|
+
size=0.1,
|
|
2721
|
+
color=[vmin_b, vmax_b],
|
|
2722
|
+
colorscale=colorscale_b,
|
|
2723
|
+
cmin=vmin_b,
|
|
2724
|
+
cmax=vmax_b,
|
|
2725
|
+
colorbar=dict(title=building_value_name, len=0.5, y=0.8),
|
|
2726
|
+
showscale=True,
|
|
2727
|
+
),
|
|
2728
|
+
showlegend=False,
|
|
2729
|
+
hoverinfo='skip',
|
|
2730
|
+
)
|
|
2731
|
+
)
|
|
2732
|
+
|
|
2733
|
+
# Ground simulation surface overlay
|
|
2734
|
+
if ground_sim_grid is not None and ground_dem_grid is not None:
|
|
2735
|
+
sim_vals = np.asarray(ground_sim_grid, dtype=float)
|
|
2736
|
+
finite = np.isfinite(sim_vals)
|
|
2737
|
+
vmin_g = ground_vmin if ground_vmin is not None else (float(np.nanmin(sim_vals[finite])) if np.any(finite) else 0.0)
|
|
2738
|
+
vmax_g = ground_vmax if ground_vmax is not None else (float(np.nanmax(sim_vals[finite])) if np.any(finite) else 1.0)
|
|
2739
|
+
|
|
2740
|
+
# Determine z offset
|
|
2741
|
+
z_off = ground_z_offset if ground_z_offset is not None else ground_view_point_height
|
|
2742
|
+
try:
|
|
2743
|
+
z_off = float(z_off) if z_off is not None else 1.5
|
|
2744
|
+
except Exception:
|
|
2745
|
+
z_off = 1.5
|
|
2746
|
+
if meshsize is not None:
|
|
2747
|
+
try:
|
|
2748
|
+
ms = float(meshsize)
|
|
2749
|
+
if z_off < ms:
|
|
2750
|
+
z_off = ms
|
|
2751
|
+
z_off = ms * math.ceil(z_off / ms)
|
|
2752
|
+
except Exception:
|
|
2753
|
+
pass
|
|
2754
|
+
|
|
2755
|
+
# Normalize DEM so its minimum becomes 0, matching voxel Z coordinates
|
|
2756
|
+
try:
|
|
2757
|
+
dem_norm = np.asarray(ground_dem_grid, dtype=float)
|
|
2758
|
+
dem_norm = dem_norm - np.nanmin(dem_norm)
|
|
2759
|
+
except Exception:
|
|
2760
|
+
dem_norm = ground_dem_grid
|
|
2761
|
+
|
|
2762
|
+
sim_mesh = create_sim_surface_mesh(
|
|
2763
|
+
ground_sim_grid,
|
|
2764
|
+
dem_norm,
|
|
2765
|
+
meshsize=meshsize,
|
|
2766
|
+
z_offset=z_off,
|
|
2767
|
+
cmap_name=ground_colormap,
|
|
2768
|
+
vmin=vmin_g,
|
|
2769
|
+
vmax=vmax_g,
|
|
2770
|
+
)
|
|
2771
|
+
|
|
2772
|
+
if sim_mesh is not None and getattr(sim_mesh, 'vertices', None) is not None:
|
|
2773
|
+
V = np.asarray(sim_mesh.vertices)
|
|
2774
|
+
F = np.asarray(sim_mesh.faces)
|
|
2775
|
+
|
|
2776
|
+
facecolor = None
|
|
2777
|
+
try:
|
|
2778
|
+
colors_rgba = np.asarray(sim_mesh.visual.face_colors)
|
|
2779
|
+
if colors_rgba.ndim == 2 and colors_rgba.shape[0] == len(F):
|
|
2780
|
+
facecolor = [f"rgb({int(c[0])},{int(c[1])},{int(c[2])})" for c in colors_rgba]
|
|
2781
|
+
except Exception:
|
|
2782
|
+
facecolor = None
|
|
2783
|
+
|
|
2784
|
+
if ground_shaded:
|
|
2785
|
+
lighting = dict(ambient=0.35, diffuse=1.0, specular=0.4, roughness=0.5, fresnel=0.1)
|
|
2786
|
+
flat = False
|
|
2787
|
+
else:
|
|
2788
|
+
lighting = dict(ambient=1.0, diffuse=0.0, specular=0.0, roughness=0.0, fresnel=0.0)
|
|
2789
|
+
flat = False
|
|
2790
|
+
|
|
2791
|
+
cx = float((V[:,0].min() + V[:,0].max()) * 0.5)
|
|
2792
|
+
cy = float((V[:,1].min() + V[:,1].max()) * 0.5)
|
|
2793
|
+
cz = float((V[:,2].min() + V[:,2].max()) * 0.5)
|
|
2794
|
+
lx = cx + (V[:,0].max() - V[:,0].min() + meshsize) * 0.9
|
|
2795
|
+
ly = cy + (V[:,1].max() - V[:,1].min() + meshsize) * 0.6
|
|
2796
|
+
lz = cz + (V[:,2].max() - V[:,2].min() + meshsize) * 1.4
|
|
2797
|
+
|
|
2798
|
+
fig.add_trace(
|
|
2799
|
+
go.Mesh3d(
|
|
2800
|
+
x=V[:,0], y=V[:,1], z=V[:,2],
|
|
2801
|
+
i=F[:,0], j=F[:,1], k=F[:,2],
|
|
2802
|
+
facecolor=facecolor,
|
|
2803
|
+
color=None if facecolor is not None else 'rgb(200,200,200)',
|
|
2804
|
+
opacity=float(sim_surface_opacity),
|
|
2805
|
+
flatshading=flat,
|
|
2806
|
+
lighting=lighting,
|
|
2807
|
+
lightposition=dict(x=lx, y=ly, z=lz),
|
|
2808
|
+
name='sim_surface'
|
|
2809
|
+
)
|
|
2810
|
+
)
|
|
2811
|
+
|
|
2812
|
+
# Colorbar for ground overlay
|
|
2813
|
+
colorscale_g = _mpl_cmap_to_plotly_colorscale(ground_colormap)
|
|
2814
|
+
fig.add_trace(
|
|
2815
|
+
go.Scatter3d(
|
|
2816
|
+
x=[None], y=[None], z=[None],
|
|
2817
|
+
mode='markers',
|
|
2818
|
+
marker=dict(
|
|
2819
|
+
size=0.1,
|
|
2820
|
+
color=[vmin_g, vmax_g],
|
|
2821
|
+
colorscale=colorscale_g,
|
|
2822
|
+
cmin=vmin_g,
|
|
2823
|
+
cmax=vmax_g,
|
|
2824
|
+
colorbar=dict(title='ground', len=0.5, y=0.2),
|
|
2825
|
+
showscale=True,
|
|
2826
|
+
),
|
|
2827
|
+
showlegend=False,
|
|
2828
|
+
hoverinfo='skip',
|
|
2829
|
+
)
|
|
2830
|
+
)
|
|
2745
2831
|
|
|
2746
2832
|
fig.update_layout(
|
|
2747
|
-
title=title or "VoxCity 3D
|
|
2833
|
+
title=title or "VoxCity 3D",
|
|
2748
2834
|
width=width,
|
|
2749
2835
|
height=height,
|
|
2750
2836
|
scene=dict(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|