voxcity 0.6.15__py3-none-any.whl → 0.7.0__py3-none-any.whl

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