Source code for voxcity.geoprocessor.conversion
"""
Conversion utilities between GeoJSON-like features and GeoPandas GeoDataFrames,
plus helpers to filter and transform geometries for export.
"""
import json
from typing import List, Dict
import geopandas as gpd
import pandas as pd
from shapely.geometry import Polygon, shape
[docs]
def filter_and_convert_gdf_to_geojson(gdf, rectangle_vertices):
"""
Filter a GeoDataFrame by a bounding rectangle and convert to GeoJSON format.
This function performs spatial filtering on a GeoDataFrame using a bounding rectangle,
and converts the filtered data to GeoJSON format. It handles both Polygon and MultiPolygon
geometries, splitting MultiPolygons into separate Polygon features.
Args:
gdf (GeoDataFrame): Input GeoDataFrame containing building data
Must have 'geometry' and 'height' columns
Any CRS is accepted, will be converted to WGS84 if needed
rectangle_vertices (list): List of (lon, lat) tuples defining the bounding rectangle
Must be in WGS84 (EPSG:4326) coordinate system
Must form a valid rectangle (4 vertices, clockwise or counterclockwise)
Returns:
list: List of GeoJSON features within the bounding rectangle
Each feature contains:
- geometry: Polygon coordinates in WGS84
- properties: Dictionary with 'height', 'confidence', and 'id'
- type: Always "Feature"
Memory Optimization:
- Uses spatial indexing for efficient filtering
- Downcasts numeric columns to save memory
- Cleans up intermediate data structures
- Splits MultiPolygons into separate features
"""
if gdf.crs != 'EPSG:4326':
gdf = gdf.to_crs(epsg=4326)
gdf['height'] = pd.to_numeric(gdf['height'], downcast='float')
gdf['confidence'] = -1.0
rectangle_polygon = Polygon(rectangle_vertices)
gdf.sindex
possible_matches_index = list(gdf.sindex.intersection(rectangle_polygon.bounds))
possible_matches = gdf.iloc[possible_matches_index]
precise_matches = possible_matches[possible_matches.intersects(rectangle_polygon)]
filtered_gdf = precise_matches.copy()
del gdf, possible_matches, precise_matches
features = []
feature_id = 1
for _, row in filtered_gdf.iterrows():
geom = row['geometry'].__geo_interface__
properties = {
'height': row['height'],
'confidence': row['confidence'],
'id': feature_id
}
if geom['type'] == 'MultiPolygon':
for polygon_coords in geom['coordinates']:
single_geom = {
'type': 'Polygon',
'coordinates': polygon_coords
}
feature = {
'type': 'Feature',
'properties': properties.copy(),
'geometry': single_geom
}
features.append(feature)
feature_id += 1
elif geom['type'] == 'Polygon':
feature = {
'type': 'Feature',
'properties': properties,
'geometry': geom
}
features.append(feature)
feature_id += 1
else:
pass
geojson = {
'type': 'FeatureCollection',
'features': features
}
del filtered_gdf, features
return geojson["features"]
[docs]
def geojson_to_gdf(geojson_data, id_col='id'):
"""
Convert a list of GeoJSON-like dict features into a GeoDataFrame.
This function takes a list of GeoJSON feature dictionaries (Fiona-like format)
and converts them into a GeoDataFrame, handling geometry conversion and property
extraction. It ensures each feature has a unique identifier.
"""
geometries = []
all_props = []
for i, feature in enumerate(geojson_data):
geom = feature.get('geometry')
shapely_geom = shape(geom) if geom else None
props = feature.get('properties', {})
if id_col not in props:
props[id_col] = i
geometries.append(shapely_geom)
all_props.append(props)
gdf = gpd.GeoDataFrame(all_props, geometry=geometries, crs="EPSG:4326")
return gdf
[docs]
def gdf_to_geojson_dicts(gdf, id_col='id'):
"""
Convert a GeoDataFrame to a list of dicts similar to GeoJSON features.
"""
records = gdf.to_dict(orient='records')
features = []
for rec in records:
geom = rec.pop('geometry', None)
if geom is not None:
geom = geom.__geo_interface__
_ = rec.get(id_col, None)
props = {k: v for k, v in rec.items() if k != id_col}
feature = {
'type': 'Feature',
'properties': props,
'geometry': geom
}
features.append(feature)
return features