voxcity 0.7.0__py3-none-any.whl → 1.0.2__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 -14
- voxcity/exporter/__init__.py +12 -12
- voxcity/exporter/cityles.py +633 -633
- voxcity/exporter/envimet.py +733 -728
- voxcity/exporter/magicavoxel.py +333 -333
- voxcity/exporter/netcdf.py +238 -238
- voxcity/exporter/obj.py +1480 -1480
- voxcity/generator/__init__.py +47 -44
- voxcity/generator/api.py +721 -675
- voxcity/generator/grids.py +381 -379
- voxcity/generator/io.py +94 -94
- voxcity/generator/pipeline.py +282 -282
- voxcity/generator/update.py +429 -0
- voxcity/generator/voxelizer.py +18 -6
- voxcity/geoprocessor/__init__.py +75 -75
- voxcity/geoprocessor/draw.py +1488 -1219
- voxcity/geoprocessor/merge_utils.py +91 -91
- voxcity/geoprocessor/mesh.py +806 -806
- voxcity/geoprocessor/network.py +708 -708
- voxcity/geoprocessor/raster/buildings.py +435 -428
- voxcity/geoprocessor/raster/export.py +93 -93
- voxcity/geoprocessor/raster/landcover.py +5 -2
- voxcity/geoprocessor/utils.py +824 -824
- voxcity/models.py +113 -113
- voxcity/simulator/solar/__init__.py +66 -43
- voxcity/simulator/solar/integration.py +336 -336
- voxcity/simulator/solar/sky.py +668 -0
- voxcity/simulator/solar/temporal.py +792 -434
- voxcity/utils/__init__.py +11 -0
- voxcity/utils/classes.py +194 -0
- voxcity/utils/lc.py +80 -39
- voxcity/utils/shape.py +230 -0
- voxcity/visualizer/__init__.py +24 -24
- voxcity/visualizer/builder.py +43 -43
- voxcity/visualizer/grids.py +141 -141
- voxcity/visualizer/maps.py +187 -187
- voxcity/visualizer/renderer.py +1145 -928
- {voxcity-0.7.0.dist-info → voxcity-1.0.2.dist-info}/METADATA +90 -49
- {voxcity-0.7.0.dist-info → voxcity-1.0.2.dist-info}/RECORD +42 -38
- {voxcity-0.7.0.dist-info → voxcity-1.0.2.dist-info}/WHEEL +0 -0
- {voxcity-0.7.0.dist-info → voxcity-1.0.2.dist-info}/licenses/AUTHORS.rst +0 -0
- {voxcity-0.7.0.dist-info → voxcity-1.0.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,428 +1,435 @@
|
|
|
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
|
-
|
|
202
|
-
if
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if
|
|
249
|
-
continue
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if '
|
|
323
|
-
filtered_gdf['
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
+
# Fix: Handle NaN values for is_inner (NaN is truthy, causing buildings to be skipped)
|
|
202
|
+
if pd.isna(is_inner):
|
|
203
|
+
is_inner = False
|
|
204
|
+
feature_id = row.get('id', idx_b)
|
|
205
|
+
if not polygon.is_valid:
|
|
206
|
+
try:
|
|
207
|
+
polygon = polygon.buffer(0)
|
|
208
|
+
if not polygon.is_valid:
|
|
209
|
+
polygon = polygon.simplify(1e-8)
|
|
210
|
+
except Exception:
|
|
211
|
+
pass
|
|
212
|
+
bounding_box = polygon.bounds
|
|
213
|
+
building_polygons.append((
|
|
214
|
+
polygon, bounding_box, height, min_height, is_inner, feature_id
|
|
215
|
+
))
|
|
216
|
+
|
|
217
|
+
idx = index.Index()
|
|
218
|
+
for i_b, (poly, bbox, _, _, _, _) in enumerate(building_polygons):
|
|
219
|
+
idx.insert(i_b, bbox)
|
|
220
|
+
|
|
221
|
+
INTERSECTION_THRESHOLD = 0.3
|
|
222
|
+
for i in range(grid_size[0]):
|
|
223
|
+
for j in range(grid_size[1]):
|
|
224
|
+
cell = create_cell_polygon(origin, i, j, adjusted_meshsize, u_vec, v_vec)
|
|
225
|
+
if not cell.is_valid:
|
|
226
|
+
cell = cell.buffer(0)
|
|
227
|
+
cell_area = cell.area
|
|
228
|
+
potential = list(idx.intersection(cell.bounds))
|
|
229
|
+
if not potential:
|
|
230
|
+
continue
|
|
231
|
+
cell_buildings = []
|
|
232
|
+
for k in potential:
|
|
233
|
+
bpoly, bbox, height, minh, inr, fid = building_polygons[k]
|
|
234
|
+
sort_val = height if (height is not None) else -float('inf')
|
|
235
|
+
cell_buildings.append((k, bpoly, bbox, height, minh, inr, fid, sort_val))
|
|
236
|
+
cell_buildings.sort(key=lambda x: x[-1], reverse=True)
|
|
237
|
+
|
|
238
|
+
found_intersection = False
|
|
239
|
+
all_zero_or_nan = True
|
|
240
|
+
for (k, polygon, bbox, height, min_height, is_inner, feature_id, _) in cell_buildings:
|
|
241
|
+
try:
|
|
242
|
+
minx_p, miny_p, maxx_p, maxy_p = bbox
|
|
243
|
+
minx_c, miny_c, maxx_c, maxy_c = cell.bounds
|
|
244
|
+
overlap_minx = max(minx_p, minx_c)
|
|
245
|
+
overlap_miny = max(miny_p, miny_c)
|
|
246
|
+
overlap_maxx = min(maxx_p, maxx_c)
|
|
247
|
+
overlap_maxy = min(maxy_p, maxy_c)
|
|
248
|
+
if (overlap_maxx <= overlap_minx) or (overlap_maxy <= overlap_miny):
|
|
249
|
+
continue
|
|
250
|
+
bbox_intersect_area = (overlap_maxx - overlap_minx) * (overlap_maxy - overlap_miny)
|
|
251
|
+
if bbox_intersect_area < INTERSECTION_THRESHOLD * cell_area:
|
|
252
|
+
continue
|
|
253
|
+
if not polygon.is_valid:
|
|
254
|
+
polygon = polygon.buffer(0)
|
|
255
|
+
if cell.intersects(polygon):
|
|
256
|
+
intersection = cell.intersection(polygon)
|
|
257
|
+
inter_area = intersection.area
|
|
258
|
+
if (inter_area / cell_area) > INTERSECTION_THRESHOLD:
|
|
259
|
+
found_intersection = True
|
|
260
|
+
if not is_inner:
|
|
261
|
+
building_min_height_grid[i, j].append([min_height, height])
|
|
262
|
+
building_id_grid[i, j] = feature_id
|
|
263
|
+
if (height is not None and not np.isnan(height) and height > 0):
|
|
264
|
+
all_zero_or_nan = False
|
|
265
|
+
current_height = building_height_grid[i, j]
|
|
266
|
+
if (current_height == 0 or np.isnan(current_height) or current_height < height):
|
|
267
|
+
building_height_grid[i, j] = height
|
|
268
|
+
else:
|
|
269
|
+
building_min_height_grid[i, j] = [[0, 0]]
|
|
270
|
+
building_height_grid[i, j] = 0
|
|
271
|
+
found_intersection = True
|
|
272
|
+
all_zero_or_nan = False
|
|
273
|
+
break
|
|
274
|
+
except (GEOSException, ValueError):
|
|
275
|
+
try:
|
|
276
|
+
simplified_polygon = polygon.simplify(1e-8)
|
|
277
|
+
if simplified_polygon.is_valid:
|
|
278
|
+
intersection = cell.intersection(simplified_polygon)
|
|
279
|
+
inter_area = intersection.area
|
|
280
|
+
if (inter_area / cell_area) > INTERSECTION_THRESHOLD:
|
|
281
|
+
found_intersection = True
|
|
282
|
+
if not is_inner:
|
|
283
|
+
building_min_height_grid[i, j].append([min_height, height])
|
|
284
|
+
building_id_grid[i, j] = feature_id
|
|
285
|
+
if (height is not None and not np.isnan(height) and height > 0):
|
|
286
|
+
all_zero_or_nan = False
|
|
287
|
+
if (building_height_grid[i, j] == 0 or
|
|
288
|
+
np.isnan(building_height_grid[i, j]) or
|
|
289
|
+
building_height_grid[i, j] < height):
|
|
290
|
+
building_height_grid[i, j] = height
|
|
291
|
+
else:
|
|
292
|
+
building_min_height_grid[i, j] = [[0, 0]]
|
|
293
|
+
building_height_grid[i, j] = 0
|
|
294
|
+
found_intersection = True
|
|
295
|
+
all_zero_or_nan = False
|
|
296
|
+
break
|
|
297
|
+
except Exception:
|
|
298
|
+
continue
|
|
299
|
+
if found_intersection and all_zero_or_nan:
|
|
300
|
+
building_height_grid[i, j] = np.nan
|
|
301
|
+
|
|
302
|
+
return building_height_grid, building_min_height_grid, building_id_grid, filtered_gdf
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _process_with_rasterio(filtered_gdf, grid_size, adjusted_meshsize, origin, u_vec, v_vec, rectangle_vertices, complement_height):
|
|
306
|
+
u_step = adjusted_meshsize[0] * u_vec
|
|
307
|
+
v_step = adjusted_meshsize[1] * v_vec
|
|
308
|
+
top_left = origin + grid_size[1] * v_step
|
|
309
|
+
transform = Affine(u_step[0], -v_step[0], top_left[0],
|
|
310
|
+
u_step[1], -v_step[1], top_left[1])
|
|
311
|
+
|
|
312
|
+
filtered_gdf = filtered_gdf.copy()
|
|
313
|
+
if complement_height is not None:
|
|
314
|
+
mask = (filtered_gdf['height'] == 0) | (filtered_gdf['height'].isna())
|
|
315
|
+
filtered_gdf.loc[mask, 'height'] = complement_height
|
|
316
|
+
|
|
317
|
+
# Preserve existing min_height values; only set default for missing/NaN
|
|
318
|
+
if 'min_height' not in filtered_gdf.columns:
|
|
319
|
+
filtered_gdf['min_height'] = 0
|
|
320
|
+
else:
|
|
321
|
+
filtered_gdf['min_height'] = filtered_gdf['min_height'].fillna(0)
|
|
322
|
+
if 'is_inner' not in filtered_gdf.columns:
|
|
323
|
+
filtered_gdf['is_inner'] = False
|
|
324
|
+
else:
|
|
325
|
+
try:
|
|
326
|
+
filtered_gdf['is_inner'] = filtered_gdf['is_inner'].fillna(False).astype(bool)
|
|
327
|
+
except Exception:
|
|
328
|
+
filtered_gdf['is_inner'] = False
|
|
329
|
+
if 'id' not in filtered_gdf.columns:
|
|
330
|
+
filtered_gdf['id'] = range(len(filtered_gdf))
|
|
331
|
+
|
|
332
|
+
regular_buildings = filtered_gdf[~filtered_gdf['is_inner']].copy()
|
|
333
|
+
regular_buildings = regular_buildings.sort_values('height', ascending=True, na_position='first')
|
|
334
|
+
|
|
335
|
+
height_raster = np.zeros((grid_size[1], grid_size[0]), dtype=np.float64)
|
|
336
|
+
id_raster = np.zeros((grid_size[1], grid_size[0]), dtype=np.float64)
|
|
337
|
+
|
|
338
|
+
if len(regular_buildings) > 0:
|
|
339
|
+
valid_buildings = regular_buildings[regular_buildings.geometry.is_valid].copy()
|
|
340
|
+
if len(valid_buildings) > 0:
|
|
341
|
+
height_shapes = [(mapping(geom), height) for geom, height in
|
|
342
|
+
zip(valid_buildings.geometry, valid_buildings['height'])
|
|
343
|
+
if pd.notna(height) and height > 0]
|
|
344
|
+
if height_shapes:
|
|
345
|
+
height_raster = features.rasterize(
|
|
346
|
+
height_shapes,
|
|
347
|
+
out_shape=(grid_size[1], grid_size[0]),
|
|
348
|
+
transform=transform,
|
|
349
|
+
fill=0,
|
|
350
|
+
dtype=np.float64
|
|
351
|
+
)
|
|
352
|
+
id_shapes = [(mapping(geom), id_val) for geom, id_val in
|
|
353
|
+
zip(valid_buildings.geometry, valid_buildings['id'])]
|
|
354
|
+
if id_shapes:
|
|
355
|
+
id_raster = features.rasterize(
|
|
356
|
+
id_shapes,
|
|
357
|
+
out_shape=(grid_size[1], grid_size[0]),
|
|
358
|
+
transform=transform,
|
|
359
|
+
fill=0,
|
|
360
|
+
dtype=np.float64
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
inner_buildings = filtered_gdf[filtered_gdf['is_inner']].copy()
|
|
364
|
+
if len(inner_buildings) > 0:
|
|
365
|
+
inner_shapes = [(mapping(geom), 1) for geom in inner_buildings.geometry if geom.is_valid]
|
|
366
|
+
if inner_shapes:
|
|
367
|
+
inner_mask = features.rasterize(
|
|
368
|
+
inner_shapes,
|
|
369
|
+
out_shape=(grid_size[1], grid_size[0]),
|
|
370
|
+
transform=transform,
|
|
371
|
+
fill=0,
|
|
372
|
+
dtype=np.uint8
|
|
373
|
+
)
|
|
374
|
+
height_raster[inner_mask > 0] = 0
|
|
375
|
+
id_raster[inner_mask > 0] = 0
|
|
376
|
+
|
|
377
|
+
building_min_height_grid = np.empty(grid_size, dtype=object)
|
|
378
|
+
min_heights_raster = np.zeros((grid_size[1], grid_size[0]), dtype=np.float64)
|
|
379
|
+
if len(regular_buildings) > 0:
|
|
380
|
+
valid_buildings = regular_buildings[regular_buildings.geometry.is_valid].copy()
|
|
381
|
+
if len(valid_buildings) > 0:
|
|
382
|
+
min_height_shapes = [(mapping(geom), min_h) for geom, min_h in
|
|
383
|
+
zip(valid_buildings.geometry, valid_buildings['min_height'])
|
|
384
|
+
if pd.notna(min_h)]
|
|
385
|
+
if min_height_shapes:
|
|
386
|
+
min_heights_raster = features.rasterize(
|
|
387
|
+
min_height_shapes,
|
|
388
|
+
out_shape=(grid_size[1], grid_size[0]),
|
|
389
|
+
transform=transform,
|
|
390
|
+
fill=0,
|
|
391
|
+
dtype=np.float64
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
building_height_grid = np.flipud(height_raster).T
|
|
395
|
+
building_id_grid = np.flipud(id_raster).T
|
|
396
|
+
min_heights = np.flipud(min_heights_raster).T
|
|
397
|
+
|
|
398
|
+
for i in range(grid_size[0]):
|
|
399
|
+
for j in range(grid_size[1]):
|
|
400
|
+
if building_height_grid[i, j] > 0:
|
|
401
|
+
building_min_height_grid[i, j] = [[min_heights[i, j], building_height_grid[i, j]]]
|
|
402
|
+
else:
|
|
403
|
+
building_min_height_grid[i, j] = []
|
|
404
|
+
|
|
405
|
+
return building_height_grid, building_min_height_grid, building_id_grid, filtered_gdf
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def create_building_height_grid_from_open_building_temporal_polygon(meshsize, rectangle_vertices, output_dir):
|
|
409
|
+
"""
|
|
410
|
+
Create a building height grid from OpenBuildings temporal data within a polygon.
|
|
411
|
+
"""
|
|
412
|
+
roi = get_roi(rectangle_vertices)
|
|
413
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
414
|
+
geotiff_path = os.path.join(output_dir, "building_height.tif")
|
|
415
|
+
save_geotiff_open_buildings_temporal(roi, geotiff_path)
|
|
416
|
+
from .raster import create_height_grid_from_geotiff_polygon
|
|
417
|
+
building_height_grid = create_height_grid_from_geotiff_polygon(geotiff_path, meshsize, rectangle_vertices)
|
|
418
|
+
|
|
419
|
+
building_min_height_grid = np.empty(building_height_grid.shape, dtype=object)
|
|
420
|
+
for i in range(building_height_grid.shape[0]):
|
|
421
|
+
for j in range(building_height_grid.shape[1]):
|
|
422
|
+
if building_height_grid[i, j] <= 0:
|
|
423
|
+
building_min_height_grid[i, j] = []
|
|
424
|
+
else:
|
|
425
|
+
building_min_height_grid[i, j] = [[0, building_height_grid[i, j]]]
|
|
426
|
+
|
|
427
|
+
filtered_buildings = gpd.GeoDataFrame()
|
|
428
|
+
building_id_grid = np.zeros_like(building_height_grid, dtype=int)
|
|
429
|
+
non_zero_positions = np.nonzero(building_height_grid)
|
|
430
|
+
sequence = np.arange(1, len(non_zero_positions[0]) + 1)
|
|
431
|
+
building_id_grid[non_zero_positions] = sequence
|
|
432
|
+
|
|
433
|
+
return building_height_grid, building_min_height_grid, building_id_grid, filtered_buildings
|
|
434
|
+
|
|
435
|
+
|