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
86Main 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
1516import 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 } " )
0 commit comments