voxcity 0.3.3__py3-none-any.whl → 0.3.5__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/download/eubucco.py +9 -17
- voxcity/download/gee.py +4 -3
- voxcity/download/mbfp.py +7 -7
- voxcity/download/oemj.py +22 -22
- voxcity/download/omt.py +10 -10
- voxcity/download/osm.py +23 -21
- voxcity/download/overture.py +7 -15
- voxcity/file/envimet.py +4 -4
- voxcity/file/geojson.py +25 -39
- voxcity/geo/__init_.py +2 -1
- voxcity/geo/draw.py +41 -45
- voxcity/geo/grid.py +68 -145
- voxcity/geo/network.py +193 -0
- voxcity/geo/utils.py +79 -66
- voxcity/sim/solar.py +5 -5
- voxcity/sim/view.py +5 -5
- voxcity/utils/__init_.py +2 -1
- voxcity/utils/material.py +139 -0
- voxcity/utils/visualization.py +128 -354
- voxcity/utils/weather.py +7 -7
- voxcity/voxcity.py +56 -8
- {voxcity-0.3.3.dist-info → voxcity-0.3.5.dist-info}/METADATA +6 -5
- voxcity-0.3.5.dist-info/RECORD +36 -0
- {voxcity-0.3.3.dist-info → voxcity-0.3.5.dist-info}/WHEEL +1 -1
- voxcity-0.3.3.dist-info/RECORD +0 -34
- {voxcity-0.3.3.dist-info → voxcity-0.3.5.dist-info}/AUTHORS.rst +0 -0
- {voxcity-0.3.3.dist-info → voxcity-0.3.5.dist-info}/LICENSE +0 -0
- {voxcity-0.3.3.dist-info → voxcity-0.3.5.dist-info}/top_level.txt +0 -0
voxcity/utils/visualization.py
CHANGED
|
@@ -3,6 +3,7 @@ import matplotlib.pyplot as plt
|
|
|
3
3
|
from mpl_toolkits.mplot3d import Axes3D
|
|
4
4
|
from tqdm import tqdm
|
|
5
5
|
import matplotlib.colors as mcolors
|
|
6
|
+
from matplotlib.colors import ListedColormap, BoundaryNorm
|
|
6
7
|
import contextily as ctx
|
|
7
8
|
from shapely.geometry import Polygon
|
|
8
9
|
import plotly.graph_objects as go
|
|
@@ -21,7 +22,8 @@ from .lc import get_land_cover_classes
|
|
|
21
22
|
from ..geo.grid import (
|
|
22
23
|
calculate_grid_size,
|
|
23
24
|
create_coordinate_mesh,
|
|
24
|
-
create_cell_polygon
|
|
25
|
+
create_cell_polygon,
|
|
26
|
+
grid_to_geodataframe
|
|
25
27
|
)
|
|
26
28
|
|
|
27
29
|
from ..geo.utils import (
|
|
@@ -31,18 +33,7 @@ from ..geo.utils import (
|
|
|
31
33
|
setup_transformer,
|
|
32
34
|
transform_coords,
|
|
33
35
|
)
|
|
34
|
-
|
|
35
|
-
def get_material_dict():
|
|
36
|
-
return {
|
|
37
|
-
"unknown": -3,
|
|
38
|
-
"brick": -11,
|
|
39
|
-
"wood": -12,
|
|
40
|
-
"concrete": -13,
|
|
41
|
-
"metal": -14,
|
|
42
|
-
"stone": -15,
|
|
43
|
-
"glass": -16,
|
|
44
|
-
"plaster": -17,
|
|
45
|
-
}
|
|
36
|
+
from .material import get_material_dict
|
|
46
37
|
|
|
47
38
|
def get_default_voxel_color_map():
|
|
48
39
|
return {
|
|
@@ -251,119 +242,6 @@ def visualize_3d_voxel_plotly(voxel_grid, color_map = get_default_voxel_color_ma
|
|
|
251
242
|
print("Visualization complete. Displaying plot...")
|
|
252
243
|
fig.show()
|
|
253
244
|
|
|
254
|
-
# def plot_grid(grid, origin, adjusted_meshsize, u_vec, v_vec, transformer, vertices, data_type, vmin=None, vmax=None, alpha=0.5, buf=0.2, edge=True, **kwargs):
|
|
255
|
-
# fig, ax = plt.subplots(figsize=(12, 12))
|
|
256
|
-
|
|
257
|
-
# if data_type == 'land_cover':
|
|
258
|
-
# land_cover_classes = kwargs.get('land_cover_classes')
|
|
259
|
-
# colors = [mcolors.to_rgb(f'#{r:02x}{g:02x}{b:02x}') for r, g, b in land_cover_classes.keys()]
|
|
260
|
-
# cmap = mcolors.ListedColormap(colors)
|
|
261
|
-
# norm = mcolors.BoundaryNorm(range(len(land_cover_classes)+1), cmap.N)
|
|
262
|
-
# title = 'Grid Cells with Dominant Land Cover Classes'
|
|
263
|
-
# label = 'Land Cover Class'
|
|
264
|
-
# tick_labels = list(land_cover_classes.values())
|
|
265
|
-
# elif data_type == 'building_height':
|
|
266
|
-
# # Create a masked array to handle special values
|
|
267
|
-
# masked_grid = np.ma.masked_array(grid, mask=(np.isnan(grid) | (grid == 0)))
|
|
268
|
-
|
|
269
|
-
# # Set up colormap and normalization for positive values
|
|
270
|
-
# cmap = plt.cm.viridis
|
|
271
|
-
# if vmin is None:
|
|
272
|
-
# vmin = np.nanmin(masked_grid[masked_grid > 0])
|
|
273
|
-
# if vmax is None:
|
|
274
|
-
# vmax = np.nanmax(masked_grid)
|
|
275
|
-
# norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
|
|
276
|
-
|
|
277
|
-
# title = 'Grid Cells with Building Heights'
|
|
278
|
-
# label = 'Building Height (m)'
|
|
279
|
-
# tick_labels = None
|
|
280
|
-
# elif data_type == 'dem':
|
|
281
|
-
# cmap = plt.cm.terrain
|
|
282
|
-
# if vmin is None:
|
|
283
|
-
# vmin = np.nanmin(grid)
|
|
284
|
-
# if vmax is None:
|
|
285
|
-
# vmax = np.nanmax(grid)
|
|
286
|
-
# norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
|
|
287
|
-
# title = 'DEM Grid Overlaid on Map'
|
|
288
|
-
# label = 'Elevation (m)'
|
|
289
|
-
# tick_labels = None
|
|
290
|
-
# elif data_type == 'canopy_height':
|
|
291
|
-
# cmap = plt.cm.Greens
|
|
292
|
-
# if vmin is None:
|
|
293
|
-
# vmin = np.nanmin(grid)
|
|
294
|
-
# if vmax is None:
|
|
295
|
-
# vmax = np.nanmax(grid)
|
|
296
|
-
# norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
|
|
297
|
-
# title = 'Canopy Height Grid Overlaid on Map'
|
|
298
|
-
# label = 'Canopy Height (m)'
|
|
299
|
-
# tick_labels = None
|
|
300
|
-
# else:
|
|
301
|
-
# raise ValueError("Invalid data_type. Choose 'land_cover', 'building_height', 'canopy_height', or 'dem'.")
|
|
302
|
-
|
|
303
|
-
# # Ensure grid is in the correct orientation
|
|
304
|
-
# grid = grid.T
|
|
305
|
-
|
|
306
|
-
# for i in range(grid.shape[0]):
|
|
307
|
-
# for j in range(grid.shape[1]):
|
|
308
|
-
# cell = create_cell_polygon(origin, j, i, adjusted_meshsize, u_vec, v_vec) # Note the swap of i and j
|
|
309
|
-
# x, y = cell.exterior.xy
|
|
310
|
-
# x, y = zip(*[transformer.transform(lon, lat) for lat, lon in zip(x, y)])
|
|
311
|
-
|
|
312
|
-
# value = grid[i, j]
|
|
313
|
-
|
|
314
|
-
# if data_type == 'building_height':
|
|
315
|
-
# if np.isnan(value):
|
|
316
|
-
# # White fill for NaN values
|
|
317
|
-
# ax.fill(x, y, alpha=alpha, fc='white', ec='black' if edge else None, linewidth=0.1)
|
|
318
|
-
# elif value == 0:
|
|
319
|
-
# # No fill for zero values, only edges if enabled
|
|
320
|
-
# if edge:
|
|
321
|
-
# ax.plot(x, y, color='black', linewidth=0.1)
|
|
322
|
-
# elif value > 0:
|
|
323
|
-
# # Viridis colormap for positive values
|
|
324
|
-
# color = cmap(norm(value))
|
|
325
|
-
# ax.fill(x, y, alpha=alpha, fc=color, ec='black' if edge else None, linewidth=0.1)
|
|
326
|
-
# else:
|
|
327
|
-
# color = cmap(norm(value))
|
|
328
|
-
# if edge:
|
|
329
|
-
# ax.fill(x, y, alpha=alpha, fc=color, ec='black', linewidth=0.1)
|
|
330
|
-
# else:
|
|
331
|
-
# ax.fill(x, y, alpha=alpha, fc=color, ec=None)
|
|
332
|
-
|
|
333
|
-
# crs_epsg_3857 = CRS.from_epsg(3857)
|
|
334
|
-
# ctx.add_basemap(ax, crs=crs_epsg_3857, source=ctx.providers.CartoDB.DarkMatter)
|
|
335
|
-
|
|
336
|
-
# if data_type == 'building_height':
|
|
337
|
-
# buildings = kwargs.get('buildings', [])
|
|
338
|
-
# for building in buildings:
|
|
339
|
-
# polygon = Polygon(building['geometry']['coordinates'][0])
|
|
340
|
-
# x, y = polygon.exterior.xy
|
|
341
|
-
# x, y = zip(*[transformer.transform(lon, lat) for lat, lon in zip(x, y)])
|
|
342
|
-
# ax.plot(x, y, color='red', linewidth=1)
|
|
343
|
-
|
|
344
|
-
# # Safe calculation of plot limits
|
|
345
|
-
# all_coords = np.array(vertices)
|
|
346
|
-
# x, y = zip(*[transformer.transform(lon, lat) for lat, lon in all_coords])
|
|
347
|
-
|
|
348
|
-
# # Calculate limits safely
|
|
349
|
-
# x_min, x_max = min(x), max(x)
|
|
350
|
-
# y_min, y_max = min(y), max(y)
|
|
351
|
-
|
|
352
|
-
# if x_min != x_max and y_min != y_max and buf != 0:
|
|
353
|
-
# dist_x = x_max - x_min
|
|
354
|
-
# dist_y = y_max - y_min
|
|
355
|
-
# # Set limits with buffer
|
|
356
|
-
# ax.set_xlim(x_min - buf * dist_x, x_max + buf * dist_x)
|
|
357
|
-
# ax.set_ylim(y_min - buf * dist_y, y_max + buf * dist_y)
|
|
358
|
-
# else:
|
|
359
|
-
# # If coordinates are the same or buffer is 0, set limits without buffer
|
|
360
|
-
# ax.set_xlim(x_min, x_max)
|
|
361
|
-
# ax.set_ylim(y_min, y_max)
|
|
362
|
-
|
|
363
|
-
# plt.axis('off')
|
|
364
|
-
# plt.tight_layout()
|
|
365
|
-
# plt.show()
|
|
366
|
-
|
|
367
245
|
def plot_grid(grid, origin, adjusted_meshsize, u_vec, v_vec, transformer, vertices, data_type, vmin=None, vmax=None, color_map=None, alpha=0.5, buf=0.2, edge=True, basemap='CartoDB light', **kwargs):
|
|
368
246
|
fig, ax = plt.subplots(figsize=(12, 12))
|
|
369
247
|
|
|
@@ -631,31 +509,6 @@ def visualize_numerical_grid_on_map(canopy_height_grid, rectangle_vertices, mesh
|
|
|
631
509
|
# Plot the results
|
|
632
510
|
plot_grid(canopy_height_grid, origin, adjusted_meshsize, u_vec, v_vec, transformer,
|
|
633
511
|
rectangle_vertices, type, vmin=vmin, vmax=vmax, color_map=color_map, alpha=alpha, buf=buf, edge=edge, basemap=basemap)
|
|
634
|
-
|
|
635
|
-
# def visualize_land_cover_grid(grid, mesh_size, color_map, land_cover_classes):
|
|
636
|
-
# all_classes = list(land_cover_classes.values())# + ['No Data']
|
|
637
|
-
# # for cls in all_classes:
|
|
638
|
-
# # if cls not in color_map:
|
|
639
|
-
# # color_map[cls] = [0.5, 0.5, 0.5]
|
|
640
|
-
|
|
641
|
-
# sorted_classes = sorted(all_classes)
|
|
642
|
-
# colors = [color_map[cls] for cls in sorted_classes]
|
|
643
|
-
# cmap = mcolors.ListedColormap(colors)
|
|
644
|
-
|
|
645
|
-
# bounds = np.arange(len(sorted_classes) + 1)
|
|
646
|
-
# norm = mcolors.BoundaryNorm(bounds, cmap.N)
|
|
647
|
-
|
|
648
|
-
# class_to_num = {cls: i for i, cls in enumerate(sorted_classes)}
|
|
649
|
-
# numeric_grid = np.vectorize(class_to_num.get)(grid)
|
|
650
|
-
|
|
651
|
-
# plt.figure(figsize=(10, 10))
|
|
652
|
-
# im = plt.imshow(numeric_grid, cmap=cmap, norm=norm, interpolation='nearest')
|
|
653
|
-
# cbar = plt.colorbar(im, ticks=bounds[:-1] + 0.5)
|
|
654
|
-
# cbar.set_ticklabels(sorted_classes)
|
|
655
|
-
# plt.title(f'Land Use/Land Cover Grid (Mesh Size: {mesh_size}m)')
|
|
656
|
-
# plt.xlabel('Grid Cells (X)')
|
|
657
|
-
# plt.ylabel('Grid Cells (Y)')
|
|
658
|
-
# plt.show()
|
|
659
512
|
|
|
660
513
|
def visualize_land_cover_grid(grid, mesh_size, color_map, land_cover_classes):
|
|
661
514
|
all_classes = list(land_cover_classes.values())
|
|
@@ -688,204 +541,6 @@ def visualize_numerical_grid(grid, mesh_size, title, cmap='viridis', label='Valu
|
|
|
688
541
|
plt.ylabel('Grid Cells (Y)')
|
|
689
542
|
plt.show()
|
|
690
543
|
|
|
691
|
-
def get_modulo_numbers(window_ratio):
|
|
692
|
-
"""
|
|
693
|
-
Determines the appropriate modulo numbers for x, y, z based on window_ratio.
|
|
694
|
-
|
|
695
|
-
Parameters:
|
|
696
|
-
window_ratio: float between 0 and 1.0
|
|
697
|
-
|
|
698
|
-
Returns:
|
|
699
|
-
tuple (x_mod, y_mod, z_mod): modulo numbers for each dimension
|
|
700
|
-
"""
|
|
701
|
-
if window_ratio <= 0.125 + 0.0625: # around 0.125
|
|
702
|
-
return (2, 2, 2)
|
|
703
|
-
elif window_ratio <= 0.25 + 0.125: # around 0.25
|
|
704
|
-
combinations = [(2, 2, 1), (2, 1, 2), (1, 2, 2)]
|
|
705
|
-
return combinations[hash(str(window_ratio)) % len(combinations)]
|
|
706
|
-
elif window_ratio <= 0.5 + 0.125: # around 0.5
|
|
707
|
-
combinations = [(2, 1, 1), (1, 2, 1), (1, 1, 2)]
|
|
708
|
-
return combinations[hash(str(window_ratio)) % len(combinations)]
|
|
709
|
-
elif window_ratio <= 0.75 + 0.125: # around 0.75
|
|
710
|
-
combinations = [(2, 1, 1), (1, 2, 1), (1, 1, 2)]
|
|
711
|
-
return combinations[hash(str(window_ratio)) % len(combinations)]
|
|
712
|
-
else: # above 0.875
|
|
713
|
-
return (1, 1, 1)
|
|
714
|
-
|
|
715
|
-
def set_building_material_by_id(voxelcity_grid, building_id_grid_ori, ids, mark, window_ratio=0.125, glass_id=-10):
|
|
716
|
-
"""
|
|
717
|
-
Marks cells in voxelcity_grid based on building IDs and window ratio.
|
|
718
|
-
Never sets glass_id to cells with maximum z index.
|
|
719
|
-
|
|
720
|
-
Parameters:
|
|
721
|
-
voxelcity_grid: 3D numpy array
|
|
722
|
-
building_id_grid_ori: 2D numpy array containing building IDs
|
|
723
|
-
ids: list/array of building IDs to check
|
|
724
|
-
mark: value to set for marked cells
|
|
725
|
-
window_ratio: float between 0 and 1.0, determines window density:
|
|
726
|
-
~0.125: sparse windows (2,2,2)
|
|
727
|
-
~0.25: medium-sparse windows (2,2,1), (2,1,2), or (1,2,2)
|
|
728
|
-
~0.5: medium windows (2,1,1), (1,2,1), or (1,1,2)
|
|
729
|
-
~0.75: dense windows (2,1,1), (1,2,1), or (1,1,2)
|
|
730
|
-
>0.875: maximum density (1,1,1)
|
|
731
|
-
glass_id: value to set for glass cells (default: -10)
|
|
732
|
-
|
|
733
|
-
Returns:
|
|
734
|
-
Modified voxelcity_grid
|
|
735
|
-
"""
|
|
736
|
-
building_id_grid = np.flipud(building_id_grid_ori.copy())
|
|
737
|
-
|
|
738
|
-
# Get modulo numbers based on window_ratio
|
|
739
|
-
x_mod, y_mod, z_mod = get_modulo_numbers(window_ratio)
|
|
740
|
-
|
|
741
|
-
# Get positions where building IDs match
|
|
742
|
-
building_positions = np.where(np.isin(building_id_grid, ids))
|
|
743
|
-
|
|
744
|
-
# Loop through each position that matches building IDs
|
|
745
|
-
for i in range(len(building_positions[0])):
|
|
746
|
-
x, y = building_positions[0][i], building_positions[1][i]
|
|
747
|
-
z_mask = voxelcity_grid[x, y, :] == -3
|
|
748
|
-
voxelcity_grid[x, y, z_mask] = mark
|
|
749
|
-
|
|
750
|
-
# Check if x and y meet the modulo conditions
|
|
751
|
-
if x % x_mod == 0 and y % y_mod == 0:
|
|
752
|
-
z_mask = voxelcity_grid[x, y, :] == mark
|
|
753
|
-
if np.any(z_mask):
|
|
754
|
-
# Find the maximum z index where z_mask is True
|
|
755
|
-
z_indices = np.where(z_mask)[0]
|
|
756
|
-
max_z_index = np.max(z_indices)
|
|
757
|
-
|
|
758
|
-
# Create base mask excluding maximum z index
|
|
759
|
-
base_mask = z_mask.copy()
|
|
760
|
-
base_mask[max_z_index] = False
|
|
761
|
-
|
|
762
|
-
# Create pattern mask based on z modulo
|
|
763
|
-
pattern_mask = np.zeros_like(z_mask)
|
|
764
|
-
valid_z_indices = z_indices[z_indices != max_z_index] # Exclude max_z_index
|
|
765
|
-
if len(valid_z_indices) > 0:
|
|
766
|
-
pattern_mask[valid_z_indices[valid_z_indices % z_mod == 0]] = True
|
|
767
|
-
|
|
768
|
-
# For window_ratio around 0.75, add additional pattern
|
|
769
|
-
if 0.625 < window_ratio <= 0.875 and len(valid_z_indices) > 0:
|
|
770
|
-
additional_pattern = np.zeros_like(z_mask)
|
|
771
|
-
additional_pattern[valid_z_indices[valid_z_indices % (z_mod + 1) == 0]] = True
|
|
772
|
-
pattern_mask = np.logical_or(pattern_mask, additional_pattern)
|
|
773
|
-
|
|
774
|
-
# Final mask combines base_mask and pattern_mask
|
|
775
|
-
final_glass_mask = np.logical_and(base_mask, pattern_mask)
|
|
776
|
-
|
|
777
|
-
# Set glass_id for all positions in the final mask
|
|
778
|
-
voxelcity_grid[x, y, final_glass_mask] = glass_id
|
|
779
|
-
|
|
780
|
-
return voxelcity_grid
|
|
781
|
-
|
|
782
|
-
def set_building_material_by_gdf(voxelcity_grid_ori, building_id_grid, gdf_buildings, material_id_dict=None):
|
|
783
|
-
voxelcity_grid = voxelcity_grid_ori.copy()
|
|
784
|
-
if material_id_dict == None:
|
|
785
|
-
material_id_dict = get_material_dict()
|
|
786
|
-
|
|
787
|
-
for index, row in gdf_buildings.iterrows():
|
|
788
|
-
# Access properties
|
|
789
|
-
osmid = row['building_id']
|
|
790
|
-
surface_material = row['surface_material']
|
|
791
|
-
window_ratio = row['window_ratio']
|
|
792
|
-
if surface_material is None:
|
|
793
|
-
surface_material = 'unknown'
|
|
794
|
-
set_building_material_by_id(voxelcity_grid, building_id_grid, osmid, material_id_dict[surface_material], window_ratio=window_ratio, glass_id=material_id_dict['glass'])
|
|
795
|
-
|
|
796
|
-
return voxelcity_grid
|
|
797
|
-
|
|
798
|
-
def get_modulo_numbers(window_ratio):
|
|
799
|
-
"""
|
|
800
|
-
Determines the appropriate modulo numbers for x, y, z based on window_ratio.
|
|
801
|
-
|
|
802
|
-
Parameters:
|
|
803
|
-
window_ratio: float between 0 and 1.0
|
|
804
|
-
|
|
805
|
-
Returns:
|
|
806
|
-
tuple (x_mod, y_mod, z_mod): modulo numbers for each dimension
|
|
807
|
-
"""
|
|
808
|
-
if window_ratio <= 0.125 + 0.0625: # around 0.125
|
|
809
|
-
return (2, 2, 2)
|
|
810
|
-
elif window_ratio <= 0.25 + 0.125: # around 0.25
|
|
811
|
-
combinations = [(2, 2, 1), (2, 1, 2), (1, 2, 2)]
|
|
812
|
-
return combinations[hash(str(window_ratio)) % len(combinations)]
|
|
813
|
-
elif window_ratio <= 0.5 + 0.125: # around 0.5
|
|
814
|
-
combinations = [(2, 1, 1), (1, 2, 1), (1, 1, 2)]
|
|
815
|
-
return combinations[hash(str(window_ratio)) % len(combinations)]
|
|
816
|
-
elif window_ratio <= 0.75 + 0.125: # around 0.75
|
|
817
|
-
combinations = [(2, 1, 1), (1, 2, 1), (1, 1, 2)]
|
|
818
|
-
return combinations[hash(str(window_ratio)) % len(combinations)]
|
|
819
|
-
else: # above 0.875
|
|
820
|
-
return (1, 1, 1)
|
|
821
|
-
|
|
822
|
-
def set_building_material_by_id(voxelcity_grid, building_id_grid_ori, ids, mark, window_ratio=0.125, glass_id=-16):
|
|
823
|
-
"""
|
|
824
|
-
Marks cells in voxelcity_grid based on building IDs and window ratio.
|
|
825
|
-
Never sets glass_id to cells with maximum z index.
|
|
826
|
-
|
|
827
|
-
Parameters:
|
|
828
|
-
voxelcity_grid: 3D numpy array
|
|
829
|
-
building_id_grid_ori: 2D numpy array containing building IDs
|
|
830
|
-
ids: list/array of building IDs to check
|
|
831
|
-
mark: value to set for marked cells
|
|
832
|
-
window_ratio: float between 0 and 1.0, determines window density:
|
|
833
|
-
~0.125: sparse windows (2,2,2)
|
|
834
|
-
~0.25: medium-sparse windows (2,2,1), (2,1,2), or (1,2,2)
|
|
835
|
-
~0.5: medium windows (2,1,1), (1,2,1), or (1,1,2)
|
|
836
|
-
~0.75: dense windows (2,1,1), (1,2,1), or (1,1,2)
|
|
837
|
-
>0.875: maximum density (1,1,1)
|
|
838
|
-
glass_id: value to set for glass cells (default: -10)
|
|
839
|
-
|
|
840
|
-
Returns:
|
|
841
|
-
Modified voxelcity_grid
|
|
842
|
-
"""
|
|
843
|
-
building_id_grid = np.flipud(building_id_grid_ori.copy())
|
|
844
|
-
|
|
845
|
-
# Get modulo numbers based on window_ratio
|
|
846
|
-
x_mod, y_mod, z_mod = get_modulo_numbers(window_ratio)
|
|
847
|
-
|
|
848
|
-
# Get positions where building IDs match
|
|
849
|
-
building_positions = np.where(np.isin(building_id_grid, ids))
|
|
850
|
-
|
|
851
|
-
# Loop through each position that matches building IDs
|
|
852
|
-
for i in range(len(building_positions[0])):
|
|
853
|
-
x, y = building_positions[0][i], building_positions[1][i]
|
|
854
|
-
z_mask = voxelcity_grid[x, y, :] == -3
|
|
855
|
-
voxelcity_grid[x, y, z_mask] = mark
|
|
856
|
-
|
|
857
|
-
# Check if x and y meet the modulo conditions
|
|
858
|
-
if x % x_mod == 0 and y % y_mod == 0:
|
|
859
|
-
z_mask = voxelcity_grid[x, y, :] == mark
|
|
860
|
-
if np.any(z_mask):
|
|
861
|
-
# Find the maximum z index where z_mask is True
|
|
862
|
-
z_indices = np.where(z_mask)[0]
|
|
863
|
-
max_z_index = np.max(z_indices)
|
|
864
|
-
|
|
865
|
-
# Create base mask excluding maximum z index
|
|
866
|
-
base_mask = z_mask.copy()
|
|
867
|
-
base_mask[max_z_index] = False
|
|
868
|
-
|
|
869
|
-
# Create pattern mask based on z modulo
|
|
870
|
-
pattern_mask = np.zeros_like(z_mask)
|
|
871
|
-
valid_z_indices = z_indices[z_indices != max_z_index] # Exclude max_z_index
|
|
872
|
-
if len(valid_z_indices) > 0:
|
|
873
|
-
pattern_mask[valid_z_indices[valid_z_indices % z_mod == 0]] = True
|
|
874
|
-
|
|
875
|
-
# For window_ratio around 0.75, add additional pattern
|
|
876
|
-
if 0.625 < window_ratio <= 0.875 and len(valid_z_indices) > 0:
|
|
877
|
-
additional_pattern = np.zeros_like(z_mask)
|
|
878
|
-
additional_pattern[valid_z_indices[valid_z_indices % (z_mod + 1) == 0]] = True
|
|
879
|
-
pattern_mask = np.logical_or(pattern_mask, additional_pattern)
|
|
880
|
-
|
|
881
|
-
# Final mask combines base_mask and pattern_mask
|
|
882
|
-
final_glass_mask = np.logical_and(base_mask, pattern_mask)
|
|
883
|
-
|
|
884
|
-
# Set glass_id for all positions in the final mask
|
|
885
|
-
voxelcity_grid[x, y, final_glass_mask] = glass_id
|
|
886
|
-
|
|
887
|
-
return voxelcity_grid
|
|
888
|
-
|
|
889
544
|
def convert_coordinates(coords):
|
|
890
545
|
return coords
|
|
891
546
|
|
|
@@ -904,10 +559,6 @@ def calculate_center(features):
|
|
|
904
559
|
lons.append(lon)
|
|
905
560
|
return sum(lats) / len(lats), sum(lons) / len(lons)
|
|
906
561
|
|
|
907
|
-
# def format_building_id(id_num):
|
|
908
|
-
# # Format ID to ensure it's at least 9 digits with leading zeros
|
|
909
|
-
# return f"{id_num:09d}"
|
|
910
|
-
|
|
911
562
|
def create_circle_polygon(center_lat, center_lon, radius_meters):
|
|
912
563
|
"""Create a circular polygon with given center and radius"""
|
|
913
564
|
# Convert radius from meters to degrees (approximate)
|
|
@@ -1002,4 +653,127 @@ def display_builing_ids_on_map(building_geojson, rectangle_vertices):
|
|
|
1002
653
|
).add_to(m)
|
|
1003
654
|
|
|
1004
655
|
# Save the map
|
|
1005
|
-
return m
|
|
656
|
+
return m
|
|
657
|
+
|
|
658
|
+
def visualize_landcover_grid_on_basemap(landcover_grid, rectangle_vertices, meshsize, source='Standard', alpha=0.6, figsize=(12, 8),
|
|
659
|
+
basemap='CartoDB light', show_edge=False, edge_color='black', edge_width=0.5):
|
|
660
|
+
"""Visualizes a land cover grid GeoDataFrame using predefined color schemes.
|
|
661
|
+
|
|
662
|
+
Args:
|
|
663
|
+
gdf: GeoDataFrame containing grid cells with 'geometry' and 'value' columns
|
|
664
|
+
source: Source of land cover classification (e.g., 'Standard', 'Urbanwatch', etc.)
|
|
665
|
+
title: Title for the plot (default: None)
|
|
666
|
+
alpha: Transparency of the grid overlay (default: 0.6)
|
|
667
|
+
figsize: Figure size in inches (default: (12, 8))
|
|
668
|
+
basemap: Basemap style (default: 'CartoDB light')
|
|
669
|
+
show_edge: Whether to show cell edges (default: True)
|
|
670
|
+
edge_color: Color of cell edges (default: 'black')
|
|
671
|
+
edge_width: Width of cell edges (default: 0.5)
|
|
672
|
+
"""
|
|
673
|
+
# Get land cover classes and colors
|
|
674
|
+
land_cover_classes = get_land_cover_classes(source)
|
|
675
|
+
|
|
676
|
+
gdf = grid_to_geodataframe(landcover_grid, rectangle_vertices, meshsize)
|
|
677
|
+
|
|
678
|
+
# Convert RGB tuples to normalized RGB values
|
|
679
|
+
colors = [(r/255, g/255, b/255) for (r,g,b) in land_cover_classes.keys()]
|
|
680
|
+
|
|
681
|
+
# Create custom colormap
|
|
682
|
+
cmap = ListedColormap(colors)
|
|
683
|
+
|
|
684
|
+
# Create bounds for discrete colorbar
|
|
685
|
+
bounds = np.arange(len(colors) + 1)
|
|
686
|
+
norm = BoundaryNorm(bounds, cmap.N)
|
|
687
|
+
|
|
688
|
+
# Convert to Web Mercator
|
|
689
|
+
gdf_web = gdf.to_crs(epsg=3857)
|
|
690
|
+
|
|
691
|
+
# Create figure and axis
|
|
692
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
693
|
+
|
|
694
|
+
# Plot the GeoDataFrame
|
|
695
|
+
gdf_web.plot(column='value',
|
|
696
|
+
ax=ax,
|
|
697
|
+
alpha=alpha,
|
|
698
|
+
cmap=cmap,
|
|
699
|
+
norm=norm,
|
|
700
|
+
legend=True,
|
|
701
|
+
legend_kwds={
|
|
702
|
+
'label': 'Land Cover Class',
|
|
703
|
+
'ticks': bounds[:-1] + 0.5,
|
|
704
|
+
'boundaries': bounds,
|
|
705
|
+
'format': lambda x, p: list(land_cover_classes.values())[int(x)]
|
|
706
|
+
},
|
|
707
|
+
edgecolor=edge_color if show_edge else 'none',
|
|
708
|
+
linewidth=edge_width if show_edge else 0)
|
|
709
|
+
|
|
710
|
+
# Add basemap
|
|
711
|
+
basemaps = {
|
|
712
|
+
'CartoDB dark': ctx.providers.CartoDB.DarkMatter,
|
|
713
|
+
'CartoDB light': ctx.providers.CartoDB.Positron,
|
|
714
|
+
'CartoDB voyager': ctx.providers.CartoDB.Voyager,
|
|
715
|
+
'CartoDB light no labels': ctx.providers.CartoDB.PositronNoLabels,
|
|
716
|
+
'CartoDB dark no labels': ctx.providers.CartoDB.DarkMatterNoLabels,
|
|
717
|
+
}
|
|
718
|
+
ctx.add_basemap(ax, source=basemaps[basemap])
|
|
719
|
+
|
|
720
|
+
# Set title and remove axes
|
|
721
|
+
ax.set_axis_off()
|
|
722
|
+
|
|
723
|
+
plt.tight_layout()
|
|
724
|
+
plt.show()
|
|
725
|
+
|
|
726
|
+
def visualize_numerical_grid_on_basemap(grid, rectangle_vertices, meshsize, value_name="value", cmap='viridis', vmin=None, vmax=None,
|
|
727
|
+
alpha=0.6, figsize=(12, 8), basemap='CartoDB light',
|
|
728
|
+
show_edge=False, edge_color='black', edge_width=0.5):
|
|
729
|
+
"""Visualizes a numerical grid GeoDataFrame (e.g., heights) on a basemap.
|
|
730
|
+
|
|
731
|
+
Args:
|
|
732
|
+
gdf: GeoDataFrame containing grid cells with 'geometry' and 'value' columns
|
|
733
|
+
title: Title for the plot (default: None)
|
|
734
|
+
cmap: Colormap to use (default: 'viridis')
|
|
735
|
+
vmin: Minimum value for colormap scaling (default: None)
|
|
736
|
+
vmax: Maximum value for colormap scaling (default: None)
|
|
737
|
+
alpha: Transparency of the grid overlay (default: 0.6)
|
|
738
|
+
figsize: Figure size in inches (default: (12, 8))
|
|
739
|
+
basemap: Basemap style (default: 'CartoDB light')
|
|
740
|
+
show_edge: Whether to show cell edges (default: True)
|
|
741
|
+
edge_color: Color of cell edges (default: 'black')
|
|
742
|
+
edge_width: Width of cell edges (default: 0.5)
|
|
743
|
+
"""
|
|
744
|
+
|
|
745
|
+
gdf = grid_to_geodataframe(grid, rectangle_vertices, meshsize)
|
|
746
|
+
|
|
747
|
+
# Convert to Web Mercator
|
|
748
|
+
gdf_web = gdf.to_crs(epsg=3857)
|
|
749
|
+
|
|
750
|
+
# Create figure and axis
|
|
751
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
752
|
+
|
|
753
|
+
# Plot the GeoDataFrame
|
|
754
|
+
gdf_web.plot(column='value',
|
|
755
|
+
ax=ax,
|
|
756
|
+
alpha=alpha,
|
|
757
|
+
cmap=cmap,
|
|
758
|
+
vmin=vmin,
|
|
759
|
+
vmax=vmax,
|
|
760
|
+
legend=True,
|
|
761
|
+
legend_kwds={'label': value_name},
|
|
762
|
+
edgecolor=edge_color if show_edge else 'none',
|
|
763
|
+
linewidth=edge_width if show_edge else 0)
|
|
764
|
+
|
|
765
|
+
# Add basemap
|
|
766
|
+
basemaps = {
|
|
767
|
+
'CartoDB dark': ctx.providers.CartoDB.DarkMatter,
|
|
768
|
+
'CartoDB light': ctx.providers.CartoDB.Positron,
|
|
769
|
+
'CartoDB voyager': ctx.providers.CartoDB.Voyager,
|
|
770
|
+
'CartoDB light no labels': ctx.providers.CartoDB.PositronNoLabels,
|
|
771
|
+
'CartoDB dark no labels': ctx.providers.CartoDB.DarkMatterNoLabels,
|
|
772
|
+
}
|
|
773
|
+
ctx.add_basemap(ax, source=basemaps[basemap])
|
|
774
|
+
|
|
775
|
+
# Set title and remove axes
|
|
776
|
+
ax.set_axis_off()
|
|
777
|
+
|
|
778
|
+
plt.tight_layout()
|
|
779
|
+
plt.show()
|
voxcity/utils/weather.py
CHANGED
|
@@ -163,14 +163,14 @@ def process_epw(epw_path: Union[str, Path]) -> Tuple[pd.DataFrame, Dict]:
|
|
|
163
163
|
|
|
164
164
|
return df, headers
|
|
165
165
|
|
|
166
|
-
def get_nearest_epw_from_climate_onebuilding(
|
|
166
|
+
def get_nearest_epw_from_climate_onebuilding(longitude: float, latitude: float, output_dir: str = "./", max_distance: Optional[float] = None,
|
|
167
167
|
extract_zip: bool = True, load_data: bool = True) -> Tuple[Optional[str], Optional[pd.DataFrame], Optional[Dict]]:
|
|
168
168
|
"""
|
|
169
169
|
Download and process EPW weather file from Climate.OneBuilding.Org based on coordinates.
|
|
170
170
|
|
|
171
171
|
Args:
|
|
172
|
-
latitude (float): Latitude of the location
|
|
173
172
|
longitude (float): Longitude of the location
|
|
173
|
+
latitude (float): Latitude of the location
|
|
174
174
|
output_dir (str): Directory to save the EPW file (defaults to current directory)
|
|
175
175
|
max_distance (float, optional): Maximum distance in kilometers to search for stations
|
|
176
176
|
extract_zip (bool): Whether to extract the ZIP file (default True)
|
|
@@ -222,7 +222,7 @@ def get_nearest_epw_from_climate_onebuilding(latitude: float, longitude: float,
|
|
|
222
222
|
content = re.sub(r'[^\x09\x0A\x0D\x20-\x7E\x85\xA0-\xFF]', '', content)
|
|
223
223
|
return content
|
|
224
224
|
|
|
225
|
-
def haversine_distance(
|
|
225
|
+
def haversine_distance(lon1: float, lat1: float, lon2: float, lat2: float) -> float:
|
|
226
226
|
"""Calculate the great circle distance between two points on Earth."""
|
|
227
227
|
R = 6371 # Earth's radius in kilometers
|
|
228
228
|
|
|
@@ -281,8 +281,8 @@ def get_nearest_epw_from_climate_onebuilding(latitude: float, longitude: float,
|
|
|
281
281
|
|
|
282
282
|
metadata = {
|
|
283
283
|
'url': url,
|
|
284
|
-
'latitude': lat,
|
|
285
284
|
'longitude': lon,
|
|
285
|
+
'latitude': lat,
|
|
286
286
|
'elevation': int(extract_value(r'Elevation <b>(-?\d+)</b>', '0')),
|
|
287
287
|
'name': extract_value(r'<b>(.*?)</b>'),
|
|
288
288
|
'wmo': extract_value(r'WMO <b>(\d+)</b>'),
|
|
@@ -370,7 +370,7 @@ def get_nearest_epw_from_climate_onebuilding(latitude: float, longitude: float,
|
|
|
370
370
|
|
|
371
371
|
# Calculate distances and find nearest station
|
|
372
372
|
stations_with_distances = [
|
|
373
|
-
(station, haversine_distance(
|
|
373
|
+
(station, haversine_distance(longitude, latitude, station['longitude'], station['latitude']))
|
|
374
374
|
for station in all_stations
|
|
375
375
|
]
|
|
376
376
|
|
|
@@ -445,7 +445,7 @@ def get_nearest_epw_from_climate_onebuilding(latitude: float, longitude: float,
|
|
|
445
445
|
# Print station information
|
|
446
446
|
print(f"\nDownloaded EPW file for {nearest_station['name']}")
|
|
447
447
|
print(f"Distance: {distance:.2f} km")
|
|
448
|
-
print(f"Station coordinates: {nearest_station['
|
|
448
|
+
print(f"Station coordinates: {nearest_station['longitude']}, {nearest_station['latitude']}")
|
|
449
449
|
if nearest_station['wmo']:
|
|
450
450
|
print(f"WMO: {nearest_station['wmo']}")
|
|
451
451
|
if nearest_station['climate_zone']:
|
|
@@ -520,4 +520,4 @@ def read_epw_for_solar_simulation(epw_file_path):
|
|
|
520
520
|
df = pd.DataFrame(data, columns=['time', 'DNI', 'DHI']).set_index('time')
|
|
521
521
|
df = df.sort_index()
|
|
522
522
|
|
|
523
|
-
return df,
|
|
523
|
+
return df, lon, lat, tz, elevation_m
|
voxcity/voxcity.py
CHANGED
|
@@ -52,10 +52,12 @@ from .utils.visualization import (
|
|
|
52
52
|
get_land_cover_classes,
|
|
53
53
|
visualize_land_cover_grid,
|
|
54
54
|
visualize_numerical_grid,
|
|
55
|
-
visualize_land_cover_grid_on_map,
|
|
56
|
-
visualize_numerical_grid_on_map,
|
|
57
|
-
visualize_building_height_grid_on_map,
|
|
58
|
-
visualize_3d_voxel
|
|
55
|
+
# visualize_land_cover_grid_on_map,
|
|
56
|
+
# visualize_numerical_grid_on_map,
|
|
57
|
+
# visualize_building_height_grid_on_map,
|
|
58
|
+
visualize_3d_voxel,
|
|
59
|
+
visualize_landcover_grid_on_basemap,
|
|
60
|
+
visualize_numerical_grid_on_basemap,
|
|
59
61
|
)
|
|
60
62
|
|
|
61
63
|
def get_land_cover_grid(rectangle_vertices, meshsize, source, output_dir, **kwargs):
|
|
@@ -593,10 +595,56 @@ def get_voxcity(rectangle_vertices, building_source, land_cover_source, canopy_h
|
|
|
593
595
|
# Visualize 2D grids on map if requested
|
|
594
596
|
mapvis = kwargs.get("mapvis")
|
|
595
597
|
if mapvis:
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
598
|
+
# Visualize land cover using the new function
|
|
599
|
+
visualize_landcover_grid_on_basemap(
|
|
600
|
+
land_cover_grid,
|
|
601
|
+
rectangle_vertices,
|
|
602
|
+
meshsize,
|
|
603
|
+
source=land_cover_source,
|
|
604
|
+
alpha=0.7,
|
|
605
|
+
figsize=(12, 8),
|
|
606
|
+
basemap='CartoDB light',
|
|
607
|
+
show_edge=False
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
# Visualize building heights using the new function
|
|
611
|
+
visualize_numerical_grid_on_basemap(
|
|
612
|
+
building_height_grid,
|
|
613
|
+
rectangle_vertices,
|
|
614
|
+
meshsize,
|
|
615
|
+
value_name="Building Heights (m)",
|
|
616
|
+
cmap='viridis',
|
|
617
|
+
alpha=0.7,
|
|
618
|
+
figsize=(12, 8),
|
|
619
|
+
basemap='CartoDB light',
|
|
620
|
+
show_edge=False
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
# Visualize canopy heights using the new function
|
|
624
|
+
visualize_numerical_grid_on_basemap(
|
|
625
|
+
canopy_height_grid,
|
|
626
|
+
rectangle_vertices,
|
|
627
|
+
meshsize,
|
|
628
|
+
value_name="Canopy Heights (m)",
|
|
629
|
+
cmap='Greens',
|
|
630
|
+
alpha=0.7,
|
|
631
|
+
figsize=(12, 8),
|
|
632
|
+
basemap='CartoDB light',
|
|
633
|
+
show_edge=False
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
# Visualize DEM using the new function
|
|
637
|
+
visualize_numerical_grid_on_basemap(
|
|
638
|
+
dem_grid,
|
|
639
|
+
rectangle_vertices,
|
|
640
|
+
meshsize,
|
|
641
|
+
value_name="Terrain Elevation (m)",
|
|
642
|
+
cmap='terrain',
|
|
643
|
+
alpha=0.7,
|
|
644
|
+
figsize=(12, 8),
|
|
645
|
+
basemap='CartoDB light',
|
|
646
|
+
show_edge=False
|
|
647
|
+
)
|
|
600
648
|
|
|
601
649
|
# Generate 3D voxel grid
|
|
602
650
|
voxcity_grid = create_3d_voxel(building_height_grid, building_min_height_grid, building_id_grid, land_cover_grid, dem_grid, canopy_height_grid, meshsize, land_cover_source)
|