voxcity 0.3.16__py3-none-any.whl → 0.3.18__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.

Potentially problematic release.


This version of voxcity might be problematic. Click here for more details.

voxcity/downloader/gee.py CHANGED
@@ -393,4 +393,35 @@ def save_geotiff_open_buildings_temporal(aoi, geotiff_path):
393
393
  scale=4,
394
394
  region=aoi,
395
395
  file_per_band=False
396
- )
396
+ )
397
+
398
+ def save_geotiff_england_dsm_minus_dtm(roi, geotiff_path, meshsize):
399
+ """Get the height difference between DSM and DTM from England 1m terrain data.
400
+
401
+ Args:
402
+ roi: Earth Engine geometry defining area of interest
403
+ geotiff_path: Output path for GeoTIFF file
404
+ meshsize: Size of each grid cell in meters
405
+
406
+ Returns:
407
+ ee.Image: Image representing DSM minus DTM (building/vegetation heights)
408
+ """
409
+ # Initialize Earth Engine
410
+ ee.Initialize()
411
+
412
+ # Add buffer around ROI to ensure smooth interpolation at edges
413
+ buffer_distance = 100
414
+ roi_buffered = roi.buffer(buffer_distance)
415
+
416
+ collection_name = 'UK/EA/ENGLAND_1M_TERRAIN/2022'
417
+ dtm = ee.Image(collection_name).select('dtm')
418
+ dsm = ee.Image(collection_name).select('dsm_first')
419
+
420
+ # Subtract DTM from DSM to get height difference
421
+ height_diff = dsm.subtract(dtm)
422
+
423
+ # Clip to buffered ROI
424
+ image = height_diff.clip(roi_buffered)
425
+
426
+ # Export as GeoTIFF using meshsize as scale
427
+ save_geotiff(image, geotiff_path, scale=meshsize, region=roi_buffered, crs='EPSG:4326')
voxcity/generator.py CHANGED
@@ -34,7 +34,8 @@ from .downloader.gee import (
34
34
  save_geotiff_esa_land_cover,
35
35
  save_geotiff_esri_landcover,
36
36
  save_geotiff_dynamic_world_v1,
37
- save_geotiff_open_buildings_temporal
37
+ save_geotiff_open_buildings_temporal,
38
+ save_geotiff_england_dsm_minus_dtm
38
39
  )
