voxcity 0.6.26__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.
Files changed (81) hide show
  1. voxcity/__init__.py +10 -4
  2. voxcity/downloader/__init__.py +2 -1
  3. voxcity/downloader/gba.py +210 -0
  4. voxcity/downloader/gee.py +5 -1
  5. voxcity/downloader/mbfp.py +1 -1
  6. voxcity/downloader/oemj.py +80 -8
  7. voxcity/downloader/utils.py +73 -73
  8. voxcity/errors.py +30 -0
  9. voxcity/exporter/__init__.py +9 -1
  10. voxcity/exporter/cityles.py +129 -34
  11. voxcity/exporter/envimet.py +51 -26
  12. voxcity/exporter/magicavoxel.py +42 -5
  13. voxcity/exporter/netcdf.py +27 -0
  14. voxcity/exporter/obj.py +103 -28
  15. voxcity/generator/__init__.py +47 -0
  16. voxcity/generator/api.py +721 -0
  17. voxcity/generator/grids.py +381 -0
  18. voxcity/generator/io.py +94 -0
  19. voxcity/generator/pipeline.py +282 -0
  20. voxcity/generator/update.py +429 -0
  21. voxcity/generator/voxelizer.py +392 -0
  22. voxcity/geoprocessor/__init__.py +75 -6
  23. voxcity/geoprocessor/conversion.py +153 -0
  24. voxcity/geoprocessor/draw.py +1488 -1169
  25. voxcity/geoprocessor/heights.py +199 -0
  26. voxcity/geoprocessor/io.py +101 -0
  27. voxcity/geoprocessor/merge_utils.py +91 -0
  28. voxcity/geoprocessor/mesh.py +26 -10
  29. voxcity/geoprocessor/network.py +35 -6
  30. voxcity/geoprocessor/overlap.py +84 -0
  31. voxcity/geoprocessor/raster/__init__.py +82 -0
  32. voxcity/geoprocessor/raster/buildings.py +435 -0
  33. voxcity/geoprocessor/raster/canopy.py +258 -0
  34. voxcity/geoprocessor/raster/core.py +150 -0
  35. voxcity/geoprocessor/raster/export.py +93 -0
  36. voxcity/geoprocessor/raster/landcover.py +159 -0
  37. voxcity/geoprocessor/raster/raster.py +110 -0
  38. voxcity/geoprocessor/selection.py +85 -0
  39. voxcity/geoprocessor/utils.py +824 -820
  40. voxcity/models.py +113 -0
  41. voxcity/simulator/common/__init__.py +22 -0
  42. voxcity/simulator/common/geometry.py +98 -0
  43. voxcity/simulator/common/raytracing.py +450 -0
  44. voxcity/simulator/solar/__init__.py +66 -0
  45. voxcity/simulator/solar/integration.py +336 -0
  46. voxcity/simulator/solar/kernels.py +62 -0
  47. voxcity/simulator/solar/radiation.py +648 -0
  48. voxcity/simulator/solar/sky.py +668 -0
  49. voxcity/simulator/solar/temporal.py +792 -0
  50. voxcity/simulator/view.py +36 -2286
  51. voxcity/simulator/visibility/__init__.py +29 -0
  52. voxcity/simulator/visibility/landmark.py +392 -0
  53. voxcity/simulator/visibility/view.py +508 -0
  54. voxcity/utils/__init__.py +11 -0
  55. voxcity/utils/classes.py +194 -0
  56. voxcity/utils/lc.py +80 -39
  57. voxcity/utils/logging.py +61 -0
  58. voxcity/utils/orientation.py +51 -0
  59. voxcity/utils/shape.py +230 -0
  60. voxcity/utils/weather/__init__.py +26 -0
  61. voxcity/utils/weather/epw.py +146 -0
  62. voxcity/utils/weather/files.py +36 -0
  63. voxcity/utils/weather/onebuilding.py +486 -0
  64. voxcity/visualizer/__init__.py +24 -0
  65. voxcity/visualizer/builder.py +43 -0
  66. voxcity/visualizer/grids.py +141 -0
  67. voxcity/visualizer/maps.py +187 -0
  68. voxcity/visualizer/palette.py +228 -0
  69. voxcity/visualizer/renderer.py +1145 -0
  70. {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/METADATA +162 -48
  71. voxcity-1.0.2.dist-info/RECORD +81 -0
  72. voxcity/generator.py +0 -1302
  73. voxcity/geoprocessor/grid.py +0 -1739
  74. voxcity/geoprocessor/polygon.py +0 -1344
  75. voxcity/simulator/solar.py +0 -2339
  76. voxcity/utils/visualization.py +0 -2849
  77. voxcity/utils/weather.py +0 -1038
  78. voxcity-0.6.26.dist-info/RECORD +0 -38
  79. {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/WHEEL +0 -0
  80. {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/licenses/AUTHORS.rst +0 -0
  81. {voxcity-0.6.26.dist-info → voxcity-1.0.2.dist-info}/licenses/LICENSE +0 -0
@@ -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,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
+ # 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
+