voxcity 0.6.15__py3-none-any.whl → 0.7.0__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.
- voxcity/__init__.py +14 -8
- voxcity/downloader/__init__.py +2 -1
- voxcity/downloader/citygml.py +32 -18
- voxcity/downloader/gba.py +210 -0
- voxcity/downloader/gee.py +5 -1
- voxcity/downloader/mbfp.py +1 -1
- voxcity/downloader/oemj.py +80 -8
- voxcity/downloader/osm.py +23 -7
- voxcity/downloader/overture.py +26 -1
- voxcity/downloader/utils.py +73 -73
- voxcity/errors.py +30 -0
- voxcity/exporter/__init__.py +13 -4
- voxcity/exporter/cityles.py +633 -535
- voxcity/exporter/envimet.py +728 -708
- voxcity/exporter/magicavoxel.py +334 -297
- voxcity/exporter/netcdf.py +238 -0
- voxcity/exporter/obj.py +1481 -655
- voxcity/generator/__init__.py +44 -0
- voxcity/generator/api.py +675 -0
- voxcity/generator/grids.py +379 -0
- voxcity/generator/io.py +94 -0
- voxcity/generator/pipeline.py +282 -0
- voxcity/generator/voxelizer.py +380 -0
- voxcity/geoprocessor/__init__.py +75 -6
- voxcity/geoprocessor/conversion.py +153 -0
- voxcity/geoprocessor/draw.py +62 -12
- voxcity/geoprocessor/heights.py +199 -0
- voxcity/geoprocessor/io.py +101 -0
- voxcity/geoprocessor/merge_utils.py +91 -0
- voxcity/geoprocessor/mesh.py +806 -790
- voxcity/geoprocessor/network.py +708 -679
- voxcity/geoprocessor/overlap.py +84 -0
- voxcity/geoprocessor/raster/__init__.py +82 -0
- voxcity/geoprocessor/raster/buildings.py +428 -0
- voxcity/geoprocessor/raster/canopy.py +258 -0
- voxcity/geoprocessor/raster/core.py +150 -0
- voxcity/geoprocessor/raster/export.py +93 -0
- voxcity/geoprocessor/raster/landcover.py +156 -0
- voxcity/geoprocessor/raster/raster.py +110 -0
- voxcity/geoprocessor/selection.py +85 -0
- voxcity/geoprocessor/utils.py +18 -14
- voxcity/models.py +113 -0
- voxcity/simulator/common/__init__.py +22 -0
- voxcity/simulator/common/geometry.py +98 -0
- voxcity/simulator/common/raytracing.py +450 -0
- voxcity/simulator/solar/__init__.py +43 -0
- voxcity/simulator/solar/integration.py +336 -0
- voxcity/simulator/solar/kernels.py +62 -0
- voxcity/simulator/solar/radiation.py +648 -0
- voxcity/simulator/solar/temporal.py +434 -0
- voxcity/simulator/view.py +36 -2286
- voxcity/simulator/visibility/__init__.py +29 -0
- voxcity/simulator/visibility/landmark.py +392 -0
- voxcity/simulator/visibility/view.py +508 -0
- voxcity/utils/logging.py +61 -0
- voxcity/utils/orientation.py +51 -0
- voxcity/utils/weather/__init__.py +26 -0
- voxcity/utils/weather/epw.py +146 -0
- voxcity/utils/weather/files.py +36 -0
- voxcity/utils/weather/onebuilding.py +486 -0
- voxcity/visualizer/__init__.py +24 -0
- voxcity/visualizer/builder.py +43 -0
- voxcity/visualizer/grids.py +141 -0
- voxcity/visualizer/maps.py +187 -0
- voxcity/visualizer/palette.py +228 -0
- voxcity/visualizer/renderer.py +928 -0
- {voxcity-0.6.15.dist-info → voxcity-0.7.0.dist-info}/METADATA +113 -36
- voxcity-0.7.0.dist-info/RECORD +77 -0
- {voxcity-0.6.15.dist-info → voxcity-0.7.0.dist-info}/WHEEL +1 -1
- voxcity/generator.py +0 -1137
- voxcity/geoprocessor/grid.py +0 -1568
- voxcity/geoprocessor/polygon.py +0 -1344
- voxcity/simulator/solar.py +0 -2329
- voxcity/utils/visualization.py +0 -2660
- voxcity/utils/weather.py +0 -817
- voxcity-0.6.15.dist-info/RECORD +0 -37
- {voxcity-0.6.15.dist-info → voxcity-0.7.0.dist-info/licenses}/AUTHORS.rst +0 -0
- {voxcity-0.6.15.dist-info → voxcity-0.7.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utilities for processing overlaps between building footprints.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from rtree import index
|
|
6
|
+
from shapely.errors import GEOSException
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def process_building_footprints_by_overlap(filtered_gdf, overlap_threshold=0.5):
|
|
10
|
+
"""
|
|
11
|
+
Merge overlapping buildings based on area overlap ratio, assigning the ID of the larger building
|
|
12
|
+
to smaller overlapping ones.
|
|
13
|
+
"""
|
|
14
|
+
gdf = filtered_gdf.copy()
|
|
15
|
+
|
|
16
|
+
if 'id' not in gdf.columns:
|
|
17
|
+
gdf['id'] = gdf.index
|
|
18
|
+
|
|
19
|
+
if gdf.crs is None:
|
|
20
|
+
gdf_projected = gdf.copy()
|
|
21
|
+
else:
|
|
22
|
+
gdf_projected = gdf.to_crs("EPSG:3857")
|
|
23
|
+
|
|
24
|
+
gdf_projected['area'] = gdf_projected.geometry.area
|
|
25
|
+
gdf_projected = gdf_projected.sort_values(by='area', ascending=False)
|
|
26
|
+
gdf_projected = gdf_projected.reset_index(drop=True)
|
|
27
|
+
|
|
28
|
+
spatial_idx = index.Index()
|
|
29
|
+
for i, geom in enumerate(gdf_projected.geometry):
|
|
30
|
+
if geom.is_valid:
|
|
31
|
+
spatial_idx.insert(i, geom.bounds)
|
|
32
|
+
else:
|
|
33
|
+
fixed_geom = geom.buffer(0)
|
|
34
|
+
if fixed_geom.is_valid:
|
|
35
|
+
spatial_idx.insert(i, fixed_geom.bounds)
|
|
36
|
+
|
|
37
|
+
id_mapping = {}
|
|
38
|
+
|
|
39
|
+
for i in range(1, len(gdf_projected)):
|
|
40
|
+
current_poly = gdf_projected.iloc[i].geometry
|
|
41
|
+
current_area = gdf_projected.iloc[i].area
|
|
42
|
+
current_id = gdf_projected.iloc[i]['id']
|
|
43
|
+
|
|
44
|
+
if current_id in id_mapping:
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
if not current_poly.is_valid:
|
|
48
|
+
current_poly = current_poly.buffer(0)
|
|
49
|
+
if not current_poly.is_valid:
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
potential_overlaps = [j for j in spatial_idx.intersection(current_poly.bounds) if j < i]
|
|
53
|
+
|
|
54
|
+
for j in potential_overlaps:
|
|
55
|
+
larger_poly = gdf_projected.iloc[j].geometry
|
|
56
|
+
larger_id = gdf_projected.iloc[j]['id']
|
|
57
|
+
|
|
58
|
+
if larger_id in id_mapping:
|
|
59
|
+
larger_id = id_mapping[larger_id]
|
|
60
|
+
|
|
61
|
+
if not larger_poly.is_valid:
|
|
62
|
+
larger_poly = larger_poly.buffer(0)
|
|
63
|
+
if not larger_poly.is_valid:
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
if current_poly.intersects(larger_poly):
|
|
68
|
+
overlap = current_poly.intersection(larger_poly)
|
|
69
|
+
overlap_ratio = overlap.area / current_area
|
|
70
|
+
if overlap_ratio > overlap_threshold:
|
|
71
|
+
id_mapping[current_id] = larger_id
|
|
72
|
+
gdf_projected.at[i, 'id'] = larger_id
|
|
73
|
+
break
|
|
74
|
+
except (GEOSException, ValueError):
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
for i, row in filtered_gdf.iterrows():
|
|
78
|
+
orig_id = row.get('id')
|
|
79
|
+
if orig_id in id_mapping:
|
|
80
|
+
filtered_gdf.at[i, 'id'] = id_mapping[orig_id]
|
|
81
|
+
|
|
82
|
+
return filtered_gdf
|
|
83
|
+
|
|
84
|
+
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Raster processing package.
|
|
3
|
+
|
|
4
|
+
Orientation contract:
|
|
5
|
+
- All public functions accept and return 2D grids using the canonical internal
|
|
6
|
+
orientation "north_up": row 0 is the northern/top row.
|
|
7
|
+
- Where data sources use south_up, conversions are performed internally; outputs
|
|
8
|
+
are always north_up unless explicitly documented otherwise.
|
|
9
|
+
- Columns increase eastward (col 0 = west/left), indices increase to the east.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
# Re-export public APIs from submodules
|
|
13
|
+
from .core import (
|
|
14
|
+
apply_operation,
|
|
15
|
+
translate_array,
|
|
16
|
+
group_and_label_cells,
|
|
17
|
+
process_grid_optimized,
|
|
18
|
+
process_grid,
|
|
19
|
+
calculate_grid_size,
|
|
20
|
+
create_coordinate_mesh,
|
|
21
|
+
create_cell_polygon,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
from .landcover import (
|
|
25
|
+
tree_height_grid_from_land_cover,
|
|
26
|
+
create_land_cover_grid_from_geotiff_polygon,
|
|
27
|
+
create_land_cover_grid_from_gdf_polygon,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from .raster import (
|
|
31
|
+
create_height_grid_from_geotiff_polygon,
|
|
32
|
+
create_dem_grid_from_geotiff_polygon,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
from .buildings import (
|
|
36
|
+
create_building_height_grid_from_gdf_polygon,
|
|
37
|
+
create_building_height_grid_from_open_building_temporal_polygon,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
from .export import (
|
|
41
|
+
grid_to_geodataframe,
|
|
42
|
+
grid_to_point_geodataframe,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
from .canopy import (
|
|
46
|
+
create_vegetation_height_grid_from_gdf_polygon,
|
|
47
|
+
create_dem_grid_from_gdf_polygon,
|
|
48
|
+
create_canopy_grids_from_tree_gdf,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
# core
|
|
53
|
+
"apply_operation",
|
|
54
|
+
"translate_array",
|
|
55
|
+
"group_and_label_cells",
|
|
56
|
+
"process_grid_optimized",
|
|
57
|
+
"process_grid",
|
|
58
|
+
"calculate_grid_size",
|
|
59
|
+
"create_coordinate_mesh",
|
|
60
|
+
"create_cell_polygon",
|
|
61
|
+
# landcover
|
|
62
|
+
"tree_height_grid_from_land_cover",
|
|
63
|
+
"create_land_cover_grid_from_geotiff_polygon",
|
|
64
|
+
"create_land_cover_grid_from_gdf_polygon",
|
|
65
|
+
# raster
|
|
66
|
+
"create_height_grid_from_geotiff_polygon",
|
|
67
|
+
"create_dem_grid_from_geotiff_polygon",
|
|
68
|
+
# buildings
|
|
69
|
+
"create_building_height_grid_from_gdf_polygon",
|
|
70
|
+
"create_building_height_grid_from_open_building_temporal_polygon",
|
|
71
|
+
# export
|
|
72
|
+
"grid_to_geodataframe",
|
|
73
|
+
"grid_to_point_geodataframe",
|
|
74
|
+
# vegetation/terrain/trees
|
|
75
|
+
"create_vegetation_height_grid_from_gdf_polygon",
|
|
76
|
+
"create_dem_grid_from_gdf_polygon",
|
|
77
|
+
"create_canopy_grids_from_tree_gdf",
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import List, Tuple, Optional
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import geopandas as gpd
|
|
7
|
+
from shapely.geometry import box, mapping
|
|
8
|
+
from shapely.errors import GEOSException
|
|
9
|
+
from affine import Affine
|
|
10
|
+
from rtree import index
|
|
11
|
+
from rasterio import features
|
|
12
|
+
|
|
13
|
+
from ..utils import (
|
|
14
|
+
initialize_geod,
|
|
15
|
+
calculate_distance,
|
|
16
|
+
normalize_to_one_meter,
|
|
17
|
+
convert_format_lat_lon,
|
|
18
|
+
)
|
|
19
|
+
from ..heights import (
|
|
20
|
+
extract_building_heights_from_geotiff,
|
|
21
|
+
extract_building_heights_from_gdf,
|
|
22
|
+
complement_building_heights_from_gdf,
|
|
23
|
+
)
|
|
24
|
+
from ..overlap import (
|
|
25
|
+
process_building_footprints_by_overlap,
|
|
26
|
+
)
|
|
27
|
+
from ...downloader.gee import (
|
|
28
|
+
get_roi,
|
|
29
|
+
save_geotiff_open_buildings_temporal,
|
|
30
|
+
)
|
|
31
|
+
from .core import calculate_grid_size, create_cell_polygon
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def create_building_height_grid_from_gdf_polygon(
|
|
35
|
+
gdf: gpd.GeoDataFrame,
|
|
36
|
+
meshsize: float,
|
|
37
|
+
rectangle_vertices: List[Tuple[float, float]],
|
|
38
|
+
overlapping_footprint: any = "auto",
|
|
39
|
+
gdf_comp: Optional[gpd.GeoDataFrame] = None,
|
|
40
|
+
geotiff_path_comp: Optional[str] = None,
|
|
41
|
+
complement_building_footprints: Optional[bool] = None,
|
|
42
|
+
complement_height: Optional[float] = None
|
|
43
|
+
):
|
|
44
|
+
"""
|
|
45
|
+
Create a building height grid from GeoDataFrame data within a polygon boundary.
|
|
46
|
+
Returns: (building_height_grid, building_min_height_grid, building_id_grid, filtered_buildings)
|
|
47
|
+
"""
|
|
48
|
+
geod = initialize_geod()
|
|
49
|
+
vertex_0, vertex_1, vertex_3 = rectangle_vertices[0], rectangle_vertices[1], rectangle_vertices[3]
|
|
50
|
+
|
|
51
|
+
dist_side_1 = calculate_distance(geod, vertex_0[0], vertex_0[1], vertex_1[0], vertex_1[1])
|
|
52
|
+
dist_side_2 = calculate_distance(geod, vertex_0[0], vertex_0[1], vertex_3[0], vertex_3[1])
|
|
53
|
+
|
|
54
|
+
side_1 = np.array(vertex_1) - np.array(vertex_0)
|
|
55
|
+
side_2 = np.array(vertex_3) - np.array(vertex_0)
|
|
56
|
+
u_vec = normalize_to_one_meter(side_1, dist_side_1)
|
|
57
|
+
v_vec = normalize_to_one_meter(side_2, dist_side_2)
|
|
58
|
+
|
|
59
|
+
origin = np.array(rectangle_vertices[0])
|
|
60
|
+
grid_size, adjusted_meshsize = calculate_grid_size(side_1, side_2, u_vec, v_vec, meshsize)
|
|
61
|
+
|
|
62
|
+
extent = [
|
|
63
|
+
min(coord[1] for coord in rectangle_vertices),
|
|
64
|
+
max(coord[1] for coord in rectangle_vertices),
|
|
65
|
+
min(coord[0] for coord in rectangle_vertices),
|
|
66
|
+
max(coord[0] for coord in rectangle_vertices)
|
|
67
|
+
]
|
|
68
|
+
plotting_box = box(extent[2], extent[0], extent[3], extent[1])
|
|
69
|
+
filtered_gdf = gdf[gdf.geometry.intersects(plotting_box)].copy()
|
|
70
|
+
|
|
71
|
+
zero_height_count = len(filtered_gdf[filtered_gdf['height'] == 0])
|
|
72
|
+
nan_height_count = len(filtered_gdf[filtered_gdf['height'].isna()])
|
|
73
|
+
print(f"{zero_height_count+nan_height_count} of the total {len(filtered_gdf)} building footprint from the base data source did not have height data.")
|
|
74
|
+
|
|
75
|
+
if gdf_comp is not None:
|
|
76
|
+
filtered_gdf_comp = gdf_comp[gdf_comp.geometry.intersects(plotting_box)].copy()
|
|
77
|
+
if complement_building_footprints:
|
|
78
|
+
filtered_gdf = complement_building_heights_from_gdf(filtered_gdf, filtered_gdf_comp)
|
|
79
|
+
else:
|
|
80
|
+
filtered_gdf = extract_building_heights_from_gdf(filtered_gdf, filtered_gdf_comp)
|
|
81
|
+
elif geotiff_path_comp:
|
|
82
|
+
filtered_gdf = extract_building_heights_from_geotiff(geotiff_path_comp, filtered_gdf)
|
|
83
|
+
|
|
84
|
+
filtered_gdf = process_building_footprints_by_overlap(filtered_gdf, overlap_threshold=0.5)
|
|
85
|
+
|
|
86
|
+
mode = overlapping_footprint
|
|
87
|
+
if mode is None:
|
|
88
|
+
mode = "auto"
|
|
89
|
+
mode_norm = mode.strip().lower() if isinstance(mode, str) else mode
|
|
90
|
+
|
|
91
|
+
def _decide_auto_mode(gdf_in) -> bool:
|
|
92
|
+
try:
|
|
93
|
+
n_buildings = len(gdf_in)
|
|
94
|
+
if n_buildings == 0:
|
|
95
|
+
return False
|
|
96
|
+
num_cells = max(1, int(grid_size[0]) * int(grid_size[1]))
|
|
97
|
+
density = float(n_buildings) / float(num_cells)
|
|
98
|
+
|
|
99
|
+
sample_n = min(800, n_buildings)
|
|
100
|
+
idx_rt = index.Index()
|
|
101
|
+
geoms = []
|
|
102
|
+
areas = []
|
|
103
|
+
for i, geom in enumerate(gdf_in.geometry):
|
|
104
|
+
g = geom
|
|
105
|
+
if not getattr(g, "is_valid", True):
|
|
106
|
+
try:
|
|
107
|
+
g = g.buffer(0)
|
|
108
|
+
except Exception:
|
|
109
|
+
pass
|
|
110
|
+
geoms.append(g)
|
|
111
|
+
try:
|
|
112
|
+
areas.append(g.area)
|
|
113
|
+
except Exception:
|
|
114
|
+
areas.append(0.0)
|
|
115
|
+
try:
|
|
116
|
+
idx_rt.insert(i, g.bounds)
|
|
117
|
+
except Exception:
|
|
118
|
+
pass
|
|
119
|
+
with_overlap = 0
|
|
120
|
+
step = max(1, n_buildings // sample_n)
|
|
121
|
+
checked = 0
|
|
122
|
+
for i in range(0, n_buildings, step):
|
|
123
|
+
if checked >= sample_n:
|
|
124
|
+
break
|
|
125
|
+
gi = geoms[i]
|
|
126
|
+
ai = areas[i] if i < len(areas) else 0.0
|
|
127
|
+
if gi is None:
|
|
128
|
+
continue
|
|
129
|
+
try:
|
|
130
|
+
potentials = list(idx_rt.intersection(gi.bounds))
|
|
131
|
+
except Exception:
|
|
132
|
+
potentials = []
|
|
133
|
+
overlapped = False
|
|
134
|
+
for j in potentials:
|
|
135
|
+
if j == i or j >= len(geoms):
|
|
136
|
+
continue
|
|
137
|
+
gj = geoms[j]
|
|
138
|
+
if gj is None:
|
|
139
|
+
continue
|
|
140
|
+
try:
|
|
141
|
+
if gi.intersects(gj):
|
|
142
|
+
inter = gi.intersection(gj)
|
|
143
|
+
inter_area = getattr(inter, "area", 0.0)
|
|
144
|
+
if inter_area > 0.0:
|
|
145
|
+
aj = areas[j] if j < len(areas) else 0.0
|
|
146
|
+
ref_area = max(1e-9, min(ai, aj) if ai > 0 and aj > 0 else (ai if ai > 0 else aj))
|
|
147
|
+
if (inter_area / ref_area) >= 0.2:
|
|
148
|
+
overlapped = True
|
|
149
|
+
break
|
|
150
|
+
except Exception:
|
|
151
|
+
continue
|
|
152
|
+
if overlapped:
|
|
153
|
+
with_overlap += 1
|
|
154
|
+
checked += 1
|
|
155
|
+
overlap_ratio = (with_overlap / checked) if checked > 0 else 0.0
|
|
156
|
+
if overlap_ratio >= 0.15:
|
|
157
|
+
return True
|
|
158
|
+
if overlap_ratio >= 0.08 and density > 0.15:
|
|
159
|
+
return True
|
|
160
|
+
if n_buildings <= 200 and overlap_ratio >= 0.05:
|
|
161
|
+
return True
|
|
162
|
+
return False
|
|
163
|
+
except Exception:
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
if mode_norm == "auto":
|
|
167
|
+
use_precise = _decide_auto_mode(filtered_gdf)
|
|
168
|
+
elif mode_norm is True:
|
|
169
|
+
use_precise = True
|
|
170
|
+
else:
|
|
171
|
+
use_precise = False
|
|
172
|
+
|
|
173
|
+
if use_precise:
|
|
174
|
+
return _process_with_geometry_intersection(
|
|
175
|
+
filtered_gdf, grid_size, adjusted_meshsize, origin, u_vec, v_vec, complement_height
|
|
176
|
+
)
|
|
177
|
+
return _process_with_rasterio(
|
|
178
|
+
filtered_gdf, grid_size, adjusted_meshsize, origin, u_vec, v_vec,
|
|
179
|
+
rectangle_vertices, complement_height
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _process_with_geometry_intersection(filtered_gdf, grid_size, adjusted_meshsize, origin, u_vec, v_vec, complement_height):
|
|
184
|
+
building_height_grid = np.zeros(grid_size)
|
|
185
|
+
building_id_grid = np.zeros(grid_size)
|
|
186
|
+
building_min_height_grid = np.empty(grid_size, dtype=object)
|
|
187
|
+
for i in range(grid_size[0]):
|
|
188
|
+
for j in range(grid_size[1]):
|
|
189
|
+
building_min_height_grid[i, j] = []
|
|
190
|
+
|
|
191
|
+
building_polygons = []
|
|
192
|
+
for idx_b, row in filtered_gdf.iterrows():
|
|
193
|
+
polygon = row.geometry
|
|
194
|
+
height = row.get('height', None)
|
|
195
|
+
if complement_height is not None and (height == 0 or height is None):
|
|
196
|
+
height = complement_height
|
|
197
|
+
min_height = row.get('min_height', 0)
|
|
198
|
+
if pd.isna(min_height):
|
|
199
|
+
min_height = 0
|
|
200
|
+
is_inner = row.get('is_inner', False)
|
|
201
|
+
feature_id = row.get('id', idx_b)
|
|
202
|
+
if not polygon.is_valid:
|
|
203
|
+
try:
|
|
204
|
+
polygon = polygon.buffer(0)
|
|
205
|
+
if not polygon.is_valid:
|
|
206
|
+
polygon = polygon.simplify(1e-8)
|
|
207
|
+
except Exception:
|
|
208
|
+
pass
|
|
209
|
+
bounding_box = polygon.bounds
|
|
210
|
+
building_polygons.append((
|
|
211
|
+
polygon, bounding_box, height, min_height, is_inner, feature_id
|
|
212
|
+
))
|
|
213
|
+
|
|
214
|
+
idx = index.Index()
|
|
215
|
+
for i_b, (poly, bbox, _, _, _, _) in enumerate(building_polygons):
|
|
216
|
+
idx.insert(i_b, bbox)
|
|
217
|
+
|
|
218
|
+
INTERSECTION_THRESHOLD = 0.3
|
|
219
|
+
for i in range(grid_size[0]):
|
|
220
|
+
for j in range(grid_size[1]):
|
|
221
|
+
cell = create_cell_polygon(origin, i, j, adjusted_meshsize, u_vec, v_vec)
|
|
222
|
+
if not cell.is_valid:
|
|
223
|
+
cell = cell.buffer(0)
|
|
224
|
+
cell_area = cell.area
|
|
225
|
+
potential = list(idx.intersection(cell.bounds))
|
|
226
|
+
if not potential:
|
|
227
|
+
continue
|
|
228
|
+
cell_buildings = []
|
|
229
|
+
for k in potential:
|
|
230
|
+
bpoly, bbox, height, minh, inr, fid = building_polygons[k]
|
|
231
|
+
sort_val = height if (height is not None) else -float('inf')
|
|
232
|
+
cell_buildings.append((k, bpoly, bbox, height, minh, inr, fid, sort_val))
|
|
233
|
+
cell_buildings.sort(key=lambda x: x[-1], reverse=True)
|
|
234
|
+
|
|
235
|
+
found_intersection = False
|
|
236
|
+
all_zero_or_nan = True
|
|
237
|
+
for (k, polygon, bbox, height, min_height, is_inner, feature_id, _) in cell_buildings:
|
|
238
|
+
try:
|
|
239
|
+
minx_p, miny_p, maxx_p, maxy_p = bbox
|
|
240
|
+
minx_c, miny_c, maxx_c, maxy_c = cell.bounds
|
|
241
|
+
overlap_minx = max(minx_p, minx_c)
|
|
242
|
+
overlap_miny = max(miny_p, miny_c)
|
|
243
|
+
overlap_maxx = min(maxx_p, maxx_c)
|
|
244
|
+
overlap_maxy = min(maxy_p, maxy_c)
|
|
245
|
+
if (overlap_maxx <= overlap_minx) or (overlap_maxy <= overlap_miny):
|
|
246
|
+
continue
|
|
247
|
+
bbox_intersect_area = (overlap_maxx - overlap_minx) * (overlap_maxy - overlap_miny)
|
|
248
|
+
if bbox_intersect_area < INTERSECTION_THRESHOLD * cell_area:
|
|
249
|
+
continue
|
|
250
|
+
if not polygon.is_valid:
|
|
251
|
+
polygon = polygon.buffer(0)
|
|
252
|
+
if cell.intersects(polygon):
|
|
253
|
+
intersection = cell.intersection(polygon)
|
|
254
|
+
inter_area = intersection.area
|
|
255
|
+
if (inter_area / cell_area) > INTERSECTION_THRESHOLD:
|
|
256
|
+
found_intersection = True
|
|
257
|
+
if not is_inner:
|
|
258
|
+
building_min_height_grid[i, j].append([min_height, height])
|
|
259
|
+
building_id_grid[i, j] = feature_id
|
|
260
|
+
if (height is not None and not np.isnan(height) and height > 0):
|
|
261
|
+
all_zero_or_nan = False
|
|
262
|
+
current_height = building_height_grid[i, j]
|
|
263
|
+
if (current_height == 0 or np.isnan(current_height) or current_height < height):
|
|
264
|
+
building_height_grid[i, j] = height
|
|
265
|
+
else:
|
|
266
|
+
building_min_height_grid[i, j] = [[0, 0]]
|
|
267
|
+
building_height_grid[i, j] = 0
|
|
268
|
+
found_intersection = True
|
|
269
|
+
all_zero_or_nan = False
|
|
270
|
+
break
|
|
271
|
+
except (GEOSException, ValueError):
|
|
272
|
+
try:
|
|
273
|
+
simplified_polygon = polygon.simplify(1e-8)
|
|
274
|
+
if simplified_polygon.is_valid:
|
|
275
|
+
intersection = cell.intersection(simplified_polygon)
|
|
276
|
+
inter_area = intersection.area
|
|
277
|
+
if (inter_area / cell_area) > INTERSECTION_THRESHOLD:
|
|
278
|
+
found_intersection = True
|
|
279
|
+
if not is_inner:
|
|
280
|
+
building_min_height_grid[i, j].append([min_height, height])
|
|
281
|
+
building_id_grid[i, j] = feature_id
|
|
282
|
+
if (height is not None and not np.isnan(height) and height > 0):
|
|
283
|
+
all_zero_or_nan = False
|
|
284
|
+
if (building_height_grid[i, j] == 0 or
|
|
285
|
+
np.isnan(building_height_grid[i, j]) or
|
|
286
|
+
building_height_grid[i, j] < height):
|
|
287
|
+
building_height_grid[i, j] = height
|
|
288
|
+
else:
|
|
289
|
+
building_min_height_grid[i, j] = [[0, 0]]
|
|
290
|
+
building_height_grid[i, j] = 0
|
|
291
|
+
found_intersection = True
|
|
292
|
+
all_zero_or_nan = False
|
|
293
|
+
break
|
|
294
|
+
except Exception:
|
|
295
|
+
continue
|
|
296
|
+
if found_intersection and all_zero_or_nan:
|
|
297
|
+
building_height_grid[i, j] = np.nan
|
|
298
|
+
|
|
299
|
+
return building_height_grid, building_min_height_grid, building_id_grid, filtered_gdf
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _process_with_rasterio(filtered_gdf, grid_size, adjusted_meshsize, origin, u_vec, v_vec, rectangle_vertices, complement_height):
|
|
303
|
+
u_step = adjusted_meshsize[0] * u_vec
|
|
304
|
+
v_step = adjusted_meshsize[1] * v_vec
|
|
305
|
+
top_left = origin + grid_size[1] * v_step
|
|
306
|
+
transform = Affine(u_step[0], -v_step[0], top_left[0],
|
|
307
|
+
u_step[1], -v_step[1], top_left[1])
|
|
308
|
+
|
|
309
|
+
filtered_gdf = filtered_gdf.copy()
|
|
310
|
+
if complement_height is not None:
|
|
311
|
+
mask = (filtered_gdf['height'] == 0) | (filtered_gdf['height'].isna())
|
|
312
|
+
filtered_gdf.loc[mask, 'height'] = complement_height
|
|
313
|
+
|
|
314
|
+
filtered_gdf['min_height'] = 0
|
|
315
|
+
if 'is_inner' not in filtered_gdf.columns:
|
|
316
|
+
filtered_gdf['is_inner'] = False
|
|
317
|
+
else:
|
|
318
|
+
try:
|
|
319
|
+
filtered_gdf['is_inner'] = filtered_gdf['is_inner'].fillna(False).astype(bool)
|
|
320
|
+
except Exception:
|
|
321
|
+
filtered_gdf['is_inner'] = False
|
|
322
|
+
if 'id' not in filtered_gdf.columns:
|
|
323
|
+
filtered_gdf['id'] = range(len(filtered_gdf))
|
|
324
|
+
|
|
325
|
+
regular_buildings = filtered_gdf[~filtered_gdf['is_inner']].copy()
|
|
326
|
+
regular_buildings = regular_buildings.sort_values('height', ascending=True, na_position='first')
|
|
327
|
+
|
|
328
|
+
height_raster = np.zeros((grid_size[1], grid_size[0]), dtype=np.float64)
|
|
329
|
+
id_raster = np.zeros((grid_size[1], grid_size[0]), dtype=np.float64)
|
|
330
|
+
|
|
331
|
+
if len(regular_buildings) > 0:
|
|
332
|
+
valid_buildings = regular_buildings[regular_buildings.geometry.is_valid].copy()
|
|
333
|
+
if len(valid_buildings) > 0:
|
|
334
|
+
height_shapes = [(mapping(geom), height) for geom, height in
|
|
335
|
+
zip(valid_buildings.geometry, valid_buildings['height'])
|
|
336
|
+
if pd.notna(height) and height > 0]
|
|
337
|
+
if height_shapes:
|
|
338
|
+
height_raster = features.rasterize(
|
|
339
|
+
height_shapes,
|
|
340
|
+
out_shape=(grid_size[1], grid_size[0]),
|
|
341
|
+
transform=transform,
|
|
342
|
+
fill=0,
|
|
343
|
+
dtype=np.float64
|
|
344
|
+
)
|
|
345
|
+
id_shapes = [(mapping(geom), id_val) for geom, id_val in
|
|
346
|
+
zip(valid_buildings.geometry, valid_buildings['id'])]
|
|
347
|
+
if id_shapes:
|
|
348
|
+
id_raster = features.rasterize(
|
|
349
|
+
id_shapes,
|
|
350
|
+
out_shape=(grid_size[1], grid_size[0]),
|
|
351
|
+
transform=transform,
|
|
352
|
+
fill=0,
|
|
353
|
+
dtype=np.float64
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
inner_buildings = filtered_gdf[filtered_gdf['is_inner']].copy()
|
|
357
|
+
if len(inner_buildings) > 0:
|
|
358
|
+
inner_shapes = [(mapping(geom), 1) for geom in inner_buildings.geometry if geom.is_valid]
|
|
359
|
+
if inner_shapes:
|
|
360
|
+
inner_mask = features.rasterize(
|
|
361
|
+
inner_shapes,
|
|
362
|
+
out_shape=(grid_size[1], grid_size[0]),
|
|
363
|
+
transform=transform,
|
|
364
|
+
fill=0,
|
|
365
|
+
dtype=np.uint8
|
|
366
|
+
)
|
|
367
|
+
height_raster[inner_mask > 0] = 0
|
|
368
|
+
id_raster[inner_mask > 0] = 0
|
|
369
|
+
|
|
370
|
+
building_min_height_grid = np.empty(grid_size, dtype=object)
|
|
371
|
+
min_heights_raster = np.zeros((grid_size[1], grid_size[0]), dtype=np.float64)
|
|
372
|
+
if len(regular_buildings) > 0:
|
|
373
|
+
valid_buildings = regular_buildings[regular_buildings.geometry.is_valid].copy()
|
|
374
|
+
if len(valid_buildings) > 0:
|
|
375
|
+
min_height_shapes = [(mapping(geom), min_h) for geom, min_h in
|
|
376
|
+
zip(valid_buildings.geometry, valid_buildings['min_height'])
|
|
377
|
+
if pd.notna(min_h)]
|
|
378
|
+
if min_height_shapes:
|
|
379
|
+
min_heights_raster = features.rasterize(
|
|
380
|
+
min_height_shapes,
|
|
381
|
+
out_shape=(grid_size[1], grid_size[0]),
|
|
382
|
+
transform=transform,
|
|
383
|
+
fill=0,
|
|
384
|
+
dtype=np.float64
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
building_height_grid = np.flipud(height_raster).T
|
|
388
|
+
building_id_grid = np.flipud(id_raster).T
|
|
389
|
+
min_heights = np.flipud(min_heights_raster).T
|
|
390
|
+
|
|
391
|
+
for i in range(grid_size[0]):
|
|
392
|
+
for j in range(grid_size[1]):
|
|
393
|
+
if building_height_grid[i, j] > 0:
|
|
394
|
+
building_min_height_grid[i, j] = [[min_heights[i, j], building_height_grid[i, j]]]
|
|
395
|
+
else:
|
|
396
|
+
building_min_height_grid[i, j] = []
|
|
397
|
+
|
|
398
|
+
return building_height_grid, building_min_height_grid, building_id_grid, filtered_gdf
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def create_building_height_grid_from_open_building_temporal_polygon(meshsize, rectangle_vertices, output_dir):
|
|
402
|
+
"""
|
|
403
|
+
Create a building height grid from OpenBuildings temporal data within a polygon.
|
|
404
|
+
"""
|
|
405
|
+
roi = get_roi(rectangle_vertices)
|
|
406
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
407
|
+
geotiff_path = os.path.join(output_dir, "building_height.tif")
|
|
408
|
+
save_geotiff_open_buildings_temporal(roi, geotiff_path)
|
|
409
|
+
from .raster import create_height_grid_from_geotiff_polygon
|
|
410
|
+
building_height_grid = create_height_grid_from_geotiff_polygon(geotiff_path, meshsize, rectangle_vertices)
|
|
411
|
+
|
|
412
|
+
building_min_height_grid = np.empty(building_height_grid.shape, dtype=object)
|
|
413
|
+
for i in range(building_height_grid.shape[0]):
|
|
414
|
+
for j in range(building_height_grid.shape[1]):
|
|
415
|
+
if building_height_grid[i, j] <= 0:
|
|
416
|
+
building_min_height_grid[i, j] = []
|
|
417
|
+
else:
|
|
418
|
+
building_min_height_grid[i, j] = [[0, building_height_grid[i, j]]]
|
|
419
|
+
|
|
420
|
+
filtered_buildings = gpd.GeoDataFrame()
|
|
421
|
+
building_id_grid = np.zeros_like(building_height_grid, dtype=int)
|
|
422
|
+
non_zero_positions = np.nonzero(building_height_grid)
|
|
423
|
+
sequence = np.arange(1, len(non_zero_positions[0]) + 1)
|
|
424
|
+
building_id_grid[non_zero_positions] = sequence
|
|
425
|
+
|
|
426
|
+
return building_height_grid, building_min_height_grid, building_id_grid, filtered_buildings
|
|
427
|
+
|
|
428
|
+
|