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.
- {voxcity-0.3.8 → voxcity-0.3.10}/PKG-INFO +5 -4
- {voxcity-0.3.8 → voxcity-0.3.10}/README.md +2 -2
- {voxcity-0.3.8 → voxcity-0.3.10}/pyproject.toml +2 -1
- {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/file/geojson.py +306 -99
- voxcity-0.3.10/src/voxcity/geo/network.py +505 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/sim/solar.py +12 -7
- {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/sim/view.py +5 -3
- {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/voxcity.py +23 -20
- {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity.egg-info/PKG-INFO +5 -4
- {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity.egg-info/requires.txt +1 -0
- voxcity-0.3.8/src/voxcity/geo/network.py +0 -194
- {voxcity-0.3.8 → voxcity-0.3.10}/AUTHORS.rst +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/CONTRIBUTING.rst +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/HISTORY.rst +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/LICENSE +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/MANIFEST.in +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/docs/Makefile +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/docs/archive/README.rst +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/docs/authors.rst +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/docs/conf.py +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/docs/index.rst +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/docs/make.bat +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/setup.cfg +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/__init__.py +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/download/__init__.py +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/download/eubucco.py +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/download/gee.py +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/download/mbfp.py +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/download/oemj.py +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/download/omt.py +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/download/osm.py +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/download/overture.py +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/download/utils.py +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/file/__init_.py +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/file/envimet.py +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/file/magicavoxel.py +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/file/obj.py +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/geo/__init_.py +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/geo/draw.py +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/geo/grid.py +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/geo/utils.py +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/sim/__init_.py +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/sim/utils.py +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/utils/__init_.py +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/utils/lc.py +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/utils/material.py +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/utils/visualization.py +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity/utils/weather.py +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity.egg-info/SOURCES.txt +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity.egg-info/dependency_links.txt +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/src/voxcity.egg-info/top_level.txt +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/tests/__init__.py +0 -0
- {voxcity-0.3.8 → voxcity-0.3.10}/tests/voxelcity.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: voxcity
|
|
3
|
-
Version: 0.3.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
262
|
+
# updated_geojson_data_0.append(feature)
|
|
263
263
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
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
|
"""
|