39
40
  from .geoprocessor.grid import (
40
41
  group_and_label_cells,
@@ -200,6 +201,13 @@ def get_building_height_grid(rectangle_vertices, meshsize, source, output_dir, *
200
201
  geotiff_path_comp = os.path.join(output_dir, "building_height.tif")
201
202
  save_geotiff_open_buildings_temporal(roi, geotiff_path_comp)
202
203
  building_height_grid, building_min_height_grid, building_id_grid, filtered_buildings = create_building_height_grid_from_gdf_polygon(gdf, meshsize, rectangle_vertices, geotiff_path_comp=geotiff_path_comp, complement_height=building_complement_height)
204
+ elif building_complementary_source == "England 1m DSM - DTM":
205
+ # Special case: use temporal height data as complement
206
+ roi = get_roi(rectangle_vertices)
207
+ os.makedirs(output_dir, exist_ok=True)
208
+ geotiff_path_comp = os.path.join(output_dir, "building_height.tif")
209
+ save_geotiff_england_dsm_minus_dtm(roi, geotiff_path_comp, meshsize)
210
+ building_height_grid, building_min_height_grid, building_id_grid, filtered_buildings = create_building_height_grid_from_gdf_polygon(gdf, meshsize, rectangle_vertices, geotiff_path_comp=geotiff_path_comp, complement_height=building_complement_height)
203
211
  else:
204
212
  # Get complementary data from other sources
205
213
  if building_complementary_source == 'Microsoft Building Footprints':
@@ -3,3 +3,4 @@ from .grid import *
3
3
  from .utils import *
4
4
  from .network import *
5
5
  from .polygon import *
6
+ from .mesh import *
@@ -0,0 +1,269 @@
1
+ import numpy as np
2
+ import trimesh
3
+ import matplotlib.colors as mcolors
4
+ import matplotlib.cm as cm
5
+
6
+ def create_voxel_mesh(voxel_array, class_id, meshsize=1.0):
7
+ """
8
+ Create a mesh from voxels preserving sharp edges, scaled by meshsize.
9
+
10
+ Parameters
11
+ ----------
12
+ voxel_array : np.ndarray (3D)
13
+ The voxel array of shape (X, Y, Z).
14
+ class_id : int
15
+ The ID of the class to extract.
16
+ meshsize : float
17
+ The real-world size of each voxel in meters, for x, y, and z.
18
+
19
+ Returns
20
+ -------
21
+ mesh : trimesh.Trimesh or None
22
+ The resulting mesh for the given class_id (or None if no voxels).
23
+ """
24
+ # Find voxels of the current class
25
+ voxel_coords = np.argwhere(voxel_array == class_id)
26
+
27
+ if len(voxel_coords) == 0:
28
+ return None
29
+
30
+ # Define the 6 faces of a unit cube (local coordinates 0..1)
31
+ unit_faces = np.array([
32
+ # Front
33
+ [[0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1]],
34
+ # Back
35
+ [[0, 0, 0], [0, 1, 0], [1, 1, 0], [1, 0, 0]],
36
+ # Right
37
+ [[1, 0, 0], [1, 1, 0], [1, 1, 1], [1, 0, 1]],
38
+ # Left
39
+ [[0, 0, 0], [0, 0, 1], [0, 1, 1], [0, 1, 0]],
40
+ # Top
41
+ [[0, 1, 0], [0, 1, 1], [1, 1, 1], [1, 1, 0]],
42
+ # Bottom
43
+ [[0, 0, 0], [1, 0, 0], [1, 0, 1], [0, 0, 1]]
44
+ ])
45
+
46
+ # Define face normals
47
+ face_normals = np.array([
48
+ [0, 0, 1], # Front
49
+ [0, 0, -1], # Back
50
+ [1, 0, 0], # Right
51
+ [-1, 0, 0], # Left
52
+ [0, 1, 0], # Top
53
+ [0, -1, 0] # Bottom
54
+ ])
55
+
56
+ vertices = []
57
+ faces = []
58
+ face_normals_list = []
59
+
60
+ for x, y, z in voxel_coords:
61
+ # Check each face of the current voxel
62
+ adjacent_coords = [
63
+ (x, y, z+1), # Front
64
+ (x, y, z-1), # Back
65
+ (x+1, y, z), # Right
66
+ (x-1, y, z), # Left
67
+ (x, y+1, z), # Top
68
+ (x, y-1, z) # Bottom
69
+ ]
70
+
71
+ # Only create faces where there's a transition between this class and "not this class"
72
+ for face_idx, adj_coord in enumerate(adjacent_coords):
73
+ try:
74
+ # If adj_coord is outside array bounds, it's a boundary => face is visible
75
+ if adj_coord[0] < 0 or adj_coord[1] < 0 or adj_coord[2] < 0:
76
+ is_boundary = True
77
+ else:
78
+ adj_value = voxel_array[adj_coord]
79
+ is_boundary = (adj_value == 0 or adj_value != class_id)
80
+ except IndexError:
81
+ # Out of range => boundary
82
+ is_boundary = True
83
+
84
+ if is_boundary:
85
+ # Local face in (0..1) for x,y,z, then shift by voxel coords
86
+ face_verts = (unit_faces[face_idx] + np.array([x, y, z])) * meshsize
87
+ current_vert_count = len(vertices)
88
+
89
+ vertices.extend(face_verts)
90
+ # Convert quad to two triangles
91
+ faces.extend([
92
+ [current_vert_count, current_vert_count + 1, current_vert_count + 2],
93
+ [current_vert_count, current_vert_count + 2, current_vert_count + 3]
94
+ ])
95
+ # Add face normals for both triangles
96
+ face_normals_list.extend([face_normals[face_idx], face_normals[face_idx]])
97
+
98
+ if not vertices:
99
+ return None
100
+
101
+ vertices = np.array(vertices)
102
+ faces = np.array(faces)
103
+ face_normals_list = np.array(face_normals_list)
104
+
105
+ # Create mesh
106
+ mesh = trimesh.Trimesh(
107
+ vertices=vertices,
108
+ faces=faces,
109
+ face_normals=face_normals_list
110
+ )
111
+
112
+ # Merge vertices that are at the same position
113
+ mesh.merge_vertices()
114
+
115
+ return mesh
116
+
117
+ def create_sim_surface_mesh(sim_grid, dem_grid,
118
+ meshsize=1.0, z_offset=1.5,
119
+ cmap_name='viridis',
120
+ vmin=None, vmax=None):
121
+ """
122
+ Create a planar surface mesh from sim_grid located at dem_grid + z_offset.
123
+ Skips any cells in sim_grid that are NaN, and flips both sim_grid and dem_grid
124
+ (up-down) to match voxel_array orientation. Applies meshsize scaling in x,y.
125
+
126
+ Parameters
127
+ ----------
128
+ sim_grid : 2D np.ndarray
129
+ 2D array of simulation values (e.g., Green View Index).
130
+ dem_grid : 2D np.ndarray
131
+ 2D array of ground elevations (same shape as sim_grid).
132
+ meshsize : float
133
+ Size of each cell in meters (same in x and y).
134
+ z_offset : float
135
+ Additional offset added to dem_grid for placing the mesh.
136
+ cmap_name : str
137
+ Matplotlib colormap name. Default is 'viridis'.
138
+ vmin : float or None
139
+ Minimum value for color mapping. If None, use min of sim_grid (non-NaN).
140
+ vmax : float or None
141
+ Maximum value for color mapping. If None, use max of sim_grid (non-NaN).
142
+
143
+ Returns
144
+ -------
145
+ trimesh.Trimesh or None
146
+ A single mesh containing one square face per non-NaN cell.
147
+ Returns None if there are no valid cells.
148
+ """
149
+ # Flip arrays vertically
150
+ sim_grid_flipped = np.flipud(sim_grid)
151
+ dem_grid_flipped = np.flipud(dem_grid)
152
+
153
+ # Identify valid (non-NaN) values
154
+ valid_mask = ~np.isnan(sim_grid_flipped)
155
+ valid_values = sim_grid_flipped[valid_mask]
156
+ if valid_values.size == 0:
157
+ return None
158
+
159
+ # If vmin/vmax not provided, use actual min/max of the valid sim data
160
+ if vmin is None:
161
+ vmin = np.nanmin(valid_values)
162
+ if vmax is None:
163
+ vmax = np.nanmax(valid_values)
164
+
165
+ # Prepare the colormap
166
+ norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
167
+ scalar_map = cm.ScalarMappable(norm=norm, cmap=cmap_name)
168
+
169
+ vertices = []
170
+ faces = []
171
+ face_colors = []
172
+
173
+ vert_index = 0
174
+ nrows, ncols = sim_grid_flipped.shape
175
+
176
+ # Build a quad (two triangles) for each valid cell
177
+ for x in range(nrows):
178
+ for y in range(ncols):
179
+ val = sim_grid_flipped[x, y]
180
+ if np.isnan(val):
181
+ continue
182
+
183
+ z_base = dem_grid_flipped[x, y] + z_offset
184
+
185
+ # 4 corners in (x,y)*meshsize
186
+ v0 = [ x * meshsize, y * meshsize, z_base ]
187
+ v1 = [(x + 1) * meshsize, y * meshsize, z_base ]
188
+ v2 = [(x + 1) * meshsize, (y + 1) * meshsize, z_base ]
189
+ v3 = [ x * meshsize, (y + 1) * meshsize, z_base ]
190
+
191
+ vertices.extend([v0, v1, v2, v3])
192
+ faces.extend([
193
+ [vert_index, vert_index + 1, vert_index + 2],
194
+ [vert_index, vert_index + 2, vert_index + 3]
195
+ ])
196
+
197
+ # Get color from colormap
198
+ color_rgba = np.array(scalar_map.to_rgba(val)) # shape (4,)
199
+
200
+ # Each cell has 2 faces => add the color twice
201
+ face_colors.append(color_rgba)
202
+ face_colors.append(color_rgba)
203
+
204
+ vert_index += 4
205
+
206
+ if len(vertices) == 0:
207
+ return None
208
+
209
+ vertices = np.array(vertices, dtype=float)
210
+ faces = np.array(faces, dtype=int)
211
+ face_colors = np.array(face_colors, dtype=float)
212
+
213
+ mesh = trimesh.Trimesh(
214
+ vertices=vertices,
215
+ faces=faces,
216
+ face_colors=face_colors,
217
+ process=False # skip auto merge if you want to preserve quads
218
+ )
219
+
220
+ return mesh
221
+
222
+ def create_city_meshes(voxel_array, vox_dict, meshsize=1.0):
223
+ """
224
+ Create meshes from voxel data with sharp edges preserved.
225
+ Applies meshsize for voxel scaling.
226
+ """
227
+ meshes = {}
228
+
229
+ # Convert RGB colors to hex for material properties
230
+ color_dict = {k: mcolors.rgb2hex([v[0]/255, v[1]/255, v[2]/255])
231
+ for k, v in vox_dict.items() if k != 0} # Exclude air
232
+
233
+ # Create vertices and faces for each object class
234
+ for class_id in np.unique(voxel_array):
235
+ if class_id == 0: # Skip air
236
+ continue
237
+
238
+ try:
239
+ mesh = create_voxel_mesh(voxel_array, class_id, meshsize=meshsize)
240
+
241
+ if mesh is None:
242
+ continue
243
+
244
+ # Convert hex color to RGBA
245
+ rgb_color = np.array(mcolors.hex2color(color_dict[class_id]))
246
+ rgba_color = np.concatenate([rgb_color, [1.0]])
247
+
248
+ # Assign color to all faces
249
+ mesh.visual.face_colors = np.tile(rgba_color, (len(mesh.faces), 1))
250
+
251
+ meshes[class_id] = mesh
252
+
253
+ except ValueError as e:
254
+ print(f"Skipping class {class_id}: {e}")
255
+
256
+ return meshes
257
+
258
+ def export_meshes(meshes, output_directory, base_filename):
259
+ """
260
+ Export meshes to OBJ (with MTL) and STL formats.
261
+ """
262
+ # Export combined mesh as OBJ with materials
263
+ combined_mesh = trimesh.util.concatenate(list(meshes.values()))
264
+ combined_mesh.export(f"{output_directory}/{base_filename}.obj")
265
+
266
+ # Export individual meshes as STL
267
+ for class_id, mesh in meshes.items():
268
+ # Convert class_id to a string for filename
269
+ mesh.export(f"{output_directory}/{base_filename}_{class_id}.stl")
@@ -625,8 +625,8 @@ def extract_building_heights_from_geotiff(geotiff_path, gdf):
625
625
  # Print statistics about height updates
626
626
  if count_0 > 0:
627
627
  print(f"{count_0} of the total {len(gdf)} building footprint from OSM did not have height data.")
628
- print(f"For {count_1} of these building footprints without height, values from Open Building 2.5D Temporal were assigned.")
629
- print(f"For {count_2} of these building footprints without height, no data exist in Open Building 2.5D Temporal. Height values of 10m were set instead")
628
+ print(f"For {count_1} of these building footprints without height, values from complementary data were assigned.")
629
+ print(f"For {count_2} of these building footprints without height, no data exist in complementary data. Height values of 10m were set instead")
630
630
 
631
631
  return gdf
632
632
 
@@ -16,6 +16,10 @@ import seaborn as sns
16
16
  import random
17
17
  import folium
18
18
  import math
19
+ import trimesh
20
+ import pyvista as pv
21
+ from IPython.display import display
22
+ import os
19
23
 
20
24
  from .lc import get_land_cover_classes
21
25
  # from ..geo.geojson import filter_buildings
@@ -25,7 +29,6 @@ from ..geoprocessor.grid import (
25
29
  create_cell_polygon,
26
30
  grid_to_geodataframe
27
31
  )
28
-
29
32
  from ..geoprocessor.utils import (
30
33
  initialize_geod,
31
34
  calculate_distance,
@@ -33,6 +36,12 @@ from ..geoprocessor.utils import (
33
36
  setup_transformer,
34
37
  transform_coords,
35
38
  )
39
+ from ..geoprocessor.mesh import (
40
+ create_voxel_mesh,
41
+ create_sim_surface_mesh,
42
+ create_city_meshes,
43
+ export_meshes
44
+ )
36
45
  from .material import get_material_dict
37
46
 
38
47
  def get_default_voxel_color_map():
@@ -911,4 +920,282 @@ def visualize_point_grid_on_basemap(point_gdf, value_name='value', **kwargs):
911
920
 
912
921
  # Adjust layout to prevent colorbar cutoff
913
922
  plt.tight_layout()
914
- plt.show()
923
+ plt.show()
924
+
925
+ def create_multi_view_scene(meshes, output_directory="output"):
926
+ """
927
+ Create multiple views of the scene from different angles.
928
+ """
929
+ # Get the first mesh to compute bounding box
930
+ first_mesh = next(iter(meshes.values()))
931
+ vertices = first_mesh.vertices
932
+ bbox = np.array([
933
+ [vertices[:, 0].min(), vertices[:, 1].min(), vertices[:, 2].min()],
934
+ [vertices[:, 0].max(), vertices[:, 1].max(), vertices[:, 2].max()]
935
+ ])
936
+
937
+ # Compute the center and diagonal of the bounding box
938
+ center = (bbox[1] + bbox[0]) / 2
939
+ diagonal = np.linalg.norm(bbox[1] - bbox[0])
940
+
941
+ # Use the diagonal to set the camera distance
942
+ distance = diagonal * 2
943
+
944
+ # Define the isometric viewing angles
945
+ iso_angles = {
946
+ 'iso_front_right': (1, 1, 0.7),
947
+ 'iso_front_left': (-1, 1, 0.7),
948
+ 'iso_back_right': (1, -1, 0.7),
949
+ 'iso_back_left': (-1, -1, 0.7)
950
+ }
951
+
952
+ # Compute camera positions for isometric views
953
+ camera_positions = {}
954
+ for name, direction in iso_angles.items():
955
+ direction = np.array(direction)
956
+ direction = direction / np.linalg.norm(direction)
957
+ camera_pos = center + direction * distance
958
+ camera_positions[name] = [camera_pos, center, (0, 0, 1)]
959
+
960
+ # Add orthographic views
961
+ ortho_views = {
962
+ 'xy_top': [center + np.array([0, 0, distance]), center, (0, 1, 0)],
963
+ 'yz_right': [center + np.array([distance, 0, 0]), center, (0, 0, 1)],
964
+ 'xz_front': [center + np.array([0, distance, 0]), center, (0, 0, 1)],
965
+ 'yz_left': [center + np.array([-distance, 0, 0]), center, (0, 0, 1)],
966
+ 'xz_back': [center + np.array([0, -distance, 0]), center, (0, 0, 1)]
967
+ }
968
+ camera_positions.update(ortho_views)
969
+
970
+ images = []
971
+ for view_name, camera_pos in camera_positions.items():
972
+ # Create new plotter for each view
973
+ plotter = pv.Plotter(notebook=True, off_screen=True)
974
+
975
+ # Add each mesh to the scene
976
+ for class_id, mesh in meshes.items():
977
+ vertices = mesh.vertices
978
+ faces = np.hstack([[3, *face] for face in mesh.faces])
979
+ pv_mesh = pv.PolyData(vertices, faces)
980
+
981
+ if hasattr(mesh.visual, 'face_colors'):
982
+ colors = mesh.visual.face_colors
983
+ if colors.max() > 1:
984
+ colors = colors / 255.0
985
+ pv_mesh.cell_data['colors'] = colors
986
+
987
+ plotter.add_mesh(pv_mesh,
988
+ rgb=True,
989
+ scalars='colors' if hasattr(mesh.visual, 'face_colors') else None)
990
+
991
+ # Set camera position for this view
992
+ plotter.camera_position = camera_pos
993
+
994
+ # Save screenshot
995
+ filename = f'{output_directory}/city_view_{view_name}.png'
996
+ plotter.screenshot(filename)
997
+ images.append((view_name, filename))
998
+ plotter.close()
999
+
1000
+ return images
1001
+
1002
+ def visualize_voxcity_multi_view(voxel_array, **kwargs):
1003
+ """
1004
+ Create multiple views of the voxel city data.
1005
+ """
1006
+
1007
+ os.system('Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &')
1008
+ os.environ['DISPLAY'] = ':99'
1009
+
1010
+ # Configure PyVista settings
1011
+ pv.set_plot_theme('document')
1012
+ pv.global_theme.background = 'white'
1013
+ pv.global_theme.window_size = [1024, 768]
1014
+ pv.global_theme.jupyter_backend = 'static'
1015
+
1016
+ # view_kwargs = {
1017
+ # "view_point_height": 1.5, # To set height of view point in meters. Default: 1.5 m.
1018
+ # "dem_grid": dem_grid,
1019
+ # "colormap": 'viridis', # Choose a colormap. Default: 'viridis'.
1020
+ # "obj_export": True, # Set "True" if you want to export the result in an OBJ file.
1021
+ # "output_directory": f'output/{key}/obj', # To set directory path for output files. Default: False.
1022
+ # "output_file_name": 'gvi', # To set file name excluding extension. Default: 'view_index'.
1023
+ # "num_colors": 10, # Number of discrete colors
1024
+ # "alpha": 1.0, # Set transparency (0.0 to 1.0)
1025
+ # "vmin": 0.0, # Minimum value for colormap normalization
1026
+ # "vmax": 1.0 # Maximum value for colormap normalization
1027
+ # }
1028
+ # Parse kwargs
1029
+ vox_dict = kwargs.get("vox_dict", get_default_voxel_color_map())
1030
+ output_directory = kwargs.get("output_directory", 'output')
1031
+ base_filename = kwargs.get("output_file_name", None)
1032
+ sim_grid = kwargs.get("sim_grid", None)
1033
+ dem_grid = kwargs.get("dem_grid", None)
1034
+ meshsize = kwargs.get("meshsize", 1.0) / 5
1035
+ z_offset = kwargs.get("z_offset", 1.5)
1036
+ cmap_name = kwargs.get("colormap", "viridis")
1037
+ vmin = kwargs.get("vmin", 0)
1038
+ vmax = kwargs.get("vmax", 1)
1039
+
1040
+ # Create meshes
1041
+ print("Creating voxel meshes...")
1042
+ meshes = create_city_meshes(voxel_array, vox_dict, meshsize=meshsize)
1043
+
1044
+ # Create sim_grid surface mesh if provided
1045
+ if sim_grid is not None and dem_grid is not None:
1046
+ print("Creating sim_grid surface mesh...")
1047
+ sim_mesh = create_sim_surface_mesh(
1048
+ sim_grid, dem_grid,
1049
+ meshsize=meshsize,
1050
+ z_offset=z_offset,
1051
+ cmap_name=cmap_name,
1052
+ vmin=vmin,
1053
+ vmax=vmax
1054
+ )
1055
+ if sim_mesh is not None:
1056
+ meshes["sim_surface"] = sim_mesh
1057
+
1058
+ # Export if filename provided
1059
+ if base_filename is not None:
1060
+ print(f"Exporting files to '{base_filename}.*' ...")# Create output directory if it doesn't exist
1061
+ os.makedirs(output_directory, exist_ok=True)
1062
+ export_meshes(meshes, output_directory, base_filename)
1063
+
1064
+ # Create and save multiple views
1065
+ print("Creating multiple views...")
1066
+ # Create output directory if it doesn't exist
1067
+ os.makedirs(output_directory, exist_ok=True)
1068
+ image_files = create_multi_view_scene(meshes, output_directory=output_directory)
1069
+
1070
+ # Display each view separately
1071
+ for view_name, img_file in image_files:
1072
+ plt.figure(figsize=(12, 8))
1073
+ img = plt.imread(img_file)
1074
+ plt.imshow(img)
1075
+ plt.title(view_name.replace('_', ' ').title(), pad=20)
1076
+ plt.axis('off')
1077
+ plt.show()
1078
+ plt.close()
1079
+
1080
+ # def create_interactive_scene(meshes):
1081
+ # scene = trimesh.Scene()
1082
+ # scene.ambient_light = np.array([0.1, 0.1, 0.1, 1.0])
1083
+ # scene.directional_light = np.array([0.1, 0.1, 0.1, 1.0])
1084
+
1085
+ # for class_id, mesh in meshes.items():
1086
+
1087
+ # # If this is our sim_surface, do NOT override the per-face colors.
1088
+ # if class_id == "sim_surface":
1089
+ # # Just add the mesh as-is, retaining mesh.visual.face_colors
1090
+ # scene.add_geometry(mesh, node_name=f"class_{class_id}")
1091
+ # else:
1092
+ # # Existing code for single-color classes
1093
+ # material = trimesh.visual.material.PBRMaterial(
1094
+ # baseColorFactor=mesh.visual.face_colors[0],
1095
+ # metallicFactor=0.2,
1096
+ # roughnessFactor=0.8,
1097
+ # emissiveFactor=np.array([0.1, 0.1, 0.1]),
1098
+ # alphaMode='OPAQUE'
1099
+ # )
1100
+ # mesh.visual = trimesh.visual.TextureVisuals(
1101
+ # material=material,
1102
+ # uv=None
1103
+ # )
1104
+ # scene.add_geometry(mesh, node_name=f"class_{class_id}")
1105
+
1106
+ # # (Optional) add checkboxes if in Jupyter:
1107
+ # try:
1108
+ # import ipywidgets as widgets
1109
+ # from IPython.display import display
1110
+
1111
+ # def update_visibility(cid, visible):
1112
+ # scene.graph.nodes[f"class_{cid}"].visible = visible
1113
+
1114
+ # checkboxes = []
1115
+ # for cid in meshes.keys():
1116
+ # checkbox = widgets.Checkbox(value=True, description=f'Class {cid}')
1117
+ # checkbox.observe(
1118
+ # lambda change, _cid=cid: update_visibility(_cid, change['new']),
1119
+ # names='value'
1120
+ # )
1121
+ # checkboxes.append(checkbox)
1122
+ # display(widgets.VBox(checkboxes))
1123
+ # except ImportError:
1124
+ # pass # Not running in Jupyter
1125
+
1126
+ # return scene
1127
+
1128
+ # def visualize_voxcity_interactive(voxel_array, **kwargs):
1129
+ # """
1130
+ # Process voxel city data:
1131
+ # - create voxel meshes,
1132
+ # - optionally create a sim_grid surface mesh,
1133
+ # - optionally export,
1134
+ # - return a trimesh Scene for visualization.
1135
+
1136
+ # Optional arguments via **kwargs:
1137
+ # --------------------------------
1138
+ # base_filename : str, default "city_model"
1139
+ # Base name for exported files (OBJ, STL).
1140
+ # sim_grid : 2D np.ndarray or None, default None
1141
+ # Simulation array for creating a 2D surface mesh.
1142
+ # dem_grid : 2D np.ndarray or None, default None
1143
+ # DEM array for the surface mesh. Must match sim_grid shape.
1144
+ # meshsize : float, default 1.0
1145
+ # Real-world size (in meters) per voxel/cell in x,y,z.
1146
+ # z_offset : float, default 1.5
1147
+ # Offset added to dem_grid when placing sim_grid surface.
1148
+ # cmap_name : str, default 'viridis'
1149
+ # Matplotlib colormap name for sim_grid.
1150
+ # vmin : float or None, default 0
1151
+ # Minimum value for color mapping. If None, auto from data.
1152
+ # vmax : float or None, default 1
1153
+ # Maximum value for color mapping. If None, auto from data.
1154
+ # """
1155
+
1156
+ # # 1. Parse **kwargs
1157
+ # vox_dict = kwargs.get("vox_dict", get_default_voxel_color_map())
1158
+ # base_filename = kwargs.get("base_filename", None)
1159
+ # sim_grid = kwargs.get("sim_grid", None)
1160
+ # dem_grid = kwargs.get("dem_grid", None)
1161
+ # meshsize = kwargs.get("meshsize", 1.0) / 5
1162
+ # z_offset = kwargs.get("z_offset", 1.5)
1163
+ # cmap_name = kwargs.get("cmap_name", "viridis")
1164
+ # vmin = kwargs.get("vmin", 0)
1165
+ # vmax = kwargs.get("vmax", 1)
1166
+
1167
+ # # 2. Create voxel-based meshes (same logic as before)
1168
+ # print("Creating voxel meshes...")
1169
+ # meshes = create_city_meshes(voxel_array, vox_dict, meshsize=meshsize)
1170
+
1171
+ # # 3. Optionally create the sim_grid surface mesh
1172
+ # if sim_grid is not None and dem_grid is not None:
1173
+ # print("Creating sim_grid surface mesh...")
1174
+ # sim_mesh = create_sim_surface_mesh(
1175
+ # sim_grid,
1176
+ # dem_grid,
1177
+ # meshsize=meshsize,
1178
+ # z_offset=z_offset,
1179
+ # cmap_name=cmap_name,
1180
+ # vmin=vmin,
1181
+ # vmax=vmax
1182
+ # )
1183
+ # if sim_mesh is not None:
1184
+ # meshes["sim_surface"] = sim_mesh
1185
+ # else:
1186
+ # print("No valid cells in sim_grid (all NaN?). Skipping surface mesh.")
1187
+
1188
+ # # 4. Optionally export
1189
+ # if base_filename is not None:
1190
+ # print(f"Exporting files to '{base_filename}.*' ...")
1191
+ # export_meshes(meshes, base_filename)
1192
+ # else:
1193
+ # print("Skipping export step.")
1194
+
1195
+ # # 5. Create interactive visualization (voxel + optional sim_surface)
1196
+ # print("Creating interactive visualization...")
1197
+ # scene = create_interactive_scene(meshes)
1198
+
1199
+ # scene.show()
1200
+
1201
+ # return scene
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: voxcity
3
- Version: 0.3.16
3
+ Version: 0.3.18
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
  Author-email: Kunihiko Fujiwara <kunihiko@nus.edu.sg>
6
6
  Maintainer-email: Kunihiko Fujiwara <kunihiko@nus.edu.sg>
@@ -50,6 +50,9 @@ Requires-Dist: timezonefinder
50
50
  Requires-Dist: astral
51
51
  Requires-Dist: osmnx
52
52
  Requires-Dist: joblib
53
+ Requires-Dist: trimesh
54
+ Requires-Dist: pyvista
55
+ Requires-Dist: IPython
53
56
  Provides-Extra: dev
54
57
  Requires-Dist: coverage; extra == "dev"
55
58
  Requires-Dist: mypy; extra == "dev"
@@ -1,8 +1,8 @@
1
1
  voxcity/__init__.py,sha256=el9v3gfybHOF_GUYPeSOqN0-vCrTW0eU1mcvi0sEfeU,252
2
- voxcity/generator.py,sha256=3cr520doEAdoXasvdywiK0FOvIfh1YdALv4tlhFvbAk,33654
2
+ voxcity/generator.py,sha256=2wmfiCXJREOC0zxq6GB8ECxtGjk42UYzGvf5vGZuEOg,34359
3
3
  voxcity/downloader/__init__.py,sha256=OgGcGxOXF4tjcEL6DhOnt13DYPTvOigUelp5xIpTqM0,171
4
4
  voxcity/downloader/eubucco.py,sha256=XCkkdEPNuWdrnuxzL80Ext37WsgiCiZGueb-aQV5rvI,14476
5
- voxcity/downloader/gee.py,sha256=j7jmzp44T3M6j_4DwhU9Y8Y6gqbZo1zFIlduQPc0jvk,14339
5
+ voxcity/downloader/gee.py,sha256=C5Y3UFAK4-AcBQwUd5FQNDwwxT_iqfElAHWiVJTkF9k,15471
6
6
  voxcity/downloader/mbfp.py,sha256=5fXq9S7qNVSLDdVtj67Da1pBAJP6kL4P8qLZTOmWqdw,3895
7
7
  voxcity/downloader/oemj.py,sha256=YlCuWBQfi40gfmwQcGDeHiPOs4Pk_jLZq65d5R3IGMU,7886
8
8
  voxcity/downloader/omt.py,sha256=ByFvoQDnBOJF4qdVYNkDjn7cMvEhWwtD0mIV_T-zMEs,9017
@@ -13,11 +13,12 @@ voxcity/exporter/__init_.py,sha256=cVyNyE6axEpSd3CT5hGuMOAlOyU1p8lVP4jkF1-0Ad8,9
13
13
  voxcity/exporter/envimet.py,sha256=m-y2IYw-yp45AT2wN9UIlxvMjvDvupTKzyfRJl057fE,24300
14
14
  voxcity/exporter/magicavoxel.py,sha256=Fsv7yGRXeKmp82xcG3rOb0t_HtoqltNq2tHl08xVlqY,7500
15
15
  voxcity/exporter/obj.py,sha256=oW-kPoZj53nfmO9tXP3Wvizq6Kkjh-QQR8UBexRuMiI,21609
16
- voxcity/geoprocessor/__init_.py,sha256=FFJFf6idmAtmNkwfKPt3ERGSIzjb8tt35D1n9QQbCA8,112
16
+ voxcity/geoprocessor/__init_.py,sha256=JzPVhhttxBWvaZ0IGX2w7OWL5bCo_TIvpHefWeNXruA,133
17
17
  voxcity/geoprocessor/draw.py,sha256=8Em2NvazFpYfFJUqG9LofNXaxdghKLL_rNuztmPwn8Q,13911
18
18
  voxcity/geoprocessor/grid.py,sha256=aLb_iInDrhh5cacQOOtHPZ9IWFTWsQtF6hEgsA6TB2Y,43761
19
+ voxcity/geoprocessor/mesh.py,sha256=arXGQM5NUNazwU021vQwYhFdFrR_2Udl88uCQDZSh2s,9147
19
20
  voxcity/geoprocessor/network.py,sha256=opb_kpUCAxDd1qtrWPStqR5reYZtVe96XxazNSen7Lk,18851
20
- voxcity/geoprocessor/polygon.py,sha256=8fU2Ayu2Y_G1z7Mbj8KoSKVurdPuAVbASjGMVS36ftM,32249
21
+ voxcity/geoprocessor/polygon.py,sha256=EeAlawsI0AQHctxG_1rS8y3sLFijwmWtdIFtLIXHUCg,32231
21
22
  voxcity/geoprocessor/utils.py,sha256=1BRHp-DDeOA8HG8jplY7Eo75G3oXkVGL6DGONL4BA8A,19815
22
23
  voxcity/simulator/__init_.py,sha256=APdkcdaovj0v_RPOaA4SBvFUKT2RM7Hxuuz3Sux4gCo,65
23
24
  voxcity/simulator/solar.py,sha256=FOcHoUm4miJNyeCcGs2oL93Vu38Affyywt29dJcmIT4,31974
@@ -26,11 +27,11 @@ voxcity/simulator/view.py,sha256=zNbfTLQ2Jo0V5-rFA3-xamRjOuw3H3MBrLKpQp8x3hY,367
26
27
  voxcity/utils/__init_.py,sha256=nLYrj2huBbDBNMqfchCwexGP8Tlt9O_XluVDG7MoFkw,98
27
28
  voxcity/utils/lc.py,sha256=RwPd-VY3POV3gTrBhM7TubgGb9MCd3nVah_G8iUEF7k,11562
28
29
  voxcity/utils/material.py,sha256=Vt3IID5Ft54HNJcEC4zi31BCPqi_687X3CSp7rXaRVY,5907
29
- voxcity/utils/visualization.py,sha256=2bv3y-1zkUX0cm_YbMHwe_Vt9J2R3QhouaVAGNifQXg,36805
30
+ voxcity/utils/visualization.py,sha256=pMQRqmNiAfibRdfRLHy3wkZ7Qbs__cUEfvGF7va0Pak,47980
30
31
  voxcity/utils/weather.py,sha256=P6s1y_EstBL1OGP_MR_6u3vr-t6Uawg8uDckJnoI7FI,21482
31
- voxcity-0.3.16.dist-info/AUTHORS.rst,sha256=m82vkI5QokEGdcHof2OxK39lf81w1P58kG9ZNNAKS9U,175
32
- voxcity-0.3.16.dist-info/LICENSE,sha256=-hGliOFiwUrUSoZiB5WF90xXGqinKyqiDI2t6hrnam8,1087
33
- voxcity-0.3.16.dist-info/METADATA,sha256=d67Drf-UV5hy7212m7y3rPmqC-cZ8LwloyVYgwq3EsA,25114
34
- voxcity-0.3.16.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
35
- voxcity-0.3.16.dist-info/top_level.txt,sha256=00b2U-LKfDllt6RL1R33MXie5MvxzUFye0NGD96t_8I,8
36
- voxcity-0.3.16.dist-info/RECORD,,
32
+ voxcity-0.3.18.dist-info/AUTHORS.rst,sha256=m82vkI5QokEGdcHof2OxK39lf81w1P58kG9ZNNAKS9U,175
33
+ voxcity-0.3.18.dist-info/LICENSE,sha256=-hGliOFiwUrUSoZiB5WF90xXGqinKyqiDI2t6hrnam8,1087
34
+ voxcity-0.3.18.dist-info/METADATA,sha256=DBPn1oJmBwUP3TZzUUN4H6PhPk56ESJUOC_or4GTMXA,25186
35
+ voxcity-0.3.18.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
36
+ voxcity-0.3.18.dist-info/top_level.txt,sha256=00b2U-LKfDllt6RL1R33MXie5MvxzUFye0NGD96t_8I,8
37
+ voxcity-0.3.18.dist-info/RECORD,,