voxcity 0.3.8__tar.gz → 0.3.10__tar.gz

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.

Potentially problematic release.


This version of voxcity might be problematic. Click here for more details.

Files changed (53) hide show
  1. {voxcity-0.3.8 → voxcity-0.3.10}/PKG-INFO +5 -4
  2. {voxcity-0.3.8 → voxcity-0.3.10}/README.md +2 -2
  3. {voxcity-0.3.8 → voxcity-0.3.10}/pyproject.toml +2 -1
  4. {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/file/geojson.py +306 -99
  5. voxcity-0.3.10/src/voxcity/geo/network.py +505 -0
  6. {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/sim/solar.py +12 -7
  7. {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/sim/view.py +5 -3
  8. {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/voxcity.py +23 -20
  9. {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity.egg-info/PKG-INFO +5 -4
  10. {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity.egg-info/requires.txt +1 -0
  11. voxcity-0.3.8/src/voxcity/geo/network.py +0 -194
  12. {voxcity-0.3.8 → voxcity-0.3.10}/AUTHORS.rst +0 -0
  13. {voxcity-0.3.8 → voxcity-0.3.10}/CONTRIBUTING.rst +0 -0
  14. {voxcity-0.3.8 → voxcity-0.3.10}/HISTORY.rst +0 -0
  15. {voxcity-0.3.8 → voxcity-0.3.10}/LICENSE +0 -0
  16. {voxcity-0.3.8 → voxcity-0.3.10}/MANIFEST.in +0 -0
  17. {voxcity-0.3.8 → voxcity-0.3.10}/docs/Makefile +0 -0
  18. {voxcity-0.3.8 → voxcity-0.3.10}/docs/archive/README.rst +0 -0
  19. {voxcity-0.3.8 → voxcity-0.3.10}/docs/authors.rst +0 -0
  20. {voxcity-0.3.8 → voxcity-0.3.10}/docs/conf.py +0 -0
  21. {voxcity-0.3.8 → voxcity-0.3.10}/docs/index.rst +0 -0
  22. {voxcity-0.3.8 → voxcity-0.3.10}/docs/make.bat +0 -0
  23. {voxcity-0.3.8 → voxcity-0.3.10}/setup.cfg +0 -0
  24. {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/__init__.py +0 -0
  25. {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/download/__init__.py +0 -0
  26. {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/download/eubucco.py +0 -0
  27. {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/download/gee.py +0 -0
  28. {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/download/mbfp.py +0 -0
  29. {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/download/oemj.py +0 -0
  30. {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/download/omt.py +0 -0
  31. {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/download/osm.py +0 -0
  32. {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/download/overture.py +0 -0
  33. {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/download/utils.py +0 -0
  34. {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/file/__init_.py +0 -0
  35. {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/file/envimet.py +0 -0
  36. {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/file/magicavoxel.py +0 -0
  37. {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/file/obj.py +0 -0
  38. {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/geo/__init_.py +0 -0
  39. {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/geo/draw.py +0 -0
  40. {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/geo/grid.py +0 -0
  41. {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/geo/utils.py +0 -0
  42. {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/sim/__init_.py +0 -0
  43. {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/sim/utils.py +0 -0
  44. {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/utils/__init_.py +0 -0
  45. {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/utils/lc.py +0 -0
  46. {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/utils/material.py +0 -0
  47. {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/utils/visualization.py +0 -0
  48. {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/utils/weather.py +0 -0
  49. {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity.egg-info/SOURCES.txt +0 -0
  50. {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity.egg-info/dependency_links.txt +0 -0
  51. {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity.egg-info/top_level.txt +0 -0
  52. {voxcity-0.3.8 → voxcity-0.3.10}/tests/__init__.py +0 -0
  53. {voxcity-0.3.8 → voxcity-0.3.10}/tests/voxelcity.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: voxcity
3
- Version: 0.3.8
3
+ Version: 0.3.10
4
4
  Summary: voxcity is an easy and one-stop tool to output 3d city models for microclimate simulation by integrating multiple geospatial open-data
5
5
  Author-email: Kunihiko Fujiwara <kunihiko@nus.edu.sg>
6
6
  Maintainer-email: Kunihiko Fujiwara <kunihiko@nus.edu.sg>
@@ -49,6 +49,7 @@ Requires-Dist: protobuf==3.20.3
49
49
  Requires-Dist: timezonefinder
50
50
  Requires-Dist: astral
51
51
  Requires-Dist: osmnx
52
+ Requires-Dist: joblib
52
53
  Provides-Extra: dev
53
54
  Requires-Dist: coverage; extra == "dev"
54
55
  Requires-Dist: mypy; extra == "dev"
@@ -65,7 +66,7 @@ Requires-Dist: ruff; extra == "dev"
65
66
 
66
67
  # VoxCity
67
68
 
68
- **VoxCity** is a Python package that facilitates the creation of voxel-based 3D urban environments and related geospatial analyses. It integrates various geospatial datasets—such as building footprints, land cover, canopy height, and digital elevation models (DEMs)—to generate 2D and 3D representations of urban areas. It can export data in formats compatible with popular simulation tools like ENVI-MET, as well as visualization tools like MagicaVoxel, and supports simulations such as sky view index and green view index calculations.
69
+ **VoxCity** is a Python package that provides a one-stop solution for grid-based 3D city model generation and urban simulation for cities worldwide. It integrates various geospatial datasets—such as building footprints, land cover, canopy height, and digital elevation models (DEMs)—to generate 2D and 3D representations of urban areas. It can export data in formats compatible with popular simulation tools like ENVI-MET, as well as visualization tools like MagicaVoxel, and supports simulations such as sky view index and green view index calculations.
69
70
 
70
71
  <!-- <p align="center">
71
72
  <picture>
@@ -283,7 +284,7 @@ The generated OBJ files can be opened and rendered in the following 3D visualiza
283
284
  <img src="https://raw.githubusercontent.com/kunifujiwara/VoxCity/main/images/obj.png" alt="OBJ 3D City Model Rendered in Rhino" width="600">
284
285
  </p>
285
286
  <p align="center">
286
- <em>Example Output Exported in OBJ and Rendered in MagicaVoxel</em>
287
+ <em>Example Output Exported in OBJ and Rendered in Rhino</em>
287
288
  </p>
288
289
 
289
290
  #### MagicaVoxel VOX Files:
@@ -8,7 +8,7 @@
8
8
 
9
9
  # VoxCity
10
10
 
11
- **VoxCity** is a Python package that facilitates the creation of voxel-based 3D urban environments and related geospatial analyses. It integrates various geospatial datasets—such as building footprints, land cover, canopy height, and digital elevation models (DEMs)—to generate 2D and 3D representations of urban areas. It can export data in formats compatible with popular simulation tools like ENVI-MET, as well as visualization tools like MagicaVoxel, and supports simulations such as sky view index and green view index calculations.
11
+ **VoxCity** is a Python package that provides a one-stop solution for grid-based 3D city model generation and urban simulation for cities worldwide. It integrates various geospatial datasets—such as building footprints, land cover, canopy height, and digital elevation models (DEMs)—to generate 2D and 3D representations of urban areas. It can export data in formats compatible with popular simulation tools like ENVI-MET, as well as visualization tools like MagicaVoxel, and supports simulations such as sky view index and green view index calculations.
12
12
 
13
13
  <!-- <p align="center">
14
14
  <picture>
@@ -226,7 +226,7 @@ The generated OBJ files can be opened and rendered in the following 3D visualiza
226
226
  <img src="https://raw.githubusercontent.com/kunifujiwara/VoxCity/main/images/obj.png" alt="OBJ 3D City Model Rendered in Rhino" width="600">
227
227
  </p>
228
228
  <p align="center">
229
- <em>Example Output Exported in OBJ and Rendered in MagicaVoxel</em>
229
+ <em>Example Output Exported in OBJ and Rendered in Rhino</em>
230
230
  </p>
231
231
 
232
232
  #### MagicaVoxel VOX Files:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "voxcity"
3
- version = "0.3.8"
3
+ version = "0.3.10"
4
4
  requires-python = ">=3.10,<3.13"
5
5
  classifiers = [
6
6
  "Programming Language :: Python :: 3.10",
@@ -51,6 +51,7 @@ dependencies = [
51
51
  "timezonefinder",
52
52
  "astral",
53
53
  "osmnx",
54
+ "joblib",
54
55
  ]
55
56
 
56
57
  [project.optional-dependencies]
@@ -187,118 +187,325 @@ def extract_building_heights_from_geojson(geojson_data_0: List[Dict], geojson_da
187
187
 
188
188
  return updated_geojson_data_0
189
189
 
190
- from typing import List, Dict
191
- from shapely.geometry import shape
192
- from shapely.errors import GEOSException
193
- import numpy as np
194
-
195
- def complement_building_heights_from_geojson(geojson_data_0: List[Dict], geojson_data_1: List[Dict]) -> List[Dict]:
196
- """
197
- Complement building heights in one GeoJSON dataset with data from another and add non-intersecting buildings.
190
+ # from typing import List, Dict
191
+ # from shapely.geometry import shape
192
+ # from shapely.errors import GEOSException
193
+ # import numpy as np
194
+
195
+ # def complement_building_heights_from_geojson(geojson_data_0: List[Dict], geojson_data_1: List[Dict]) -> List[Dict]:
196
+ # """
197
+ # Complement building heights in one GeoJSON dataset with data from another and add non-intersecting buildings.
198
198
 
199
- Args:
200
- geojson_data_0 (List[Dict]): Primary GeoJSON features to update with heights
201
- geojson_data_1 (List[Dict]): Reference GeoJSON features containing height data
199
+ # Args:
200
+ # geojson_data_0 (List[Dict]): Primary GeoJSON features to update with heights
201
+ # geojson_data_1 (List[Dict]): Reference GeoJSON features containing height data
202
202
 
203
- Returns:
204
- List[Dict]: Updated GeoJSON features with complemented heights and additional buildings
205
- """
206
- # Convert primary dataset to Shapely polygons for intersection checking
207
- existing_buildings = []
208
- for feature in geojson_data_0:
209
- geom = shape(feature['geometry'])
210
- existing_buildings.append(geom)
203
+ # Returns:
204
+ # List[Dict]: Updated GeoJSON features with complemented heights and additional buildings
205
+ # """
206
+ # # Convert primary dataset to Shapely polygons for intersection checking
207
+ # existing_buildings = []
208
+ # for feature in geojson_data_0:
209
+ # geom = shape(feature['geometry'])
210
+ # existing_buildings.append(geom)
211
211
 
212
- # Convert reference dataset to Shapely polygons with height info
213
- reference_buildings = []
214
- for feature in geojson_data_1:
215
- geom = shape(feature['geometry'])
216
- height = feature['properties']['height']
217
- reference_buildings.append((geom, height, feature))
212
+ # # Convert reference dataset to Shapely polygons with height info
213
+ # reference_buildings = []
214
+ # for feature in geojson_data_1:
215
+ # geom = shape(feature['geometry'])
216
+ # height = feature['properties']['height']
217
+ # reference_buildings.append((geom, height, feature))
218
218
 
219
- # Initialize counters for statistics
220
- count_0 = 0 # Buildings without height
221
- count_1 = 0 # Buildings updated with height
222
- count_2 = 0 # Buildings with no height data found
223
- count_3 = 0 # New non-intersecting buildings added
219
+ # # Initialize counters for statistics
220
+ # count_0 = 0 # Buildings without height
221
+ # count_1 = 0 # Buildings updated with height
222
+ # count_2 = 0 # Buildings with no height data found
223
+ # count_3 = 0 # New non-intersecting buildings added
224
224
 
225
- # Process primary dataset and update heights where needed
226
- updated_geojson_data_0 = []
227
- for feature in geojson_data_0:
228
- geom = shape(feature['geometry'])
229
- height = feature['properties']['height']
230
- if height == 0:
231
- count_0 += 1
232
- # Calculate weighted average height based on overlapping areas
233
- overlapping_height_area = 0
234
- overlapping_area = 0
235
- for ref_geom, ref_height, _ in reference_buildings:
236
- try:
237
- if geom.intersects(ref_geom):
238
- overlap_area = geom.intersection(ref_geom).area
239
- overlapping_height_area += ref_height * overlap_area
240
- overlapping_area += overlap_area
241
- except GEOSException as e:
242
- # Try to fix invalid geometries
243
- try:
244
- fixed_ref_geom = ref_geom.buffer(0)
245
- if geom.intersects(fixed_ref_geom):
246
- overlap_area = geom.intersection(ref_geom).area
247
- overlapping_height_area += ref_height * overlap_area
248
- overlapping_area += overlap_area
249
- except Exception as fix_error:
250
- print(f"Failed to fix polygon")
251
- continue
225
+ # # Process primary dataset and update heights where needed
226
+ # updated_geojson_data_0 = []
227
+ # for feature in geojson_data_0:
228
+ # geom = shape(feature['geometry'])
229
+ # height = feature['properties']['height']
230
+ # if height == 0:
231
+ # count_0 += 1
232
+ # # Calculate weighted average height based on overlapping areas
233
+ # overlapping_height_area = 0
234
+ # overlapping_area = 0
235
+ # for ref_geom, ref_height, _ in reference_buildings:
236
+ # try:
237
+ # if geom.intersects(ref_geom):
238
+ # overlap_area = geom.intersection(ref_geom).area
239
+ # overlapping_height_area += ref_height * overlap_area
240
+ # overlapping_area += overlap_area
241
+ # except GEOSException as e:
242
+ # # Try to fix invalid geometries
243
+ # try:
244
+ # fixed_ref_geom = ref_geom.buffer(0)
245
+ # if geom.intersects(fixed_ref_geom):
246
+ # overlap_area = geom.intersection(ref_geom).area
247
+ # overlapping_height_area += ref_height * overlap_area
248
+ # overlapping_area += overlap_area
249
+ # except Exception as fix_error:
250
+ # print(f"Failed to fix polygon")
251
+ # continue
252
252
 
253
- # Update height if overlapping buildings found
254
- if overlapping_height_area > 0:
255
- count_1 += 1
256
- new_height = overlapping_height_area / overlapping_area
257
- feature['properties']['height'] = new_height
258
- else:
259
- count_2 += 1
260
- feature['properties']['height'] = np.nan
253
+ # # Update height if overlapping buildings found
254
+ # if overlapping_height_area > 0:
255
+ # count_1 += 1
256
+ # new_height = overlapping_height_area / overlapping_area
257
+ # feature['properties']['height'] = new_height
258
+ # else:
259
+ # count_2 += 1
260
+ # feature['properties']['height'] = np.nan
261
261
 
262
- updated_geojson_data_0.append(feature)
262
+ # updated_geojson_data_0.append(feature)
263
263
 
264
- # Add non-intersecting buildings from reference dataset
265
- for ref_geom, ref_height, ref_feature in reference_buildings:
266
- has_intersection = False
267
- try:
268
- # Check if reference building intersects with any existing building
269
- for existing_geom in existing_buildings:
270
- if ref_geom.intersects(existing_geom):
271
- has_intersection = True
272
- break
264
+ # # Add non-intersecting buildings from reference dataset
265
+ # for ref_geom, ref_height, ref_feature in reference_buildings:
266
+ # has_intersection = False
267
+ # try:
268
+ # # Check if reference building intersects with any existing building
269
+ # for existing_geom in existing_buildings:
270
+ # if ref_geom.intersects(existing_geom):
271
+ # has_intersection = True
272
+ # break
273
273
 
274
- # Add building if it doesn't intersect with any existing ones
275
- if not has_intersection:
276
- updated_geojson_data_0.append(ref_feature)
277
- count_3 += 1
274
+ # # Add building if it doesn't intersect with any existing ones
275
+ # if not has_intersection:
276
+ # updated_geojson_data_0.append(ref_feature)
277
+ # count_3 += 1
278
278
 
279
- except GEOSException as e:
280
- # Try to fix invalid geometries
281
- try:
282
- fixed_ref_geom = ref_geom.buffer(0)
283
- for existing_geom in existing_buildings:
284
- if fixed_ref_geom.intersects(existing_geom):
285
- has_intersection = True
286
- break
279
+ # except GEOSException as e:
280
+ # # Try to fix invalid geometries
281
+ # try:
282
+ # fixed_ref_geom = ref_geom.buffer(0)
283
+ # for existing_geom in existing_buildings:
284
+ # if fixed_ref_geom.intersects(existing_geom):
285
+ # has_intersection = True
286
+ # break
287
287
 
288
- if not has_intersection:
289
- updated_geojson_data_0.append(ref_feature)
290
- count_3 += 1
291
- except Exception as fix_error:
292
- print(f"Failed to process non-intersecting building")
293
- continue
288
+ # if not has_intersection:
289
+ # updated_geojson_data_0.append(ref_feature)
290
+ # count_3 += 1
291
+ # except Exception as fix_error:
292
+ # print(f"Failed to process non-intersecting building")
293
+ # continue
294
294
 
295
- # Print statistics about updates
296
- if count_0 > 0:
297
- print(f"{count_0} of the total {len(geojson_data_0)} building footprint from base source did not have height data.")
298
- print(f"For {count_1} of these building footprints without height, values from complement source were assigned.")
299
- print(f"{count_3} non-intersecting buildings from Microsoft Building Footprints were added to the output.")
295
+ # # Print statistics about updates
296
+ # if count_0 > 0:
297
+ # print(f"{count_0} of the total {len(geojson_data_0)} building footprint from base source did not have height data.")
298
+ # print(f"For {count_1} of these building footprints without height, values from complement source were assigned.")
299
+ # print(f"{count_3} non-intersecting buildings from Microsoft Building Footprints were added to the output.")
300
300
 
301
- return updated_geojson_data_0
301
+ # return updated_geojson_data_0
302
+
303
+ import numpy as np
304
+ import geopandas as gpd
305
+ import pandas as pd
306
+ from shapely.geometry import shape
307
+ from shapely.errors import GEOSException
308
+
309
+ def geojson_to_gdf(geojson_data, id_col='id'):
310
+ """
311
+ Convert a list of GeoJSON-like dict features into a GeoDataFrame.
312
+
313
+ Args:
314
+ geojson_data (List[Dict]): A list of feature dicts (Fiona-like).
315
+ id_col (str): Name of property to use as an identifier. If not found,
316
+ we'll try to create a unique ID.
317
+
318
+ Returns:
319
+ gpd.GeoDataFrame: GeoDataFrame with geometry and property columns.
320
+ """
321
+ # Build lists for geometry and properties
322
+ geometries = []
323
+ all_props = []
324
+
325
+ for i, feature in enumerate(geojson_data):
326
+ # Extract geometry
327
+ geom = feature.get('geometry')
328
+ shapely_geom = shape(geom) if geom else None
329
+
330
+ # Extract properties
331
+ props = feature.get('properties', {})
332
+
333
+ # If an ID column is missing, create one
334
+ if id_col not in props:
335
+ props[id_col] = i # fallback ID
336
+
337
+ # Capture geometry and all props
338
+ geometries.append(shapely_geom)
339
+ all_props.append(props)
340
+
341
+ gdf = gpd.GeoDataFrame(all_props, geometry=geometries, crs="EPSG:4326")
342
+ return gdf
343
+
344
+
345
+ def complement_building_heights_gdf(geojson_data_0, geojson_data_1,
346
+ primary_id='id', ref_id='id'):
347
+ """
348
+ Use a vectorized approach with GeoPandas to:
349
+ 1) Convert both datasets to GeoDataFrames
350
+ 2) Find intersections and compute weighted average heights
351
+ 3) Update heights in the primary dataset
352
+ 4) Add non-intersecting buildings from the reference dataset
353
+
354
+ Args:
355
+ geojson_data_0 (List[Dict]): Primary GeoJSON-like features
356
+ geojson_data_1 (List[Dict]): Reference GeoJSON-like features
357
+ primary_id (str): Name of the unique identifier in primary dataset's properties
358
+ ref_id (str): Name of the unique identifier in reference dataset's properties
359
+
360
+ Returns:
361
+ gpd.GeoDataFrame: Updated GeoDataFrame (including new buildings).
362
+ You can convert it back to a list of dict features if needed.
363
+ """
364
+ # ----------------------------------------------------------------
365
+ # 1) Convert primary and reference data to GeoDataFrames
366
+ # ----------------------------------------------------------------
367
+ gdf_primary = geojson_to_gdf(geojson_data_0, id_col=primary_id)
368
+ gdf_ref = geojson_to_gdf(geojson_data_1, id_col=ref_id)
369
+
370
+ # Ensure both are in the same CRS, e.g. EPSG:4326 or some projected CRS
371
+ # If needed, do something like:
372
+ # gdf_primary = gdf_primary.to_crs("EPSG:xxxx")
373
+ # gdf_ref = gdf_ref.to_crs("EPSG:xxxx")
374
+
375
+ # Make sure height columns exist
376
+ if 'height' not in gdf_primary.columns:
377
+ gdf_primary['height'] = 0.0
378
+ if 'height' not in gdf_ref.columns:
379
+ gdf_ref['height'] = 0.0
380
+
381
+ # ----------------------------------------------------------------
382
+ # 2) Intersection to compute areas for overlapping buildings
383
+ # ----------------------------------------------------------------
384
+ # We'll rename columns to avoid collision after overlay
385
+ gdf_primary = gdf_primary.rename(columns={'height': 'height_primary'})
386
+ gdf_ref = gdf_ref.rename(columns={'height': 'height_ref'})
387
+
388
+ # We perform an 'intersection' overlay to get the overlapping polygons
389
+ intersect_gdf = gpd.overlay(gdf_primary, gdf_ref, how='intersection')
390
+
391
+ # Compute intersection area
392
+ intersect_gdf['intersect_area'] = intersect_gdf.area
393
+ # Weighted area (height_ref * intersect_area)
394
+ intersect_gdf['height_area'] = intersect_gdf['height_ref'] * intersect_gdf['intersect_area']
395
+
396
+ # ----------------------------------------------------------------
397
+ # 3) Aggregate to get weighted average height for each primary building
398
+ # ----------------------------------------------------------------
399
+ # We group by the primary building ID, summing up the area and the 'height_area'
400
+ group_cols = {
401
+ 'height_area': 'sum',
402
+ 'intersect_area': 'sum'
403
+ }
404
+ grouped = intersect_gdf.groupby(gdf_primary[primary_id].name).agg(group_cols)
405
+
406
+ # Weighted average
407
+ grouped['weighted_height'] = grouped['height_area'] / grouped['intersect_area']
408
+
409
+ # ----------------------------------------------------------------
410
+ # 4) Merge aggregated results back to the primary GDF
411
+ # ----------------------------------------------------------------
412
+ # After merging, the primary GDF will have a column 'weighted_height'
413
+ gdf_primary = gdf_primary.merge(grouped['weighted_height'],
414
+ left_on=primary_id,
415
+ right_index=True,
416
+ how='left')
417
+
418
+ # Where primary had zero or missing height, we assign the new weighted height
419
+ zero_or_nan_mask = (gdf_primary['height_primary'] == 0) | (gdf_primary['height_primary'].isna())
420
+ gdf_primary.loc[zero_or_nan_mask, 'height_primary'] = gdf_primary.loc[zero_or_nan_mask, 'weighted_height']
421
+
422
+ # For any building that had no overlap, 'weighted_height' might be NaN.
423
+ # Keep it as NaN or set to 0 if you prefer:
424
+ gdf_primary['height_primary'] = gdf_primary['height_primary'].fillna(np.nan)
425
+
426
+ # ----------------------------------------------------------------
427
+ # 5) Identify reference buildings that do not intersect any primary building
428
+ # ----------------------------------------------------------------
429
+ # Another overlay or spatial join can do this:
430
+ # Option A: use 'difference' on reference to get non-overlapping parts, but that can chop polygons.
431
+ # Option B: check building-level intersection. We'll do a bounding test with sjoin.
432
+
433
+ # For building-level intersection, do a left join of ref onto primary.
434
+ # Then we'll identify which reference IDs are missing from the intersection result.
435
+ sjoin_gdf = gpd.sjoin(gdf_ref, gdf_primary, how='left', op='intersects')
436
+
437
+ # All reference buildings that did not intersect any primary building
438
+ non_intersect_ids = sjoin_gdf.loc[sjoin_gdf[primary_id].isna(), ref_id].unique()
439
+
440
+ # Extract them from the original reference GDF
441
+ gdf_ref_non_intersect = gdf_ref[gdf_ref[ref_id].isin(non_intersect_ids)]
442
+
443
+ # We'll rename columns back to 'height' to be consistent
444
+ gdf_ref_non_intersect = gdf_ref_non_intersect.rename(columns={'height_ref': 'height'})
445
+
446
+ # Also rename any other properties you prefer. For clarity, keep an ID so you know they came from reference.
447
+
448
+ # ----------------------------------------------------------------
449
+ # 6) Combine the updated primary GDF with the new reference buildings
450
+ # ----------------------------------------------------------------
451
+ # First, rename columns in updated primary GDF
452
+ gdf_primary = gdf_primary.rename(columns={'height_primary': 'height'})
453
+ # Drop the 'weighted_height' column to clean up
454
+ if 'weighted_height' in gdf_primary.columns:
455
+ gdf_primary.drop(columns='weighted_height', inplace=True)
456
+
457
+ # Concatenate
458
+ final_gdf = pd.concat([gdf_primary, gdf_ref_non_intersect], ignore_index=True)
459
+
460
+ # ----------------------------------------------------------------
461
+ # Return the combined GeoDataFrame
462
+ # (You can convert it back to a list of GeoJSON-like dictionaries)
463
+ # ----------------------------------------------------------------
464
+ return final_gdf
465
+
466
+
467
+ def gdf_to_geojson_dicts(gdf, id_col='id'):
468
+ """
469
+ Convert a GeoDataFrame to a list of dicts similar to GeoJSON features.
470
+ """
471
+ records = gdf.to_dict(orient='records')
472
+ features = []
473
+ for rec in records:
474
+ # geometry is separate
475
+ geom = rec.pop('geometry', None)
476
+ if geom is not None:
477
+ geom = geom.__geo_interface__
478
+ # use or set ID
479
+ feature_id = rec.get(id_col, None)
480
+ props = {k: v for k, v in rec.items() if k != id_col}
481
+ # build GeoJSON-like feature dict
482
+ feature = {
483
+ 'type': 'Feature',
484
+ 'properties': props,
485
+ 'geometry': geom
486
+ }
487
+ features.append(feature)
488
+
489
+ return features
490
+
491
+
492
+ def complement_building_heights_from_geojson(geojson_data_0, geojson_data_1,
493
+ primary_id='id', ref_id='id'):
494
+ """
495
+ High-level function that wraps the GeoPandas approach end-to-end.
496
+ Returns a list of GeoJSON-like feature dicts.
497
+ """
498
+ # 1) Complement building heights using the GeoDataFrame approach
499
+ final_gdf = complement_building_heights_gdf(
500
+ geojson_data_0,
501
+ geojson_data_1,
502
+ primary_id=primary_id,
503
+ ref_id=ref_id
504
+ )
505
+
506
+ # 2) Convert back to geojson-like dict format
507
+ updated_features = gdf_to_geojson_dicts(final_gdf, id_col=primary_id)
508
+ return updated_features
302
509
 
303
510
  def load_geojsons_from_multiple_gz(file_paths):
304
511
  """