Skip to content

Commit d4ad0de

Browse files
committed
Refactor raster processing and add terrain grid functions
- Moved DEM grid creation from GeoDataFrame polygon to a new terrain module using KDTree for faster processing. - Updated the raster module to import new terrain functions and removed redundant DEM grid creation code. - Enhanced building height grid function to handle empty GeoDataFrames and improved height data reporting. - Adjusted grid processing to maintain consistent (Y, X) orientation across functions. - Introduced new test scripts for LOD2 CityGML processing and timing analysis to identify performance bottlenecks.
1 parent 3529ff1 commit d4ad0de

17 files changed

Lines changed: 7073 additions & 389 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ wheels/
2727
app/data/
2828
reference/
2929
output/
30+
data/
3031

3132
# PyInstaller
3233
# Usually these files are written by a python script from a template

src/voxcity/downloader/citygml.py

Lines changed: 178 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
"""
2-
CityGML Parser Module for PLATEAU Data
2+
CityGML Downloader Module for PLATEAU Data
33
4-
This module provides functionality to parse CityGML files from Japan's PLATEAU dataset,
5-
extracting building footprints, terrain information, and vegetation data.
6-
The module handles various LOD (Level of Detail) representations and coordinate systems.
4+
This module provides download functionality for PLATEAU CityGML data.
75
86
Main features:
97
- Download and extract PLATEAU data from URLs
10-
- Parse CityGML files for buildings, terrain, and vegetation
11-
- Handle coordinate transformations and validations
128
- Support for mesh code decoding
9+
10+
Note:
11+
CityGML parsing functionality has been moved to voxcity.geoprocessor.citygml.
12+
For backward compatibility, `load_buid_dem_veg_from_citygml` is re-exported
13+
from this module but internally uses the new geoprocessor.citygml module.
1314
"""
1415

