-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathkml_reader.py
More file actions
152 lines (127 loc) · 6.93 KB
/
Copy pathkml_reader.py
File metadata and controls
152 lines (127 loc) · 6.93 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
"""
KML and KMZ GIS Data Geometry and Metadata Extraction Engine
Portfolio / Open-Source Edition
This module parses geospatial KML and compressed binary KMZ files to extract geometry
coordination features, tracking boundaries, and descriptive metadata attributes.
It structures extracted shapes into feature objects, isolates potential agency name strings,
client tags, timeline years, and project identification keys across filename rules, folder names,
and text descriptions to assist form automation, and calculates dynamic center coordinates.
:author: Lucas Polo
:date: 2026-07-13
:version: 2.6.0
"""
import xml.etree.ElementTree as ET
import re
import zipfile
import os
def parse_kml_geometries(filepath, filename_guessed_id=None):
"""
Parses a KML or KMZ file to extract coordinates, features, center points, and metadata tags.
:param filepath: Local file path to the .kml or .kmz file
:param filename_guessed_id: Optional ID string extracted from the filename
:return: Tuple containing (features, master_center, guessed_agency, guessed_client,
guessed_type, guessed_year, final_id)
"""
# Standard geospatial markup language XML namespace dictionary
namespace = {'kml': 'http://www.opengis.net/kml/2.2'}
# Selecting the file stream reading strategy based on file extension
try:
if filepath.lower().endswith('.kmz'):
with zipfile.ZipFile(filepath, 'r') as archive:
kml_files = [f for f in archive.namelist() if f.lower().endswith('.kml')]
if not kml_files:
print("Error: Could not locate a valid internal .kml document inside the KMZ archive.")
return None, None, None, None, None, None, None
with archive.open(kml_files[0]) as extracted_file:
root = ET.fromstring(extracted_file.read())
else:
tree = ET.parse(filepath)
root = tree.getroot()
except Exception as e:
print(f"Error: Failed to parse XML structure inside geometry file.\n{str(e)}")
return None, None, None, None, None, None, None
features = []
all_center_points = []
guessed_agency = None
guessed_client = None
guessed_type = None
guessed_year = None
xml_project_id = None
folder_guessed_id = None
# Strategy 1: Scanning internal Folder names for a potential Project ID
for folder in root.findall('.//kml:Folder', namespace):
folder_name_node = folder.find('kml:name', namespace)
if folder_name_node is not None and folder_name_node.text:
f_text = folder_name_node.text.strip()
# Extracting the first continuous group of numbers or standard decimal indices
id_match = re.search(r'^\d+(?:\.\d+)?', f_text)
if id_match:
folder_guessed_id = id_match.group(0)
break # Stopping loop once an ID is found in a top-level folder
# Looping through every standalone geospatial placemark node found in the file
for placemark in root.findall('.//kml:Placemark', namespace):
description_node = placemark.find('kml:description', namespace)
# Pulling the description tag text block to scan for background metadata strings
if description_node is not None and description_node.text:
desc_text = description_node.text.strip()
# Scanning for agency key name strings if not already extracted
if guessed_agency is None:
agency_match = re.search(r'Agency:\s*([^<\n]+)', desc_text, re.IGNORECASE)
if agency_match:
guessed_agency = agency_match.group(1).strip()
# Scanning for client or customer names if not already extracted
if guessed_client is None:
client_match = re.search(r'(?:Client|Customer):\s*([^<\n]+)', desc_text, re.IGNORECASE)
if client_match:
guessed_client = client_match.group(1).strip()
# Scanning for project type or scope fields if not already extracted
if guessed_type is None:
type_match = re.search(r'(?:Project\s*Type|Type|Scope):\s*([^<\n]+)', desc_text, re.IGNORECASE)
if type_match:
guessed_type = type_match.group(1).strip()
# Scanning for project tracking years if not already extracted
if guessed_year is None:
year_match = re.search(r'(?:Project\s*Year|Year|Date):\s*(\d{4})', desc_text, re.IGNORECASE)
if year_match:
guessed_year = year_match.group(1).strip()
# Scanning for project identifier indexing variations inside descriptions
if xml_project_id is None:
id_match = re.search(r'(?:Project\s*ID|Project\s*No|Project\s*Number|Job\s*No|Job\s*Number):\s*([^<\n]+)', desc_text, re.IGNORECASE)
if id_match:
xml_project_id = id_match.group(1).strip()
coordinate_node = placemark.find('.//kml:coordinates', namespace)
# Validating and isolating coordinate string blocks from the active node
if coordinate_node is not None and coordinate_node.text:
local_points = []
raw_coordinates = coordinate_node.text.strip().split()
for point in raw_coordinates:
parts = point.split(',')
if len(parts) >= 2:
longitude = float(parts[0])
latitude = float(parts[1])
local_points.append([latitude, longitude])
if local_points:
is_point = placemark.find('.//kml:Point', namespace) is not None
if is_point:
features.append({"name": "TEMP", "type": "point", "center": local_points[0]})
all_center_points.append(local_points[0])
else:
features.append({"name": "TEMP", "type": "area", "coords": local_points})
all_center_points.append(local_points[0])
if not features:
return None, None, guessed_agency, guessed_client, guessed_type, guessed_year, xml_project_id
avg_lat = sum(p[0] for p in all_center_points) / len(all_center_points)
avg_lon = sum(p[1] for p in all_center_points) / len(all_center_points)
master_center = [round(avg_lat, 6), round(avg_lon, 6)]
# Resolving final project identity designation using hierarchical fallback checks
if filename_guessed_id:
final_id = filename_guessed_id
elif folder_guessed_id:
final_id = folder_guessed_id
else:
final_id = xml_project_id
# Updating feature name tags retroactively
if final_id:
for f in features:
f["name"] = final_id
return features, master_center, guessed_agency, guessed_client, guessed_type, guessed_year, final_id