voxcity 0.3.13__py3-none-any.whl → 0.3.15__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.
Potentially problematic release.
This version of voxcity might be problematic. Click here for more details.
- voxcity/downloader/eubucco.py +20 -34
- voxcity/downloader/mbfp.py +4 -5
- voxcity/downloader/omt.py +11 -4
- voxcity/downloader/osm.py +31 -35
- voxcity/downloader/overture.py +7 -4
- voxcity/generator.py +31 -30
- voxcity/geoprocessor/draw.py +13 -21
- voxcity/geoprocessor/grid.py +286 -97
- voxcity/geoprocessor/polygon.py +145 -167
- voxcity/simulator/view.py +4 -5
- voxcity/utils/visualization.py +135 -0
- {voxcity-0.3.13.dist-info → voxcity-0.3.15.dist-info}/METADATA +3 -3
- {voxcity-0.3.13.dist-info → voxcity-0.3.15.dist-info}/RECORD +17 -17
- {voxcity-0.3.13.dist-info → voxcity-0.3.15.dist-info}/AUTHORS.rst +0 -0
- {voxcity-0.3.13.dist-info → voxcity-0.3.15.dist-info}/LICENSE +0 -0
- {voxcity-0.3.13.dist-info → voxcity-0.3.15.dist-info}/WHEEL +0 -0
- {voxcity-0.3.13.dist-info → voxcity-0.3.15.dist-info}/top_level.txt +0 -0
voxcity/geoprocessor/polygon.py
CHANGED
|
@@ -120,55 +120,72 @@ def get_geojson_from_gpkg(gpkg_path, rectangle_vertices):
|
|
|
120
120
|
geojson = filter_and_convert_gdf_to_geojson(gdf, rectangle_vertices)
|
|
121
121
|
return geojson
|
|
122
122
|
|
|
123
|
-
def
|
|
123
|
+
def extract_building_heights_from_gdf(gdf_0: gpd.GeoDataFrame, gdf_1: gpd.GeoDataFrame) -> gpd.GeoDataFrame:
|
|
124
124
|
"""
|
|
125
|
-
Extract building heights from one
|
|
125
|
+
Extract building heights from one GeoDataFrame and apply them to another based on spatial overlap.
|
|
126
126
|
|
|
127
127
|
Args:
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
gdf_0 (gpd.GeoDataFrame): Primary GeoDataFrame to update with heights
|
|
129
|
+
gdf_1 (gpd.GeoDataFrame): Reference GeoDataFrame containing height data
|
|
130
130
|
|
|
131
131
|
Returns:
|
|
132
|
-
|
|
132
|
+
gpd.GeoDataFrame: Updated primary GeoDataFrame with extracted heights
|
|
133
133
|
"""
|
|
134
|
-
#
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
134
|
+
# Make a copy of input GeoDataFrame to avoid modifying original
|
|
135
|
+
gdf_primary = gdf_0.copy()
|
|
136
|
+
gdf_ref = gdf_1.copy()
|
|
137
|
+
|
|
138
|
+
# Make sure height columns exist
|
|
139
|
+
if 'height' not in gdf_primary.columns:
|
|
140
|
+
gdf_primary['height'] = 0.0
|
|
141
|
+
if 'height' not in gdf_ref.columns:
|
|
142
|
+
gdf_ref['height'] = 0.0
|
|
140
143
|
|
|
141
144
|
# Initialize counters for statistics
|
|
142
145
|
count_0 = 0 # Buildings without height
|
|
143
146
|
count_1 = 0 # Buildings updated with height
|
|
144
147
|
count_2 = 0 # Buildings with no height data found
|
|
145
148
|
|
|
146
|
-
#
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
149
|
+
# Create spatial index for reference buildings
|
|
150
|
+
from rtree import index
|
|
151
|
+
spatial_index = index.Index()
|
|
152
|
+
for i, geom in enumerate(gdf_ref.geometry):
|
|
153
|
+
if geom.is_valid:
|
|
154
|
+
spatial_index.insert(i, geom.bounds)
|
|
155
|
+
|
|
156
|
+
# Process each building in primary dataset that needs height data
|
|
157
|
+
for idx_primary, row in gdf_primary.iterrows():
|
|
158
|
+
if row['height'] == 0:
|
|
159
|
+
count_0 += 1
|
|
160
|
+
geom = row.geometry
|
|
161
|
+
|
|
153
162
|
# Calculate weighted average height based on overlapping areas
|
|
154
163
|
overlapping_height_area = 0
|
|
155
164
|
overlapping_area = 0
|
|
156
|
-
|
|
165
|
+
|
|
166
|
+
# Get potential intersecting buildings using spatial index
|
|
167
|
+
potential_matches = list(spatial_index.intersection(geom.bounds))
|
|
168
|
+
|
|
169
|
+
# Check intersections with reference buildings
|
|
170
|
+
for ref_idx in potential_matches:
|
|
171
|
+
if ref_idx >= len(gdf_ref):
|
|
172
|
+
continue
|
|
173
|
+
|
|
174
|
+
ref_row = gdf_ref.iloc[ref_idx]
|
|
157
175
|
try:
|
|
158
|
-
if geom.intersects(
|
|
159
|
-
overlap_area = geom.intersection(
|
|
160
|
-
overlapping_height_area +=
|
|
176
|
+
if geom.intersects(ref_row.geometry):
|
|
177
|
+
overlap_area = geom.intersection(ref_row.geometry).area
|
|
178
|
+
overlapping_height_area += ref_row['height'] * overlap_area
|
|
161
179
|
overlapping_area += overlap_area
|
|
162
|
-
except GEOSException
|
|
180
|
+
except GEOSException:
|
|
163
181
|
# Try to fix invalid geometries using buffer(0)
|
|
164
|
-
print(f"GEOS error at a building polygon {ref_geom}")
|
|
165
182
|
try:
|
|
166
|
-
fixed_ref_geom =
|
|
183
|
+
fixed_ref_geom = ref_row.geometry.buffer(0)
|
|
167
184
|
if geom.intersects(fixed_ref_geom):
|
|
168
|
-
overlap_area = geom.intersection(
|
|
169
|
-
overlapping_height_area +=
|
|
185
|
+
overlap_area = geom.intersection(fixed_ref_geom).area
|
|
186
|
+
overlapping_height_area += ref_row['height'] * overlap_area
|
|
170
187
|
overlapping_area += overlap_area
|
|
171
|
-
except Exception
|
|
188
|
+
except Exception:
|
|
172
189
|
print(f"Failed to fix polygon")
|
|
173
190
|
continue
|
|
174
191
|
|
|
@@ -177,19 +194,17 @@ def extract_building_heights_from_geojson(geojson_data_0: List[Dict], geojson_da
|
|
|
177
194
|
count_1 += 1
|
|
178
195
|
# Calculate weighted average height
|
|
179
196
|
new_height = overlapping_height_area / overlapping_area
|
|
180
|
-
|
|
197
|
+
gdf_primary.at[idx_primary, 'height'] = new_height
|
|
181
198
|
else:
|
|
182
199
|
count_2 += 1
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
updated_geojson_data_0.append(feature)
|
|
200
|
+
gdf_primary.at[idx_primary, 'height'] = np.nan
|
|
186
201
|
|
|
187
202
|
# Print statistics about height updates
|
|
188
203
|
if count_0 > 0:
|
|
189
|
-
print(f"{count_0} of the total {len(
|
|
204
|
+
print(f"{count_0} of the total {len(gdf_primary)} building footprint from OSM did not have height data.")
|
|
190
205
|
print(f"For {count_1} of these building footprints without height, values from Microsoft Building Footprints were assigned.")
|
|
191
206
|
|
|
192
|
-
return
|
|
207
|
+
return gdf_primary
|
|
193
208
|
|
|
194
209
|
# from typing import List, Dict
|
|
195
210
|
# from shapely.geometry import shape
|
|
@@ -346,18 +361,17 @@ def geojson_to_gdf(geojson_data, id_col='id'):
|
|
|
346
361
|
return gdf
|
|
347
362
|
|
|
348
363
|
|
|
349
|
-
def
|
|
364
|
+
def complement_building_heights_from_gdf(gdf_0, gdf_1,
|
|
350
365
|
primary_id='id', ref_id='id'):
|
|
351
366
|
"""
|
|
352
367
|
Use a vectorized approach with GeoPandas to:
|
|
353
|
-
1)
|
|
354
|
-
2)
|
|
355
|
-
3)
|
|
356
|
-
4) Add non-intersecting buildings from the reference dataset
|
|
368
|
+
1) Find intersections and compute weighted average heights
|
|
369
|
+
2) Update heights in the primary dataset
|
|
370
|
+
3) Add non-intersecting buildings from the reference dataset
|
|
357
371
|
|
|
358
372
|
Args:
|
|
359
|
-
|
|
360
|
-
|
|
373
|
+
gdf_0 (gpd.GeoDataFrame): Primary GeoDataFrame
|
|
374
|
+
gdf_1 (gpd.GeoDataFrame): Reference GeoDataFrame
|
|
361
375
|
primary_id (str): Name of the unique identifier in primary dataset's properties
|
|
362
376
|
ref_id (str): Name of the unique identifier in reference dataset's properties
|
|
363
377
|
|
|
@@ -365,11 +379,9 @@ def complement_building_heights_gdf(geojson_data_0, geojson_data_1,
|
|
|
365
379
|
gpd.GeoDataFrame: Updated GeoDataFrame (including new buildings).
|
|
366
380
|
You can convert it back to a list of dict features if needed.
|
|
367
381
|
"""
|
|
368
|
-
#
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
gdf_primary = geojson_to_gdf(geojson_data_0, id_col=primary_id)
|
|
372
|
-
gdf_ref = geojson_to_gdf(geojson_data_1, id_col=ref_id)
|
|
382
|
+
# Make a copy of input GeoDataFrames to avoid modifying originals
|
|
383
|
+
gdf_primary = gdf_0.copy()
|
|
384
|
+
gdf_ref = gdf_1.copy()
|
|
373
385
|
|
|
374
386
|
# Ensure both are in the same CRS, e.g. EPSG:4326 or some projected CRS
|
|
375
387
|
# If needed, do something like:
|
|
@@ -383,7 +395,7 @@ def complement_building_heights_gdf(geojson_data_0, geojson_data_1,
|
|
|
383
395
|
gdf_ref['height'] = 0.0
|
|
384
396
|
|
|
385
397
|
# ----------------------------------------------------------------
|
|
386
|
-
#
|
|
398
|
+
# 1) Intersection to compute areas for overlapping buildings
|
|
387
399
|
# ----------------------------------------------------------------
|
|
388
400
|
# We'll rename columns to avoid collision after overlay
|
|
389
401
|
gdf_primary = gdf_primary.rename(columns={'height': 'height_primary'})
|
|
@@ -398,7 +410,7 @@ def complement_building_heights_gdf(geojson_data_0, geojson_data_1,
|
|
|
398
410
|
intersect_gdf['height_area'] = intersect_gdf['height_ref'] * intersect_gdf['intersect_area']
|
|
399
411
|
|
|
400
412
|
# ----------------------------------------------------------------
|
|
401
|
-
#
|
|
413
|
+
# 2) Aggregate to get weighted average height for each primary building
|
|
402
414
|
# ----------------------------------------------------------------
|
|
403
415
|
# We group by the primary building ID, summing up the area and the 'height_area'
|
|
404
416
|
group_cols = {
|
|
@@ -411,7 +423,7 @@ def complement_building_heights_gdf(geojson_data_0, geojson_data_1,
|
|
|
411
423
|
grouped['weighted_height'] = grouped['height_area'] / grouped['intersect_area']
|
|
412
424
|
|
|
413
425
|
# ----------------------------------------------------------------
|
|
414
|
-
#
|
|
426
|
+
# 3) Merge aggregated results back to the primary GDF
|
|
415
427
|
# ----------------------------------------------------------------
|
|
416
428
|
# After merging, the primary GDF will have a column 'weighted_height'
|
|
417
429
|
gdf_primary = gdf_primary.merge(grouped['weighted_height'],
|
|
@@ -428,7 +440,7 @@ def complement_building_heights_gdf(geojson_data_0, geojson_data_1,
|
|
|
428
440
|
gdf_primary['height_primary'] = gdf_primary['height_primary'].fillna(np.nan)
|
|
429
441
|
|
|
430
442
|
# ----------------------------------------------------------------
|
|
431
|
-
#
|
|
443
|
+
# 4) Identify reference buildings that do not intersect any primary building
|
|
432
444
|
# ----------------------------------------------------------------
|
|
433
445
|
# Another overlay or spatial join can do this:
|
|
434
446
|
# Option A: use 'difference' on reference to get non-overlapping parts, but that can chop polygons.
|
|
@@ -450,7 +462,7 @@ def complement_building_heights_gdf(geojson_data_0, geojson_data_1,
|
|
|
450
462
|
# Also rename any other properties you prefer. For clarity, keep an ID so you know they came from reference.
|
|
451
463
|
|
|
452
464
|
# ----------------------------------------------------------------
|
|
453
|
-
#
|
|
465
|
+
# 5) Combine the updated primary GDF with the new reference buildings
|
|
454
466
|
# ----------------------------------------------------------------
|
|
455
467
|
# First, rename columns in updated primary GDF
|
|
456
468
|
gdf_primary = gdf_primary.rename(columns={'height_primary': 'height'})
|
|
@@ -461,10 +473,6 @@ def complement_building_heights_gdf(geojson_data_0, geojson_data_1,
|
|
|
461
473
|
# Concatenate
|
|
462
474
|
final_gdf = pd.concat([gdf_primary, gdf_ref_non_intersect], ignore_index=True)
|
|
463
475
|
|
|
464
|
-
# ----------------------------------------------------------------
|
|
465
|
-
# Return the combined GeoDataFrame
|
|
466
|
-
# (You can convert it back to a list of GeoJSON-like dictionaries)
|
|
467
|
-
# ----------------------------------------------------------------
|
|
468
476
|
return final_gdf
|
|
469
477
|
|
|
470
478
|
|
|
@@ -492,34 +500,15 @@ def gdf_to_geojson_dicts(gdf, id_col='id'):
|
|
|
492
500
|
|
|
493
501
|
return features
|
|
494
502
|
|
|
495
|
-
|
|
496
|
-
def complement_building_heights_from_geojson(geojson_data_0, geojson_data_1,
|
|
497
|
-
primary_id='id', ref_id='id'):
|
|
498
|
-
"""
|
|
499
|
-
High-level function that wraps the GeoPandas approach end-to-end.
|
|
500
|
-
Returns a list of GeoJSON-like feature dicts.
|
|
503
|
+
def load_gdf_from_multiple_gz(file_paths):
|
|
501
504
|
"""
|
|
502
|
-
|
|
503
|
-
final_gdf = complement_building_heights_gdf(
|
|
504
|
-
geojson_data_0,
|
|
505
|
-
geojson_data_1,
|
|
506
|
-
primary_id=primary_id,
|
|
507
|
-
ref_id=ref_id
|
|
508
|
-
)
|
|
509
|
-
|
|
510
|
-
# 2) Convert back to geojson-like dict format
|
|
511
|
-
updated_features = gdf_to_geojson_dicts(final_gdf, id_col=primary_id)
|
|
512
|
-
return updated_features
|
|
513
|
-
|
|
514
|
-
def load_geojsons_from_multiple_gz(file_paths):
|
|
515
|
-
"""
|
|
516
|
-
Load GeoJSON features from multiple gzipped files.
|
|
505
|
+
Load GeoJSON features from multiple gzipped files into a GeoDataFrame.
|
|
517
506
|
|
|
518
507
|
Args:
|
|
519
508
|
file_paths (list): List of paths to gzipped GeoJSON files
|
|
520
509
|
|
|
521
510
|
Returns:
|
|
522
|
-
|
|
511
|
+
gpd.GeoDataFrame: GeoDataFrame containing features from all files
|
|
523
512
|
"""
|
|
524
513
|
geojson_objects = []
|
|
525
514
|
for gz_file_path in file_paths:
|
|
@@ -539,7 +528,15 @@ def load_geojsons_from_multiple_gz(file_paths):
|
|
|
539
528
|
geojson_objects.append(data)
|
|
540
529
|
except json.JSONDecodeError as e:
|
|
541
530
|
print(f"Skipping line in {gz_file_path} due to JSONDecodeError: {e}")
|
|
542
|
-
|
|
531
|
+
|
|
532
|
+
# Convert list of GeoJSON features to GeoDataFrame
|
|
533
|
+
# swap_coordinates(geojson_objects)
|
|
534
|
+
gdf = gpd.GeoDataFrame.from_features(geojson_objects)
|
|
535
|
+
|
|
536
|
+
# Set CRS to WGS84 which is typically used for these files
|
|
537
|
+
gdf.set_crs(epsg=4326, inplace=True)
|
|
538
|
+
|
|
539
|
+
return gdf
|
|
543
540
|
|
|
544
541
|
def filter_buildings(geojson_data, plotting_box):
|
|
545
542
|
"""
|
|
@@ -573,24 +570,19 @@ def filter_buildings(geojson_data, plotting_box):
|
|
|
573
570
|
print(f"Skipping feature due to geometry error: {e}")
|
|
574
571
|
return filtered_features
|
|
575
572
|
|
|
576
|
-
def extract_building_heights_from_geotiff(geotiff_path,
|
|
573
|
+
def extract_building_heights_from_geotiff(geotiff_path, gdf):
|
|
577
574
|
"""
|
|
578
|
-
Extract building heights from a GeoTIFF raster for
|
|
575
|
+
Extract building heights from a GeoTIFF raster for building footprints in a GeoDataFrame.
|
|
579
576
|
|
|
580
577
|
Args:
|
|
581
578
|
geotiff_path (str): Path to the GeoTIFF height raster
|
|
582
|
-
|
|
579
|
+
gdf (gpd.GeoDataFrame): GeoDataFrame containing building footprints
|
|
583
580
|
|
|
584
581
|
Returns:
|
|
585
|
-
|
|
582
|
+
gpd.GeoDataFrame: Updated GeoDataFrame with extracted heights
|
|
586
583
|
"""
|
|
587
|
-
#
|
|
588
|
-
|
|
589
|
-
geojson = json.loads(geojson_data)
|
|
590
|
-
input_was_string = True
|
|
591
|
-
else:
|
|
592
|
-
geojson = geojson_data
|
|
593
|
-
input_was_string = False
|
|
584
|
+
# Make a copy to avoid modifying the input
|
|
585
|
+
gdf = gdf.copy()
|
|
594
586
|
|
|
595
587
|
# Initialize counters for statistics
|
|
596
588
|
count_0 = 0 # Buildings without height
|
|
@@ -602,48 +594,43 @@ def extract_building_heights_from_geotiff(geotiff_path, geojson_data):
|
|
|
602
594
|
# Create coordinate transformer from WGS84 to raster CRS
|
|
603
595
|
transformer = Transformer.from_crs(CRS.from_epsg(4326), src.crs, always_xy=True)
|
|
604
596
|
|
|
605
|
-
#
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
597
|
+
# Filter buildings that need height processing
|
|
598
|
+
mask_condition = (gdf.geometry.geom_type == 'Polygon') & (gdf.get('height', 0) <= 0)
|
|
599
|
+
buildings_to_process = gdf[mask_condition]
|
|
600
|
+
count_0 = len(buildings_to_process)
|
|
601
|
+
|
|
602
|
+
for idx, row in buildings_to_process.iterrows():
|
|
603
|
+
# Transform geometry to raster CRS
|
|
604
|
+
coords = list(row.geometry.exterior.coords)
|
|
605
|
+
transformed_coords = [transformer.transform(lon, lat) for lon, lat in coords]
|
|
606
|
+
polygon = shape({"type": "Polygon", "coordinates": [transformed_coords]})
|
|
607
|
+
|
|
608
|
+
try:
|
|
609
|
+
# Extract height values from raster within polygon
|
|
610
|
+
masked_data, _ = rasterio.mask.mask(src, [polygon], crop=True, all_touched=True)
|
|
611
|
+
heights = masked_data[0][masked_data[0] != src.nodata]
|
|
615
612
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
count_2 += 1
|
|
628
|
-
feature['properties']['height'] = 10
|
|
629
|
-
print(f"No valid height data for feature: {feature['properties']}")
|
|
630
|
-
except ValueError as e:
|
|
631
|
-
print(f"Error processing feature: {feature['properties']}. Error: {str(e)}")
|
|
632
|
-
feature['properties']['extracted_height'] = None
|
|
613
|
+
# Calculate average height if valid samples exist
|
|
614
|
+
if len(heights) > 0:
|
|
615
|
+
count_1 += 1
|
|
616
|
+
gdf.at[idx, 'height'] = float(np.mean(heights))
|
|
617
|
+
else:
|
|
618
|
+
count_2 += 1
|
|
619
|
+
gdf.at[idx, 'height'] = 10
|
|
620
|
+
print(f"No valid height data for building at index {idx}")
|
|
621
|
+
except ValueError as e:
|
|
622
|
+
print(f"Error processing building at index {idx}. Error: {str(e)}")
|
|
623
|
+
gdf.at[idx, 'height'] = None
|
|
633
624
|
|
|
634
625
|
# Print statistics about height updates
|
|
635
626
|
if count_0 > 0:
|
|
636
|
-
print(f"{count_0} of the total {len(
|
|
627
|
+
print(f"{count_0} of the total {len(gdf)} building footprint from OSM did not have height data.")
|
|
637
628
|
print(f"For {count_1} of these building footprints without height, values from Open Building 2.5D Temporal were assigned.")
|
|
638
629
|
print(f"For {count_2} of these building footprints without height, no data exist in Open Building 2.5D Temporal. Height values of 10m were set instead")
|
|
639
630
|
|
|
640
|
-
|
|
641
|
-
if input_was_string:
|
|
642
|
-
return json.dumps(geojson, indent=2)
|
|
643
|
-
else:
|
|
644
|
-
return geojson
|
|
631
|
+
return gdf
|
|
645
632
|
|
|
646
|
-
def
|
|
633
|
+
def get_gdf_from_gpkg(gpkg_path, rectangle_vertices):
|
|
647
634
|
"""
|
|
648
635
|
Read a GeoPackage file and convert it to GeoJSON format within a bounding rectangle.
|
|
649
636
|
|
|
@@ -657,8 +644,18 @@ def get_geojson_from_gpkg(gpkg_path, rectangle_vertices):
|
|
|
657
644
|
# Open and read the GPKG file
|
|
658
645
|
print(f"Opening GPKG file: {gpkg_path}")
|
|
659
646
|
gdf = gpd.read_file(gpkg_path)
|
|
660
|
-
|
|
661
|
-
|
|
647
|
+
|
|
648
|
+
# Only set CRS if not already set
|
|
649
|
+
if gdf.crs is None:
|
|
650
|
+
gdf.set_crs(epsg=4326, inplace=True)
|
|
651
|
+
# Transform to WGS84 if needed
|
|
652
|
+
elif gdf.crs != "EPSG:4326":
|
|
653
|
+
gdf = gdf.to_crs(epsg=4326)
|
|
654
|
+
|
|
655
|
+
# Replace id column with index numbers
|
|
656
|
+
gdf['id'] = gdf.index
|
|
657
|
+
|
|
658
|
+
return gdf
|
|
662
659
|
|
|
663
660
|
def swap_coordinates(features):
|
|
664
661
|
"""
|
|
@@ -702,12 +699,12 @@ def save_geojson(features, save_path):
|
|
|
702
699
|
with open(save_path, 'w') as f:
|
|
703
700
|
json.dump(geojson, f, indent=2)
|
|
704
701
|
|
|
705
|
-
def find_building_containing_point(
|
|
702
|
+
def find_building_containing_point(building_gdf, target_point):
|
|
706
703
|
"""
|
|
707
704
|
Find building IDs that contain a given point.
|
|
708
705
|
|
|
709
706
|
Args:
|
|
710
|
-
|
|
707
|
+
building_gdf (GeoDataFrame): GeoDataFrame containing building geometries and IDs
|
|
711
708
|
target_point (tuple): Tuple of (lon, lat)
|
|
712
709
|
|
|
713
710
|
Returns:
|
|
@@ -717,42 +714,29 @@ def find_building_containing_point(features, target_point):
|
|
|
717
714
|
point = Point(target_point[0], target_point[1])
|
|
718
715
|
|
|
719
716
|
id_list = []
|
|
720
|
-
for
|
|
721
|
-
#
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
717
|
+
for idx, row in building_gdf.iterrows():
|
|
718
|
+
# Skip any geometry that is not Polygon
|
|
719
|
+
if not isinstance(row.geometry, Polygon):
|
|
720
|
+
continue
|
|
721
|
+
|
|
725
722
|
# Check if point is within polygon
|
|
726
|
-
if
|
|
727
|
-
id_list.append(
|
|
723
|
+
if row.geometry.contains(point):
|
|
724
|
+
id_list.append(row.get('id', None))
|
|
728
725
|
|
|
729
726
|
return id_list
|
|
730
727
|
|
|
731
|
-
def get_buildings_in_drawn_polygon(
|
|
728
|
+
def get_buildings_in_drawn_polygon(building_gdf, drawn_polygon_vertices,
|
|
732
729
|
operation='within'):
|
|
733
730
|
"""
|
|
734
|
-
Given a
|
|
731
|
+
Given a GeoDataFrame of building footprints and a set of drawn polygon
|
|
735
732
|
vertices (in lon, lat), return the building IDs that fall within or
|
|
736
733
|
intersect the drawn polygon.
|
|
737
734
|
|
|
738
735
|
Args:
|
|
739
|
-
|
|
740
|
-
A
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
"geometry": {
|
|
744
|
-
"type": "Polygon",
|
|
745
|
-
"coordinates": [
|
|
746
|
-
[
|
|
747
|
-
[lon1, lat1], [lon2, lat2], ...
|
|
748
|
-
]
|
|
749
|
-
]
|
|
750
|
-
},
|
|
751
|
-
"properties": {
|
|
752
|
-
"id": ...
|
|
753
|
-
...
|
|
754
|
-
}
|
|
755
|
-
}
|
|
736
|
+
building_gdf (GeoDataFrame):
|
|
737
|
+
A GeoDataFrame containing building footprints with:
|
|
738
|
+
- geometry column containing Polygon geometries
|
|
739
|
+
- id column containing building IDs
|
|
756
740
|
|
|
757
741
|
drawn_polygon_vertices (list):
|
|
758
742
|
A list of (lon, lat) tuples representing the polygon drawn by the user.
|
|
@@ -771,25 +755,19 @@ def get_buildings_in_drawn_polygon(building_geojson, drawn_polygon_vertices,
|
|
|
771
755
|
|
|
772
756
|
included_building_ids = []
|
|
773
757
|
|
|
774
|
-
# Check each building in the
|
|
775
|
-
for
|
|
776
|
-
# Skip any
|
|
777
|
-
if
|
|
758
|
+
# Check each building in the GeoDataFrame
|
|
759
|
+
for idx, row in building_gdf.iterrows():
|
|
760
|
+
# Skip any geometry that is not Polygon
|
|
761
|
+
if not isinstance(row.geometry, Polygon):
|
|
778
762
|
continue
|
|
779
763
|
|
|
780
|
-
# Extract coordinates
|
|
781
|
-
coords = feature['geometry']['coordinates'][0]
|
|
782
|
-
|
|
783
|
-
# Create a Shapely polygon for the building
|
|
784
|
-
building_polygon = Polygon(coords)
|
|
785
|
-
|
|
786
764
|
# Depending on the operation, check the relationship
|
|
787
765
|
if operation == 'intersect':
|
|
788
|
-
if
|
|
789
|
-
included_building_ids.append(
|
|
766
|
+
if row.geometry.intersects(drawn_polygon_shapely):
|
|
767
|
+
included_building_ids.append(row.get('id', None))
|
|
790
768
|
elif operation == 'within':
|
|
791
|
-
if
|
|
792
|
-
included_building_ids.append(
|
|
769
|
+
if row.geometry.within(drawn_polygon_shapely):
|
|
770
|
+
included_building_ids.append(row.get('id', None))
|
|
793
771
|
else:
|
|
794
772
|
raise ValueError("operation must be 'intersect' or 'within'")
|
|
795
773
|
|
voxcity/simulator/view.py
CHANGED
|
@@ -702,7 +702,7 @@ def compute_landmark_visibility(voxel_data, target_value=-30, view_height_voxel=
|
|
|
702
702
|
|
|
703
703
|
return np.flipud(visibility_map)
|
|
704
704
|
|
|
705
|
-
def get_landmark_visibility_map(voxcity_grid_ori, building_id_grid,
|
|
705
|
+
def get_landmark_visibility_map(voxcity_grid_ori, building_id_grid, building_gdf, meshsize, **kwargs):
|
|
706
706
|
"""Generate a visibility map for landmark buildings in a voxel city.
|
|
707
707
|
|
|
708
708
|
Places observers at valid locations and checks visibility to any part of the
|
|
@@ -712,7 +712,7 @@ def get_landmark_visibility_map(voxcity_grid_ori, building_id_grid, building_geo
|
|
|
712
712
|
Args:
|
|
713
713
|
voxcity_grid (ndarray): 3D array representing the voxel city
|
|
714
714
|
building_id_grid (ndarray): 3D array mapping voxels to building IDs
|
|
715
|
-
|
|
715
|
+
building_gdf (GeoDataFrame): GeoDataFrame containing building features
|
|
716
716
|
meshsize (float): Size of each voxel in meters
|
|
717
717
|
**kwargs: Additional keyword arguments
|
|
718
718
|
view_point_height (float): Height of observer viewpoint in meters
|
|
@@ -737,12 +737,11 @@ def get_landmark_visibility_map(voxcity_grid_ori, building_id_grid, building_geo
|
|
|
737
737
|
colormap = kwargs.get("colormap", 'viridis')
|
|
738
738
|
|
|
739
739
|
# Get landmark building IDs either directly or by finding buildings in rectangle
|
|
740
|
-
features = building_geojson
|
|
741
740
|
landmark_ids = kwargs.get('landmark_building_ids', None)
|
|
742
741
|
landmark_polygon = kwargs.get('landmark_polygon', None)
|
|
743
742
|
if landmark_ids is None:
|
|
744
743
|
if landmark_polygon is not None:
|
|
745
|
-
landmark_ids = get_buildings_in_drawn_polygon(
|
|
744
|
+
landmark_ids = get_buildings_in_drawn_polygon(building_gdf, landmark_polygon, operation='within')
|
|
746
745
|
else:
|
|
747
746
|
rectangle_vertices = kwargs.get("rectangle_vertices", None)
|
|
748
747
|
if rectangle_vertices is None:
|
|
@@ -757,7 +756,7 @@ def get_landmark_visibility_map(voxcity_grid_ori, building_id_grid, building_geo
|
|
|
757
756
|
target_point = (center_lon, center_lat)
|
|
758
757
|
|
|
759
758
|
# Find buildings at center point
|
|
760
|
-
landmark_ids = find_building_containing_point(
|
|
759
|
+
landmark_ids = find_building_containing_point(building_gdf, target_point)
|
|
761
760
|
|
|
762
761
|
# Mark landmark buildings in voxel grid with special value
|
|
763
762
|
target_value = -30
|