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

Files changed (33) hide show
  1. voxcity/__init__.py +1 -1
  2. voxcity/{download → downloader}/eubucco.py +21 -35
  3. voxcity/{download → downloader}/mbfp.py +5 -6
  4. voxcity/{download → downloader}/omt.py +11 -4
  5. voxcity/{download → downloader}/osm.py +31 -35
  6. voxcity/{download → downloader}/overture.py +7 -4
  7. voxcity/{file → exporter}/envimet.py +2 -2
  8. voxcity/{voxcity.py → generator.py} +34 -33
  9. voxcity/{geo → geoprocessor}/__init_.py +1 -0
  10. voxcity/{geo → geoprocessor}/draw.py +15 -22
  11. voxcity/{geo → geoprocessor}/grid.py +199 -90
  12. voxcity/{geo → geoprocessor}/network.py +7 -7
  13. voxcity/{file/geojson.py → geoprocessor/polygon.py} +146 -168
  14. voxcity/{sim → simulator}/solar.py +1 -1
  15. voxcity/{sim → simulator}/view.py +6 -7
  16. voxcity/utils/visualization.py +2 -2
  17. {voxcity-0.3.12.dist-info → voxcity-0.3.14.dist-info}/METADATA +2 -2
  18. voxcity-0.3.14.dist-info/RECORD +36 -0
  19. voxcity-0.3.12.dist-info/RECORD +0 -36
  20. /voxcity/{download → downloader}/__init__.py +0 -0
  21. /voxcity/{download → downloader}/gee.py +0 -0
  22. /voxcity/{download → downloader}/oemj.py +0 -0
  23. /voxcity/{download → downloader}/utils.py +0 -0
  24. /voxcity/{file → exporter}/__init_.py +0 -0
  25. /voxcity/{file → exporter}/magicavoxel.py +0 -0
  26. /voxcity/{file → exporter}/obj.py +0 -0
  27. /voxcity/{geo → geoprocessor}/utils.py +0 -0
  28. /voxcity/{sim → simulator}/__init_.py +0 -0
  29. /voxcity/{sim → simulator}/utils.py +0 -0
  30. {voxcity-0.3.12.dist-info → voxcity-0.3.14.dist-info}/AUTHORS.rst +0 -0
  31. {voxcity-0.3.12.dist-info → voxcity-0.3.14.dist-info}/LICENSE +0 -0
  32. {voxcity-0.3.12.dist-info → voxcity-0.3.14.dist-info}/WHEEL +0 -0
  33. {voxcity-0.3.12.dist-info → voxcity-0.3.14.dist-info}/top_level.txt +0 -0
@@ -13,6 +13,7 @@ from shapely.geometry import box
13
13
  from scipy.interpolate import griddata
14
14
  from shapely.errors import GEOSException
15
15
  import geopandas as gpd
16
+ from rtree import index
16
17
 
