voxcity 0.7.0__py3-none-any.whl → 1.0.13__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.
Files changed (81) hide show
  1. voxcity/__init__.py +14 -14
  2. voxcity/downloader/ocean.py +559 -0
  3. voxcity/exporter/__init__.py +12 -12
  4. voxcity/exporter/cityles.py +633 -633
  5. voxcity/exporter/envimet.py +733 -728
  6. voxcity/exporter/magicavoxel.py +333 -333
  7. voxcity/exporter/netcdf.py +238 -238
  8. voxcity/exporter/obj.py +1480 -1480
  9. voxcity/generator/__init__.py +47 -44
  10. voxcity/generator/api.py +727 -675
  11. voxcity/generator/grids.py +394 -379
  12. voxcity/generator/io.py +94 -94
  13. voxcity/generator/pipeline.py +582 -282
  14. voxcity/generator/update.py +429 -0
  15. voxcity/generator/voxelizer.py +18 -6
  16. voxcity/geoprocessor/__init__.py +75 -75
  17. voxcity/geoprocessor/draw.py +1494 -1219
  18. voxcity/geoprocessor/merge_utils.py +91 -91
  19. voxcity/geoprocessor/mesh.py +806 -806
  20. voxcity/geoprocessor/network.py +708 -708
  21. voxcity/geoprocessor/raster/__init__.py +2 -0
  22. voxcity/geoprocessor/raster/buildings.py +435 -428
  23. voxcity/geoprocessor/raster/core.py +31 -0
  24. voxcity/geoprocessor/raster/export.py +93 -93
  25. voxcity/geoprocessor/raster/landcover.py +178 -51
  26. voxcity/geoprocessor/raster/raster.py +1 -1
  27. voxcity/geoprocessor/utils.py +824 -824
  28. voxcity/models.py +115 -113
  29. voxcity/simulator/solar/__init__.py +66 -43
  30. voxcity/simulator/solar/integration.py +336 -336
  31. voxcity/simulator/solar/sky.py +668 -0
  32. voxcity/simulator/solar/temporal.py +792 -434
  33. voxcity/simulator_gpu/__init__.py +115 -0
  34. voxcity/simulator_gpu/common/__init__.py +9 -0
  35. voxcity/simulator_gpu/common/geometry.py +11 -0
  36. voxcity/simulator_gpu/core.py +322 -0
  37. voxcity/simulator_gpu/domain.py +262 -0
  38. voxcity/simulator_gpu/environment.yml +11 -0
  39. voxcity/simulator_gpu/init_taichi.py +154 -0
  40. voxcity/simulator_gpu/integration.py +15 -0
  41. voxcity/simulator_gpu/kernels.py +56 -0
  42. voxcity/simulator_gpu/radiation.py +28 -0
  43. voxcity/simulator_gpu/raytracing.py +623 -0
  44. voxcity/simulator_gpu/sky.py +9 -0
  45. voxcity/simulator_gpu/solar/__init__.py +178 -0
  46. voxcity/simulator_gpu/solar/core.py +66 -0
  47. voxcity/simulator_gpu/solar/csf.py +1249 -0
  48. voxcity/simulator_gpu/solar/domain.py +561 -0
  49. voxcity/simulator_gpu/solar/epw.py +421 -0
  50. voxcity/simulator_gpu/solar/integration.py +2953 -0
  51. voxcity/simulator_gpu/solar/radiation.py +3019 -0
  52. voxcity/simulator_gpu/solar/raytracing.py +686 -0
  53. voxcity/simulator_gpu/solar/reflection.py +533 -0
  54. voxcity/simulator_gpu/solar/sky.py +907 -0
  55. voxcity/simulator_gpu/solar/solar.py +337 -0
  56. voxcity/simulator_gpu/solar/svf.py +446 -0
  57. voxcity/simulator_gpu/solar/volumetric.py +1151 -0
  58. voxcity/simulator_gpu/solar/voxcity.py +2953 -0
  59. voxcity/simulator_gpu/temporal.py +13 -0
  60. voxcity/simulator_gpu/utils.py +25 -0
  61. voxcity/simulator_gpu/view.py +32 -0
  62. voxcity/simulator_gpu/visibility/__init__.py +109 -0
  63. voxcity/simulator_gpu/visibility/geometry.py +278 -0
  64. voxcity/simulator_gpu/visibility/integration.py +808 -0
  65. voxcity/simulator_gpu/visibility/landmark.py +753 -0
  66. voxcity/simulator_gpu/visibility/view.py +944 -0
  67. voxcity/utils/__init__.py +11 -0
  68. voxcity/utils/classes.py +194 -0
  69. voxcity/utils/lc.py +80 -39
  70. voxcity/utils/shape.py +230 -0
  71. voxcity/visualizer/__init__.py +24 -24
  72. voxcity/visualizer/builder.py +43 -43
  73. voxcity/visualizer/grids.py +141 -141
  74. voxcity/visualizer/maps.py +187 -187
  75. voxcity/visualizer/renderer.py +1146 -928
  76. {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/METADATA +56 -52
  77. voxcity-1.0.13.dist-info/RECORD +116 -0
  78. voxcity-0.7.0.dist-info/RECORD +0 -77
  79. {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/WHEEL +0 -0
  80. {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/licenses/AUTHORS.rst +0 -0
  81. {voxcity-0.7.0.dist-info → voxcity-1.0.13.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
- 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
-
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
+