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.

Files changed (38) hide show
  1. {voxcity-0.6.23 → voxcity-0.6.25}/PKG-INFO +1 -1
  2. {voxcity-0.6.23 → voxcity-0.6.25}/pyproject.toml +1 -1
  3. {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/exporter/cityles.py +95 -76
  4. {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/utils/visualization.py +317 -231
  5. {voxcity-0.6.23 → voxcity-0.6.25}/AUTHORS.rst +0 -0
  6. {voxcity-0.6.23 → voxcity-0.6.25}/LICENSE +0 -0
  7. {voxcity-0.6.23 → voxcity-0.6.25}/README.md +0 -0
  8. {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/__init__.py +0 -0
  9. {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/downloader/__init__.py +0 -0
  10. {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/downloader/citygml.py +0 -0
  11. {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/downloader/eubucco.py +0 -0
  12. {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/downloader/gee.py +0 -0
  13. {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/downloader/mbfp.py +0 -0
  14. {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/downloader/oemj.py +0 -0
  15. {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/downloader/osm.py +0 -0
  16. {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/downloader/overture.py +0 -0
  17. {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/downloader/utils.py +0 -0
  18. {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/exporter/__init__.py +0 -0
  19. {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/exporter/envimet.py +0 -0
  20. {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/exporter/magicavoxel.py +0 -0
  21. {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/exporter/netcdf.py +0 -0
  22. {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/exporter/obj.py +0 -0
  23. {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/generator.py +0 -0
  24. {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/geoprocessor/__init__.py +0 -0
  25. {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/geoprocessor/draw.py +0 -0
  26. {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/geoprocessor/grid.py +0 -0
  27. {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/geoprocessor/mesh.py +0 -0
  28. {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/geoprocessor/network.py +0 -0
  29. {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/geoprocessor/polygon.py +0 -0
  30. {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/geoprocessor/utils.py +0 -0
  31. {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/simulator/__init__.py +0 -0
  32. {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/simulator/solar.py +0 -0
  33. {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/simulator/utils.py +0 -0
  34. {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/simulator/view.py +0 -0
  35. {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/utils/__init__.py +0 -0
  36. {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/utils/lc.py +0 -0
  37. {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/utils/material.py +0 -0
  38. {voxcity-0.6.23 → voxcity-0.6.25}/src/voxcity/utils/weather.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voxcity
3
- Version: 0.6.23
3
+ Version: 0.6.25
4
4
  Summary: voxcity is an easy and one-stop tool to output 3d city models for microclimate simulation by integrating multiple geospatial open-data
5
5
  License: MIT
6
6
  License-File: AUTHORS.rst
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "voxcity"
3
- version = "0.6.23"
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: 1=Water, 2=Rice Paddy, 3=Crops, 4=Grassland, 5=Deciduous Broadleaf Forest,
40
- # 9=Bare Land, 10=Building, 16=Asphalt (road), etc.
41
-
42
- # OpenStreetMap / Standard
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': 9,
45
- 'Rangeland': 4,
46
- 'Shrub': 4,
47
- 'Moss and lichen': 4,
48
- 'Agriculture land': 3,
49
- 'Tree': 5,
50
- 'Wet land': 2,
51
- 'Mangroves': 5,
52
- 'Water': 1,
53
- 'Snow and ice': 9,
54
- 'Developed space': 10,
55
- 'Road': 16,
56
- 'Building': 10,
57
- 'No Data': 4
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': 10,
63
- 'Road': 16,
64
- 'Parking Lot': 16,
65
- 'Tree Canopy': 5,
66
- 'Grass/Shrub': 4,
67
- 'Agriculture': 3,
68
- 'Water': 1,
69
- 'Barren': 9,
70
- 'Unknown': 4,
71
- 'Sea': 1
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': 9,
77
- 'Rangeland': 4,
78
- 'Developed space': 10,
79
- 'Road': 16,
80
- 'Tree': 5,
81
- 'Water': 1,
82
- 'Agriculture land': 3,
83
- 'Building': 10
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': 5,
89
- 'Shrubland': 4,
90
- 'Grassland': 4,
91
- 'Cropland': 3,
92
- 'Built-up': 10,
93
- 'Barren / sparse vegetation': 9,
94
- 'Snow and ice': 9,
95
- 'Open water': 1,
96
- 'Herbaceous wetland': 2,
97
- 'Mangroves': 5,
98
- 'Moss and lichen': 9
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': 4,
104
- 'Water': 1,
105
- 'Trees': 5,
106
- 'Grass': 4,
107
- 'Flooded Vegetation': 2,
108
- 'Crops': 3,
109
- 'Scrub/Shrub': 4,
110
- 'Built Area': 10,
111
- 'Bare Ground': 9,
112
- 'Snow/Ice': 9,
113
- 'Clouds': 4
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': 1,
119
- 'Trees': 5,
120
- 'Grass': 4,
121
- 'Flooded Vegetation': 2,
122
- 'Crops': 3,
123
- 'Shrub and Scrub': 4,
124
- 'Built': 10,
125
- 'Bare': 9,
126
- 'Snow and Ice': 9
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 documentation
135
+ # Building material mapping based on corrected definitions (101-117)
130
136
  BUILDING_MATERIAL_MAPPING = {
131
- 'building': 110, # Building (general)
132
- 'concrete': 110, # Building (concrete)
133
- 'residential': 111, # Old wooden house
134
- 'wooden': 111, # Old wooden house
135
- 'commercial': 110, # Building (commercial)
136
- 'industrial': 110, # Building (industrial)
137
- 'default': 110 # Default to general building
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 + 100
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
- Interactive Plotly visualization of voxels with an overlaid building simulation mesh.
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
- classes = kwargs.get('classes')
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
- # Colors per face (constant on each triangle)
2527
- colors_rgba = np.zeros((len(F), 4), dtype=float)
2528
- colors_rgba[finite] = cmap(norm(face_vals[finite]))
2529
- nan_rgba = np.array(mcolors.to_rgba(nan_color))
2530
- colors_rgba[~finite] = nan_rgba
2531
- facecolor = [
2532
- f"rgb({int(255*c[0])},{int(255*c[1])},{int(255*c[2])})" for c in colors_rgba
2533
- ]
2534
-
2535
- # Lighting (disable shading by default for true color rendering)
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 where each occupied cell is rendered as a cube (six faces)
2587
- using Plotly Mesh3d. One Mesh3d trace per class.
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 are similar to visualize_voxcity_plotly, but rendering is via exact cubes.
2590
- voxel_color_map may be either a scheme name (str) understood by get_voxel_color_map,
2591
- or a dict mapping class_id -> [R, G, B] (0-255).
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
- raise ValueError("voxel_array must be a 3D numpy array (nx, ny, nz)")
2595
-
2596
- vox = voxel_array
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
- if downsample is not None:
2601
- stride = max(1, int(downsample))
2602
- else:
2603
- stride = 1
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
- max_dim = max(nx, ny, nz)
2606
- if max_dim > max_dimension:
2607
- stride = int(np.ceil(max_dim / max_dimension))
2608
-
2609
- if stride > 1:
2610
- vox = vox[::stride, ::stride, ::stride]
2611
-
2612
- nx, ny, nz = vox.shape
2613
-
2614
- # Coordinate of voxel centers in meters
2615
- dx = meshsize * stride
2616
- dy = meshsize * stride
2617
- dz = meshsize * stride
2618
- x = np.arange(nx, dtype=float) * dx
2619
- y = np.arange(ny, dtype=float) * dy
2620
- z = np.arange(nz, dtype=float) * dz
2621
-
2622
- # Choose classes
2623
- if classes is None:
2624
- classes = np.unique(vox[vox != 0]).tolist()
2625
- if not classes:
2626
- raise ValueError("No classes to visualize (voxel grid may be empty)")
2627
-
2628
- # Resolve color map: accept scheme name or explicit dict
2629
- if isinstance(voxel_color_map, dict):
2630
- vox_dict = voxel_color_map
2631
- else:
2632
- vox_dict = get_voxel_color_map(voxel_color_map)
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
- for cls in classes:
2734
- if not np.any(vox == cls):
2735
- continue
2736
- occ = (vox == cls)
2737
- posx, negx, posy, negy, posz, negz = exposed_face_masks(occ)
2738
- color_rgb = vox_dict.get(int(cls), [128,128,128])
2739
- add_faces(fig, posx, '+x', color_rgb)
2740
- add_faces(fig, negx, '-x', color_rgb)
2741
- add_faces(fig, posy, '+y', color_rgb)
2742
- add_faces(fig, negy, '-y', color_rgb)
2743
- add_faces(fig, posz, '+z', color_rgb)
2744
- add_faces(fig, negz, '-z', color_rgb)
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 (Voxel Cubes)",
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