17
18
  from .utils import (
18
19
  initialize_geod,
@@ -21,18 +22,18 @@ from .utils import (
21
22
  create_building_polygons,
22
23
  convert_format_lat_lon
23
24
  )
24
- from ..file.geojson import (
25
+ from ..geoprocessor.polygon import (
25
26
  filter_buildings,
26
27
  extract_building_heights_from_geotiff,
27
- extract_building_heights_from_geojson,
28
- complement_building_heights_from_geojson
28
+ extract_building_heights_from_gdf,
29
+ complement_building_heights_from_gdf
29
30
  )
30
31
  from ..utils.lc import (
31
32
  get_class_priority,
32
33
  create_land_cover_polygons,
33
34
  get_dominant_class,
34
35
  )
35
- from ..download.gee import (
36
+ from ..downloader.gee import (
36
37
  get_roi,
37
38
  save_geotiff_open_buildings_temporal
38
39
  )
@@ -304,11 +305,11 @@ def create_land_cover_grid_from_geotiff_polygon(tiff_path, mesh_size, land_cover
304
305
  # Flip grid vertically to match geographic orientation
305
306
  return np.flipud(grid)
306
307
 
307
- def create_land_cover_grid_from_geojson_polygon(geojson_data, meshsize, source, rectangle_vertices):
308
- """Create a grid of land cover classes from GeoJSON polygon data.
308
+ def create_land_cover_grid_from_gdf_polygon(gdf, meshsize, source, rectangle_vertices):
309
+ """Create a grid of land cover classes from GeoDataFrame polygon data.
309
310
 
310
311
  Args:
311
- geojson_data (dict): GeoJSON data containing land cover polygons
312
+ gdf (GeoDataFrame): GeoDataFrame containing land cover polygons
312
313
  meshsize (float): Size of each grid cell in meters
313
314
  source (str): Source of the land cover data to determine class priorities
314
315
  rectangle_vertices (list): List of 4 (lon,lat) coordinate pairs defining the rectangle bounds
@@ -366,7 +367,13 @@ def create_land_cover_grid_from_geojson_polygon(geojson_data, meshsize, source,
366
367
  plotting_box = box(extent[2], extent[0], extent[3], extent[1])
367
368
 
368
369
  # Create spatial index for efficient polygon lookup
369
- land_cover_polygons, idx = create_land_cover_polygons(geojson_data)
370
+ land_cover_polygons = []
371
+ idx = index.Index()
372
+ for i, row in gdf.iterrows():
373
+ polygon = row.geometry
374
+ land_cover_class = row['class']
375
+ land_cover_polygons.append((polygon, land_cover_class))
376
+ idx.insert(i, polygon.bounds)
370
377
 
371
378
  # Iterate through each grid cell
372
379
  for i in range(grid_size[0]):
@@ -467,160 +474,261 @@ def create_height_grid_from_geotiff_polygon(tiff_path, mesh_size, polygon):
467
474
 
468
475
  return np.flipud(grid)
469
476
 
470
- def create_building_height_grid_from_geojson_polygon(geojson_data, meshsize, rectangle_vertices, geojson_data_comp=None, geotiff_path_comp=None, complement_building_footprints=None):
477
+ def create_building_height_grid_from_gdf_polygon(
478
+ gdf,
479
+ meshsize,
480
+ rectangle_vertices,
481
+ gdf_comp=None,
482
+ geotiff_path_comp=None,
483
+ complement_building_footprints=None
484
+ ):
471
485
  """
472
- Create a building height grid from GeoJSON data within a polygon boundary.
486
+ Create a building height grid from GeoDataFrame data within a polygon boundary.
473
487
 
474
488
  Args:
475
- geojson_data (dict): GeoJSON data containing building information
489
+ gdf (geopandas.GeoDataFrame): GeoDataFrame containing building information
476
490
  meshsize (float): Size of mesh cells
477
491
  rectangle_vertices (list): List of rectangle vertices defining the boundary
478
- geojson_data_comp (dict, optional): Complementary GeoJSON data
492
+ gdf_comp (geopandas.GeoDataFrame, optional): Complementary GeoDataFrame
479
493
  geotiff_path_comp (str, optional): Path to complementary GeoTIFF file
480
- complement_building_footprints (bool, optional): Whether to complement building footprints
481
-
494
+ complement_building_footprints (bool, optional): Whether to complement footprints
495
+
482
496
  Returns:
483
497
  tuple: (building_height_grid, building_min_height_grid, building_id_grid, filtered_buildings)
484
498
  - building_height_grid (numpy.ndarray): Grid of building heights
485
- - building_min_height_grid (numpy.ndarray): Grid of minimum building heights
499
+ - building_min_height_grid (numpy.ndarray): Grid of min building heights (list per cell)
486
500
  - building_id_grid (numpy.ndarray): Grid of building IDs
487
- - filtered_buildings (list): List of filtered building features
501
+ - filtered_buildings (geopandas.GeoDataFrame): The buildings used (filtered_gdf)
488
502
  """
489
- # Initialize geodesic calculator and extract vertices
503
+ # --------------------------------------------------------------------------
504
+ # 1) INITIAL SETUP AND DATA FILTERING
505
+ # --------------------------------------------------------------------------
490
506
  geod = initialize_geod()
491
507
  vertex_0, vertex_1, vertex_3 = rectangle_vertices[0], rectangle_vertices[1], rectangle_vertices[3]
492
508
 
493
- # Calculate distances between vertices
509
+ # Distances for each side
494
510
  dist_side_1 = calculate_distance(geod, vertex_0[0], vertex_0[1], vertex_1[0], vertex_1[1])
495
511
  dist_side_2 = calculate_distance(geod, vertex_0[0], vertex_0[1], vertex_3[0], vertex_3[1])
496
512
 
497
- # Calculate normalized vectors for grid orientation
513
+ # Normalized vectors
498
514
  side_1 = np.array(vertex_1) - np.array(vertex_0)
499
515
  side_2 = np.array(vertex_3) - np.array(vertex_0)
500
516
  u_vec = normalize_to_one_meter(side_1, dist_side_1)
501
517
  v_vec = normalize_to_one_meter(side_2, dist_side_2)
502
518
 
503
- # Set up grid parameters
519
+ # Grid parameters
504
520
  origin = np.array(rectangle_vertices[0])
505
521
  grid_size, adjusted_meshsize = calculate_grid_size(side_1, side_2, u_vec, v_vec, meshsize)
506
522
 
507
523
  # Initialize output grids
508
524
  building_height_grid = np.zeros(grid_size)
509
525
  building_id_grid = np.zeros(grid_size)
526
+ # Use a Python list-of-lists or object array for min_height tracking
510
527
  building_min_height_grid = np.empty(grid_size, dtype=object)
511
-
512
- # Initialize min height grid with empty lists
513
528
  for i in range(grid_size[0]):
514
529
  for j in range(grid_size[1]):
515
530
  building_min_height_grid[i, j] = []
516
531
 
517
- # Create bounding box for filtering buildings
518
- extent = [min(coord[1] for coord in rectangle_vertices), max(coord[1] for coord in rectangle_vertices),
519
- min(coord[0] for coord in rectangle_vertices), max(coord[0] for coord in rectangle_vertices)]
532
+ # Filter the input GeoDataFrame by bounding box
533
+ extent = [
534
+ min(coord[1] for coord in rectangle_vertices),
535
+ max(coord[1] for coord in rectangle_vertices),
536
+ min(coord[0] for coord in rectangle_vertices),
537
+ max(coord[0] for coord in rectangle_vertices)
538
+ ]
520
539
  plotting_box = box(extent[2], extent[0], extent[3], extent[1])
540
+ filtered_gdf = gdf[gdf.geometry.intersects(plotting_box)].copy()
521
541
 
522
- # Filter and process buildings
523
- filtered_buildings = filter_buildings(geojson_data, plotting_box)
524
-
525
- # Handle complementary data sources
526
- if geojson_data_comp:
527
- filtered_geojson_data_comp = filter_buildings(geojson_data_comp, plotting_box)
542
+ # Optionally merge heights from complementary sources
543
+ if gdf_comp is not None:
544
+ filtered_gdf_comp = gdf_comp[gdf_comp.geometry.intersects(plotting_box)].copy()
528
545
  if complement_building_footprints:
529
- filtered_buildings_comp = complement_building_heights_from_geojson(filtered_buildings, filtered_geojson_data_comp)
546
+ filtered_gdf = complement_building_heights_gdf(filtered_gdf, filtered_gdf_comp)
530
547
  else:
531
- filtered_buildings_comp = extract_building_heights_from_geojson(filtered_buildings, filtered_geojson_data_comp)
548
+ filtered_gdf = extract_building_heights_from_gdf(filtered_gdf, filtered_gdf_comp)
532
549
  elif geotiff_path_comp:
533
- filtered_buildings_comp = extract_building_heights_from_geotiff(geotiff_path_comp, filtered_buildings)
534
- else:
535
- filtered_buildings_comp = filtered_buildings
536
-
537
- # Create building polygons and spatial index
538
- building_polygons, idx = create_building_polygons(filtered_buildings_comp)
550
+ filtered_gdf = extract_building_heights_from_geotiff(geotiff_path_comp, filtered_gdf)
551
+
552
+ # --------------------------------------------------------------------------
553
+ # 2) PREPARE BUILDING POLYGONS & SPATIAL INDEX
554
+ # - fix geometries if needed
555
+ # - store bounding boxes
556
+ # --------------------------------------------------------------------------
557
+ building_polygons = []
558
+ for idx_b, row in filtered_gdf.iterrows():
559
+ polygon = row.geometry
560
+ height = row.get('height', None)
561
+ min_height = row.get('min_height', 0)
562
+ is_inner = row.get('is_inner', False)
563
+ feature_id = row.get('id', idx_b)
564
+
565
+ # Fix invalid geometry (buffer(0) or simplify if needed)
566
+ # Doing this once per building avoids repeated overhead per cell.
567
+ if not polygon.is_valid:
568
+ try:
569
+ # Attempt simple fix
570
+ polygon = polygon.buffer(0)
571
+ if not polygon.is_valid:
572
+ # Attempt a small simplify if buffer(0) didn't fix it
573
+ polygon = polygon.simplify(1e-8)
574
+ except Exception as e:
575
+ # If still invalid, we keep it as-is; intersection attempts
576
+ # will be caught in the main loop's try/except
577
+ pass
578
+
579
+ bounding_box = polygon.bounds # (minx, miny, maxx, maxy)
580
+ building_polygons.append(
581
+ (
582
+ polygon,
583
+ bounding_box,
584
+ height,
585
+ min_height,
586
+ is_inner,
587
+ feature_id
588
+ )
589
+ )
590
+
591
+ # Build R-tree index using bounding boxes
592
+ idx = index.Index()
593
+ for i_b, (poly, bbox, _, _, _, _) in enumerate(building_polygons):
594
+ idx.insert(i_b, bbox)
595
+
596
+ # --------------------------------------------------------------------------
597
+ # 3) MAIN GRID LOOP
598
+ # --------------------------------------------------------------------------
599
+ INTERSECTION_THRESHOLD = 0.3
539
600
 
540
- # Process each grid cell
541
601
  for i in range(grid_size[0]):
542
602
  for j in range(grid_size[1]):
603
+ # Create the cell polygon once
543
604
  cell = create_cell_polygon(origin, i, j, adjusted_meshsize, u_vec, v_vec)
544
- # Ensure cell geometry is valid
545
605
  if not cell.is_valid:
546
606
  cell = cell.buffer(0)
547
-
548
- # Get potential intersecting buildings using spatial index
549
- potential_intersections = list(idx.intersection(cell.bounds))
550
-
551
- if not potential_intersections:
607
+
608
+ cell_area = cell.area # reused for intersection ratio checks
609
+
610
+ # Find possible intersections from the index
611
+ potential = list(idx.intersection(cell.bounds))
612
+ if not potential:
552
613
  continue
553
-
554
- # Sort buildings by height for proper layering
555
- cell_buildings = [(k, building_polygons[k]) for k in potential_intersections]
556
- cell_buildings.sort(key=lambda x: x[1][1] if x[1][1] is not None else -float('inf'), reverse=True)
557
-
558
- # Track intersection status
614
+
615
+ # Sort buildings by height descending
616
+ # (height=None => treat as -inf so it sorts last)
617
+ cell_buildings = []
618
+ for k in potential:
619
+ bpoly, bbox, height, minh, inr, fid = building_polygons[k]
620
+ # We only sort by height for a stable layering
621
+ sort_val = height if (height is not None) else -float('inf')
622
+ cell_buildings.append((k, bpoly, bbox, height, minh, inr, fid, sort_val))
623
+
624
+ cell_buildings.sort(key=lambda x: x[-1], reverse=True)
625
+
559
626
  found_intersection = False
560
627
  all_zero_or_nan = True
561
-
562
- # Process each potential building intersection
563
- for k, (polygon, height, min_height, is_inner, feature_id) in cell_buildings:
628
+
629
+ for (
630
+ k,
631
+ polygon,
632
+ bbox,
633
+ height,
634
+ min_height,
635
+ is_inner,
636
+ feature_id,
637
+ _
638
+ ) in cell_buildings:
564
639
  try:
640
+ # Quick bounding-box check to skip expensive geometry intersection
641
+ # if bounding boxes cannot possibly meet the threshold
642
+ # --------------------------------------------------------
643
+ minx_p, miny_p, maxx_p, maxy_p = bbox
644
+ minx_c, miny_c, maxx_c, maxy_c = cell.bounds
645
+
646
+ # Overlap bounding box
647
+ overlap_minx = max(minx_p, minx_c)
648
+ overlap_miny = max(miny_p, miny_c)
649
+ overlap_maxx = min(maxx_p, maxx_c)
650
+ overlap_maxy = min(maxy_p, maxy_c)
651
+
652
+ if (overlap_maxx <= overlap_minx) or (overlap_maxy <= overlap_miny):
653
+ # No bounding-box overlap => skip
654
+ continue
655
+
656
+ # Area of bounding-box intersection
657
+ bbox_intersect_area = (overlap_maxx - overlap_minx) * (overlap_maxy - overlap_miny)
658
+ if bbox_intersect_area < INTERSECTION_THRESHOLD * cell_area:
659
+ # Even if the polygons overlap, they can't exceed threshold
660
+ continue
661
+ # --------------------------------------------------------
662
+
565
663
  # Ensure valid geometry
566
664
  if not polygon.is_valid:
567
665
  polygon = polygon.buffer(0)
568
-
569
- # Check for intersection
666
+ # If still invalid, let intersection attempt fail
667
+
570
668
  if cell.intersects(polygon):
571
669
  intersection = cell.intersection(polygon)
572
- intersection_ratio = intersection.area / cell.area
573
-
574
- INTERSECTION_THRESHOLD = 0.3
575
-
576
- if intersection_ratio > INTERSECTION_THRESHOLD:
670
+ inter_area = intersection.area
671
+
672
+ # If the fraction of cell covered > threshold
673
+ if (inter_area / cell_area) > INTERSECTION_THRESHOLD:
577
674
  found_intersection = True
578
-
675
+
676
+ # If not an inner courtyard
579
677
  if not is_inner:
580
- # Store building information
581
678
  building_min_height_grid[i, j].append([min_height, height])
582
679
  building_id_grid[i, j] = feature_id
583
-
584
- # Update height if valid
585
- has_valid_height = height is not None and not np.isnan(height) and height > 0
586
- if has_valid_height:
680
+
681
+ # Update building height if valid
682
+ if (
683
+ height is not None
684
+ and not np.isnan(height)
685
+ and height > 0
686
+ ):
587
687
  all_zero_or_nan = False
588
-
589
688
  current_height = building_height_grid[i, j]
590
- if (current_height == 0 or
591
- current_height < height or
592
- np.isnan(current_height)):
689
+
690
+ # Replace if we had 0, nan, or smaller height
691
+ if (
692
+ current_height == 0
693
+ or np.isnan(current_height)
694
+ or current_height < height
695
+ ):
593
696
  building_height_grid[i, j] = height
697
+
594
698
  else:
595
- # Handle inner courtyards
699
+ # Inner courtyards => override with 0
596
700
  building_min_height_grid[i, j] = [[0, 0]]
597
701
  building_height_grid[i, j] = 0
598
702
  found_intersection = True
599
703
  all_zero_or_nan = False
704
+ # Because it's an inner courtyard, we can break
600
705
  break
601
-
706
+
602
707
  except (GEOSException, ValueError) as e:
603
- # Attempt to fix topology errors
708
+ # Attempt to fix in a fallback
604
709
  try:
605
710
  simplified_polygon = polygon.simplify(1e-8)
606
711
  if simplified_polygon.is_valid:
607
712
  intersection = cell.intersection(simplified_polygon)
608
- intersection_ratio = intersection.area / cell.area
609
-
610
- if intersection_ratio > INTERSECTION_THRESHOLD:
713
+ inter_area = intersection.area
714
+
715
+ if (inter_area / cell_area) > INTERSECTION_THRESHOLD:
611
716
  found_intersection = True
612
-
613
717
  if not is_inner:
614
718
  building_min_height_grid[i, j].append([min_height, height])
615
719
  building_id_grid[i, j] = feature_id
616
-
617
- has_valid_height = height is not None and not np.isnan(height) and height > 0
618
- if has_valid_height:
720
+
721
+ if (
722
+ height is not None
723
+ and not np.isnan(height)
724
+ and height > 0
725
+ ):
619
726
  all_zero_or_nan = False
620
-
621
- if (building_height_grid[i, j] == 0 or
622
- building_height_grid[i, j] < height or
623
- np.isnan(building_height_grid[i, j])):
727
+ if (
728
+ building_height_grid[i, j] == 0
729
+ or np.isnan(building_height_grid[i, j])
730
+ or building_height_grid[i, j] < height
731
+ ):
624
732
  building_height_grid[i, j] = height
625
733
  else:
626
734
  building_min_height_grid[i, j] = [[0, 0]]
@@ -629,14 +737,15 @@ def create_building_height_grid_from_geojson_polygon(geojson_data, meshsize, rec
629
737
  all_zero_or_nan = False
630
738
  break
631
739
  except Exception as fix_error:
740
+ # Log and skip
632
741
  print(f"Failed to process cell ({i}, {j}) - Building {k}: {str(fix_error)}")
633
742
  continue
634
-
635
- # Set cell to NaN if all intersecting buildings had zero/NaN height
743
+
744
+ # If we found intersecting buildings but all were zero/NaN, mark as NaN
636
745
  if found_intersection and all_zero_or_nan:
637
746
  building_height_grid[i, j] = np.nan
638
747
 
639
- return building_height_grid, building_min_height_grid, building_id_grid, filtered_buildings
748
+ return building_height_grid, building_min_height_grid, building_id_grid, filtered_gdf
640
749
 
641
750
  def create_building_height_grid_from_open_building_temporal_polygon(meshsize, rectangle_vertices, output_dir):
642
751
  """
@@ -671,7 +780,7 @@ def create_building_height_grid_from_open_building_temporal_polygon(meshsize, re
671
780
  building_min_height_grid[i, j] = [[0, building_height_grid[i, j]]]
672
781
 
673
782
  # Create building ID grid with sequential numbering for non-zero heights
674
- filtered_buildings = []
783
+ filtered_buildings = gpd.GeoDataFrame()
675
784
  building_id_grid = np.zeros_like(building_height_grid, dtype=int)
676
785
  non_zero_positions = np.nonzero(building_height_grid)
677
786
  sequence = np.arange(1, len(non_zero_positions[0]) + 1)
@@ -165,11 +165,6 @@ def get_network_values(
165
165
 
166
166
  return G, edge_gdf
167
167
 
168
- # -------------------------------------------------------------------
169
- # Optionally import your DEM helper
170
- # -------------------------------------------------------------------
171
- from voxcity.geo.grid import grid_to_geodataframe
172
-
173
168
  # -------------------------------------------------------------------
174
169
  # 1) Functions for interpolation, parallelization, and slope
175
170
  # -------------------------------------------------------------------
@@ -511,8 +506,13 @@ def analyze_network_slopes(
511
506
  legend_kwds={'label': f"{value_name} (%)"}
512
507
  )
513
508
 
514
- # Add basemap
515
- ctx.add_basemap(ax, source=settings['basemap_style'], zoom=settings['zoom'])
509
+ # Add basemap with the same extent as the rectangle
510
+ ctx.add_basemap(
511
+ ax,
512
+ source=settings['basemap_style'],
513
+ zoom=settings['zoom'],
514
+ bounds=(minx, miny, maxx, maxy) # Explicitly set the bounds of the basemap
515
+ )
516
516
 
517
517
  # Set the plot limits to the bounding box of the rectangle
518
518
  ax.set_xlim(minx, maxx)