1516
import requests
@@ -260,57 +261,103 @@ def extract_terrain_info(file_path, namespaces):
260261
- Processes TIN Relief, breaklines, and mass points
261262
- Validates all geometries before inclusion
262263
- Handles coordinate conversion and validation
264+
- Uses optimized batch processing for large terrain datasets
263265
"""
264266
try:
265267
tree = ET.parse(file_path)
266268
root = tree.getroot()
267269

268270
terrain_elements = []
269-
271+
source_file_name = Path(file_path).name
272+
270273
# Look for Relief features in the CityGML file
271274
for relief in root.findall('.//dem:ReliefFeature', namespaces):
272275
relief_id = relief.get('{http://www.opengis.net/gml}id')
273276

274-
# Extract TIN Relief components
277+
# Extract TIN Relief components - OPTIMIZED VERSION
275278
for tin in relief.findall('.//dem:TINRelief', namespaces):
276279
tin_id = tin.get('{http://www.opengis.net/gml}id')
277280

278281
triangles = tin.findall('.//gml:Triangle', namespaces)
279-
for i, triangle in enumerate(triangles):
280-
pos_lists = triangle.findall('.//gml:posList', namespaces)
281-
for pos_list in pos_lists:
282-
try:
283-
coords_text = pos_list.text.strip().split()
284-
coords = []
285-
elevations = []
286-
287-
for j in range(0, len(coords_text), 3):
288-
if j + 2 < len(coords_text):
289-
x = float(coords_text[j])
290-
y = float(coords_text[j+1])
291-
z = float(coords_text[j+2])
292-
293-
if not np.isinf(x) and not np.isinf(y) and not np.isinf(z):
294-
coords.append((x, y))
295-
elevations.append(z)
296-
297-
if len(coords) >= 3 and validate_coords(coords):
298-
polygon = Polygon(coords)
299-
if polygon.is_valid:
300-
centroid = polygon.centroid
301-
avg_elevation = np.mean(elevations)
302-
terrain_elements.append({
303-
'relief_id': relief_id,
304-
'tin_id': tin_id,
305-
'triangle_id': f"{tin_id}_tri_{i}",
306-
'elevation': avg_elevation,
307-
'geometry': centroid,
308-
'polygon': polygon,
309-
'source_file': Path(file_path).name
310-
})
311-
except (ValueError, IndexError) as e:
312-
print(f"Error processing triangle in relief {relief_id}: {e}")
313-
continue
282+
num_triangles = len(triangles)
283+
284+
if num_triangles > 10000:
285+
# Use batch processing for large terrain datasets
286+
# Pre-allocate arrays for centroids and elevations
287+
centroids_x = []
288+
centroids_y = []
289+
elevations = []
290+
291+
for i, triangle in enumerate(triangles):
292+
pos_lists = triangle.findall('.//gml:posList', namespaces)
293+
for pos_list in pos_lists:
294+
try:
295+
coords_text = pos_list.text.strip().split()
296+
if len(coords_text) >= 9: # 3 vertices * 3 coords each
297+
# Parse all 3 vertices at once
298+
x0, y0, z0 = float(coords_text[0]), float(coords_text[1]), float(coords_text[2])
299+
x1, y1, z1 = float(coords_text[3]), float(coords_text[4]), float(coords_text[5])
300+
x2, y2, z2 = float(coords_text[6]), float(coords_text[7]), float(coords_text[8])
301+
302+
# Compute centroid directly without Shapely
303+
cx = (x0 + x1 + x2) / 3.0
304+
cy = (y0 + y1 + y2) / 3.0
305+
avg_elev = (z0 + z1 + z2) / 3.0
306+
307+
if not (np.isinf(cx) or np.isinf(cy) or np.isinf(avg_elev)):
308+
centroids_x.append(cx)
309+
centroids_y.append(cy)
310+
elevations.append(avg_elev)
311+
except (ValueError, IndexError):
312+
continue
313+
314+
# Batch create Point objects at the end (much faster than one-by-one)
315+
for i, (cx, cy, elev) in enumerate(zip(centroids_x, centroids_y, elevations)):
316+
terrain_elements.append({
317+
'relief_id': relief_id,
318+
'tin_id': tin_id,
319+
'triangle_id': f"{tin_id}_tri_{i}",
320+
'elevation': elev,
321+
'geometry': Point(cx, cy),
322+
'polygon': None, # Skip polygon for speed
323+
'source_file': source_file_name
324+
})
325+
else:
326+
# Original code for small datasets
327+
for i, triangle in enumerate(triangles):
328+
pos_lists = triangle.findall('.//gml:posList', namespaces)
329+
for pos_list in pos_lists:
330+
try:
331+
coords_text = pos_list.text.strip().split()
332+
coords = []
333+
tri_elevations = []
334+
335+
for j in range(0, len(coords_text), 3):
336+
if j + 2 < len(coords_text):
337+
x = float(coords_text[j])
338+
y = float(coords_text[j+1])
339+
z = float(coords_text[j+2])
340+
341+
if not np.isinf(x) and not np.isinf(y) and not np.isinf(z):
342+
coords.append((x, y))
343+
tri_elevations.append(z)
344+
345+
if len(coords) >= 3 and validate_coords(coords):
346+
polygon = Polygon(coords)
347+
if polygon.is_valid:
348+
centroid = polygon.centroid
349+
avg_elevation = np.mean(tri_elevations)
350+
terrain_elements.append({
351+
'relief_id': relief_id,
352+
'tin_id': tin_id,
353+
'triangle_id': f"{tin_id}_tri_{i}",
354+
'elevation': avg_elevation,
355+
'geometry': centroid,
356+
'polygon': polygon,
357+
'source_file': source_file_name
358+
})
359+
except (ValueError, IndexError) as e:
360+
continue
314361

315362
# Extract breaklines
316363
for breakline in relief.findall('.//dem:breaklines', namespaces):
@@ -860,16 +907,28 @@ def load_buid_dem_veg_from_citygml(url=None,
860907
rectangle_vertices=None,
861908
ssl_verify=True,
862909
ca_bundle=None,
863-
timeout=60):
910+
timeout=60,
911+
skip_buildings=False,
912+
skip_terrain=False,
913+
skip_vegetation=False):
864914
"""
865915
Load and process PLATEAU data from URL or local files.
916+
917+
**DEPRECATED**: This function is provided for backward compatibility.
918+
New code should use `voxcity.geoprocessor.citygml.load_lod1_citygml()`.
866919
867920
Args:
868921
url (str, optional): URL to download PLATEAU data from.
869922
base_dir (str): Base directory for file operations.
870923
citygml_path (str, optional): Path to local CityGML files.
871924
rectangle_vertices (list, optional): List of (lon, lat) tuples defining
872925
a bounding rectangle for filtering tiles.
926+
ssl_verify (bool): Whether to verify SSL certificates.
927+
ca_bundle (str, optional): Path to CA certificate bundle.
928+
timeout (int): Request timeout in seconds.
929+
skip_buildings (bool): Skip building file parsing (for LOD2 mode).
930+
skip_terrain (bool): Skip terrain/DEM file parsing.
931+
skip_vegetation (bool): Skip vegetation file parsing.
873932
874933
Returns:
875934
tuple: (gdf_buildings, gdf_terrain, gdf_vegetation) GeoDataFrames
@@ -878,8 +937,56 @@ def load_buid_dem_veg_from_citygml(url=None,
878937
Notes:
879938
- Can process from URL (download & extract) or local files
880939
- Optionally filters tiles by geographic extent
881-
- Handles coordinate transformations
882-
- Creates GeoDataFrames with proper CRS
940+
- Internally uses geoprocessor.citygml.load_lod1_citygml()
941+
"""
942+
from ..geoprocessor.citygml import load_lod1_citygml
943+
944+
# Handle URL download
945+
resolved_path = citygml_path
946+
if url:
947+
resolved_path, foldername = download_and_extract_zip(
948+
url, extract_to=base_dir, ssl_verify=ssl_verify, ca_bundle=ca_bundle, timeout=timeout
949+
)
950+
# Check for nested folder structure
951+
udx_path = os.path.join(resolved_path, 'udx')
952+
if not os.path.exists(udx_path):
953+
udx_path_2 = os.path.join(resolved_path, foldername, 'udx')
954+
if os.path.exists(udx_path_2):
955+
resolved_path = os.path.join(resolved_path, foldername)
956+
elif citygml_path:
957+
resolved_path = citygml_path
958+
else:
959+
print("Either url or citygml_path must be specified")
960+
return None, None, None
961+
962+
# Use the new parser
963+
return load_lod1_citygml(
964+
citygml_path=resolved_path,
965+
rectangle_vertices=rectangle_vertices,
966+
parse_buildings=not skip_buildings,
967+
parse_terrain=not skip_terrain,
968+
parse_vegetation=not skip_vegetation,
969+
)
970+
971+
972+
# =============================================================================
973+
# Legacy functions - kept for backward compatibility but no longer used internally
974+
# =============================================================================
975+
976+
def _legacy_load_buid_dem_veg_from_citygml(url=None,
977+
base_dir='.',
978+
citygml_path=None,
979+
rectangle_vertices=None,
980+
ssl_verify=True,
981+
ca_bundle=None,
982+
timeout=60,
983+
skip_buildings=False,
984+
skip_terrain=False,
985+
skip_vegetation=False):
986+
"""
987+
Legacy implementation - kept for reference.
988+
989+
This is the original implementation before refactoring to geoprocessor.citygml.
883990
"""
884991
all_buildings = []
885992
all_terrain = []
@@ -908,18 +1015,30 @@ def load_buid_dem_veg_from_citygml(url=None,
9081015
if os.path.exists(citygml_dir_2):
9091016
citygml_dir = citygml_dir_2
9101017

911-
# Potential sub-folders
1018+
# Potential sub-folders - only include folders we need based on skip flags
9121019
bldg_dir = os.path.join(citygml_dir, 'bldg')
9131020
dem_dir = os.path.join(citygml_dir, 'dem')
9141021
veg_dir = os.path.join(citygml_dir, 'veg')
9151022

1023+
# Build list of folders to process based on skip flags
1024+
folders_to_process = []
1025+
if not skip_buildings and os.path.exists(bldg_dir):
1026+
folders_to_process.append(bldg_dir)
1027+
if not skip_terrain and os.path.exists(dem_dir):
1028+
folders_to_process.append(dem_dir)
1029+
if not skip_vegetation and os.path.exists(veg_dir):
1030+
folders_to_process.append(veg_dir)
1031+
9161032
citygml_files = []
917-
for folder in [bldg_dir, dem_dir, veg_dir, citygml_dir]:
918-
if os.path.exists(folder):
919-
citygml_files += [
920-
os.path.join(folder, f) for f in os.listdir(folder)
921-
if f.endswith(('.gml', '.xml'))
922-
]
1033+
for folder in folders_to_process:
1034+
citygml_files += [
1035+
os.path.join(folder, f) for f in os.listdir(folder)
1036+
if f.endswith(('.gml', '.xml'))
1037+
]
1038+
1039+
if not citygml_files:
1040+
print("No CityGML files to process (all types skipped or no files found)")
1041+
return None, None, None
9231042

9241043
print(f"Found {len(citygml_files)} CityGML files to process")
9251044

@@ -942,9 +1061,12 @@ def load_buid_dem_veg_from_citygml(url=None,
9421061

9431062
# Parse the file
9441063
buildings, terrain_elements, vegetation_elements = parse_file(file_path)
945-
all_buildings.extend(buildings)
946-
all_terrain.extend(terrain_elements)
947-
all_vegetation.extend(vegetation_elements)
1064+
if not skip_buildings:
1065+
all_buildings.extend(buildings)
1066+
if not skip_terrain:
1067+
all_terrain.extend(terrain_elements)
1068+
if not skip_vegetation:
1069+
all_vegetation.extend(vegetation_elements)
9481070

9491071
except Exception as e:
9501072
print(f"Error finding CityGML files: {e}")

src/voxcity/generator/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
from .io import save_voxcity, load_voxcity
2626
from .update import update_voxcity, regenerate_voxels
2727

28+
# CityGML subpackage for LOD2 parsing and voxelization
29+
from ..geoprocessor import citygml
30+
2831
__all__ = [
2932
"get_voxcity",
3033
"auto_select_data_sources",
@@ -42,6 +45,8 @@
4245
"load_voxcity",
4346
"update_voxcity",
4447
"regenerate_voxels",
48+
# CityGML subpackage
49+
"citygml",
4550
]
4651

4752

0 commit comments

Comments
 (0)