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.

@@ -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(latitude: float, longitude: float, output_dir: str = "./", max_distance: Optional[float] = None,
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(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
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(latitude, longitude, station['latitude'], station['longitude']))
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['latitude']}, {nearest_station['longitude']}")
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, lat, lon, tz, elevation_m
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
- visualize_land_cover_grid_on_map(land_cover_grid, rectangle_vertices, meshsize, source = land_cover_source)
597
- visualize_building_height_grid_on_map(building_height_grid, building_geojson, rectangle_vertices, meshsize)
598
- visualize_numerical_grid_on_map(canopy_height_grid, rectangle_vertices, meshsize, "canopy_height")
599
- visualize_numerical_grid_on_map(dem_grid, rectangle_vertices, meshsize, "dem")
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)