voxcity 0.3.13__py3-none-any.whl → 0.3.15__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/eubucco.py +20 -34
- voxcity/downloader/mbfp.py +4 -5
- voxcity/downloader/omt.py +11 -4
- voxcity/downloader/osm.py +31 -35
- voxcity/downloader/overture.py +7 -4
- voxcity/generator.py +31 -30
- voxcity/geoprocessor/draw.py +13 -21
- voxcity/geoprocessor/grid.py +286 -97
- voxcity/geoprocessor/polygon.py +145 -167
- voxcity/simulator/view.py +4 -5
- voxcity/utils/visualization.py +135 -0
- {voxcity-0.3.13.dist-info → voxcity-0.3.15.dist-info}/METADATA +3 -3
- {voxcity-0.3.13.dist-info → voxcity-0.3.15.dist-info}/RECORD +17 -17
- {voxcity-0.3.13.dist-info → voxcity-0.3.15.dist-info}/AUTHORS.rst +0 -0
- {voxcity-0.3.13.dist-info → voxcity-0.3.15.dist-info}/LICENSE +0 -0
- {voxcity-0.3.13.dist-info → voxcity-0.3.15.dist-info}/WHEEL +0 -0
- {voxcity-0.3.13.dist-info → voxcity-0.3.15.dist-info}/top_level.txt +0 -0
voxcity/geoprocessor/grid.py
CHANGED
|
@@ -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,
|
|
@@ -24,8 +25,8 @@ from .utils import (
|
|
|
24
25
|
from ..geoprocessor.polygon import (
|
|
25
26
|
filter_buildings,
|
|
26
27
|
extract_building_heights_from_geotiff,
|
|
27
|
-
|
|
28
|
-
|
|
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,
|
|
@@ -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
|
|
308
|
-
"""Create a grid of land cover classes from
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
486
|
+
Create a building height grid from GeoDataFrame data within a polygon boundary.
|
|
473
487
|
|
|
474
488
|
Args:
|
|
475
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 (
|
|
501
|
+
- filtered_buildings (geopandas.GeoDataFrame): The buildings used (filtered_gdf)
|
|
488
502
|
"""
|
|
489
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
518
|
-
extent = [
|
|
519
|
-
|
|
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
|
-
#
|
|
523
|
-
|
|
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
|
-
|
|
546
|
+
filtered_gdf = complement_building_heights_gdf(filtered_gdf, filtered_gdf_comp)
|
|
530
547
|
else:
|
|
531
|
-
|
|
548
|
+
filtered_gdf = extract_building_heights_from_gdf(filtered_gdf, filtered_gdf_comp)
|
|
532
549
|
elif geotiff_path_comp:
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
#
|
|
538
|
-
|
|
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
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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
|
|
555
|
-
|
|
556
|
-
cell_buildings
|
|
557
|
-
|
|
558
|
-
|
|
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
|
-
|
|
563
|
-
|
|
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
|
-
|
|
666
|
+
# If still invalid, let intersection attempt fail
|
|
667
|
+
|
|
570
668
|
if cell.intersects(polygon):
|
|
571
669
|
intersection = cell.intersection(polygon)
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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
|
-
|
|
586
|
-
|
|
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
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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
|
-
#
|
|
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
|
|
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
|
-
|
|
609
|
-
|
|
610
|
-
if
|
|
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
|
-
|
|
618
|
-
|
|
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
|
-
|
|
622
|
-
building_height_grid[i, j]
|
|
623
|
-
|
|
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
|
-
#
|
|
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,
|
|
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)
|
|
@@ -774,10 +883,19 @@ def grid_to_geodataframe(grid_ori, rectangle_vertices, meshsize):
|
|
|
774
883
|
|
|
775
884
|
rows, cols = grid.shape
|
|
776
885
|
|
|
777
|
-
#
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
886
|
+
# Set up transformers for accurate coordinate calculations
|
|
887
|
+
wgs84 = CRS.from_epsg(4326)
|
|
888
|
+
web_mercator = CRS.from_epsg(3857)
|
|
889
|
+
transformer_to_mercator = Transformer.from_crs(wgs84, web_mercator, always_xy=True)
|
|
890
|
+
transformer_to_wgs84 = Transformer.from_crs(web_mercator, wgs84, always_xy=True)
|
|
891
|
+
|
|
892
|
+
# Convert bounds to Web Mercator for accurate distance calculations
|
|
893
|
+
min_x, min_y = transformer_to_mercator.transform(min_lon, min_lat)
|
|
894
|
+
max_x, max_y = transformer_to_mercator.transform(max_lon, max_lat)
|
|
895
|
+
|
|
896
|
+
# Calculate cell sizes in Web Mercator coordinates
|
|
897
|
+
cell_size_x = (max_x - min_x) / cols
|
|
898
|
+
cell_size_y = (max_y - min_y) / rows
|
|
781
899
|
|
|
782
900
|
# Create lists to store data
|
|
783
901
|
polygons = []
|
|
@@ -786,12 +904,16 @@ def grid_to_geodataframe(grid_ori, rectangle_vertices, meshsize):
|
|
|
786
904
|
# Create grid cells
|
|
787
905
|
for i in range(rows):
|
|
788
906
|
for j in range(cols):
|
|
789
|
-
# Calculate cell bounds
|
|
790
|
-
|
|
791
|
-
|
|
907
|
+
# Calculate cell bounds in Web Mercator
|
|
908
|
+
cell_min_x = min_x + j * cell_size_x
|
|
909
|
+
cell_max_x = min_x + (j + 1) * cell_size_x
|
|
792
910
|
# Flip vertical axis since grid is stored with origin at top-left
|
|
793
|
-
|
|
794
|
-
|
|
911
|
+
cell_min_y = max_y - (i + 1) * cell_size_y
|
|
912
|
+
cell_max_y = max_y - i * cell_size_y
|
|
913
|
+
|
|
914
|
+
# Convert cell corners back to WGS84
|
|
915
|
+
cell_min_lon, cell_min_lat = transformer_to_wgs84.transform(cell_min_x, cell_min_y)
|
|
916
|
+
cell_max_lon, cell_max_lat = transformer_to_wgs84.transform(cell_max_x, cell_max_y)
|
|
795
917
|
|
|
796
918
|
# Create polygon for cell
|
|
797
919
|
cell_poly = box(cell_min_lon, cell_min_lat, cell_max_lon, cell_max_lat)
|
|
@@ -805,4 +927,71 @@ def grid_to_geodataframe(grid_ori, rectangle_vertices, meshsize):
|
|
|
805
927
|
'value': values
|
|
806
928
|
}, crs=CRS.from_epsg(4326))
|
|
807
929
|
|
|
930
|
+
return gdf
|
|
931
|
+
|
|
932
|
+
def grid_to_point_geodataframe(grid_ori, rectangle_vertices, meshsize):
|
|
933
|
+
"""Converts a 2D grid to a GeoDataFrame with point geometries at cell centers and values.
|
|
934
|
+
|
|
935
|
+
Args:
|
|
936
|
+
grid: 2D numpy array containing grid values
|
|
937
|
+
rectangle_vertices: List of [lon, lat] coordinates defining area corners
|
|
938
|
+
meshsize: Size of each grid cell in meters
|
|
939
|
+
|
|
940
|
+
Returns:
|
|
941
|
+
GeoDataFrame with columns:
|
|
942
|
+
- geometry: Point geometry at center of each grid cell
|
|
943
|
+
- value: Value from the grid
|
|
944
|
+
"""
|
|
945
|
+
grid = np.flipud(grid_ori.copy())
|
|
946
|
+
|
|
947
|
+
# Extract bounds from rectangle vertices
|
|
948
|
+
min_lon = min(v[0] for v in rectangle_vertices)
|
|
949
|
+
max_lon = max(v[0] for v in rectangle_vertices)
|
|
950
|
+
min_lat = min(v[1] for v in rectangle_vertices)
|
|
951
|
+
max_lat = max(v[1] for v in rectangle_vertices)
|
|
952
|
+
|
|
953
|
+
rows, cols = grid.shape
|
|
954
|
+
|
|
955
|
+
# Set up transformers for accurate coordinate calculations
|
|
956
|
+
wgs84 = CRS.from_epsg(4326)
|
|
957
|
+
web_mercator = CRS.from_epsg(3857)
|
|
958
|
+
transformer_to_mercator = Transformer.from_crs(wgs84, web_mercator, always_xy=True)
|
|
959
|
+
transformer_to_wgs84 = Transformer.from_crs(web_mercator, wgs84, always_xy=True)
|
|
960
|
+
|
|
961
|
+
# Convert bounds to Web Mercator for accurate distance calculations
|
|
962
|
+
min_x, min_y = transformer_to_mercator.transform(min_lon, min_lat)
|
|
963
|
+
max_x, max_y = transformer_to_mercator.transform(max_lon, max_lat)
|
|
964
|
+
|
|
965
|
+
# Calculate cell sizes in Web Mercator coordinates
|
|
966
|
+
cell_size_x = (max_x - min_x) / cols
|
|
967
|
+
cell_size_y = (max_y - min_y) / rows
|
|
968
|
+
|
|
969
|
+
# Create lists to store data
|
|
970
|
+
points = []
|
|
971
|
+
values = []
|
|
972
|
+
|
|
973
|
+
# Create grid points at cell centers
|
|
974
|
+
for i in range(rows):
|
|
975
|
+
for j in range(cols):
|
|
976
|
+
# Calculate cell center in Web Mercator
|
|
977
|
+
cell_center_x = min_x + (j + 0.5) * cell_size_x
|
|
978
|
+
# Flip vertical axis since grid is stored with origin at top-left
|
|
979
|
+
cell_center_y = max_y - (i + 0.5) * cell_size_y
|
|
980
|
+
|
|
981
|
+
# Convert cell center back to WGS84
|
|
982
|
+
center_lon, center_lat = transformer_to_wgs84.transform(cell_center_x, cell_center_y)
|
|
983
|
+
|
|
984
|
+
# Create point for cell center
|
|
985
|
+
from shapely.geometry import Point
|
|
986
|
+
cell_point = Point(center_lon, center_lat)
|
|
987
|
+
|
|
988
|
+
points.append(cell_point)
|
|
989
|
+
values.append(grid[i, j])
|
|
990
|
+
|
|
991
|
+
# Create GeoDataFrame
|
|
992
|
+
gdf = gpd.GeoDataFrame({
|
|
993
|
+
'geometry': points,
|
|
994
|
+
'value': values
|
|
995
|
+
}, crs=CRS.from_epsg(4326))
|
|
996
|
+
|
|
808
997
|
return gdf
|