-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgeospatial_utils.py
More file actions
129 lines (106 loc) · 5.05 KB
/
Copy pathgeospatial_utils.py
File metadata and controls
129 lines (106 loc) · 5.05 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
# C:/Users/krant/PycharmProjects/SelMapExtract/geospatial_utils.py
import logging
from pathlib import Path
from typing import List, Tuple
import geopandas as gpd
# noinspection PyPep8Naming
import lxml.etree as ET
from shapely.geometry import LineString
log = logging.getLogger(__name__)
def parse_tcx_for_coords(tcx_file: str) -> List[Tuple[float, float]]:
"""
Parses a TCX file and extracts a list of (longitude, latitude) coordinates.
This version uses a robust method to handle XML namespaces correctly.
"""
try:
with open(tcx_file, 'rb') as f:
parser = ET.XMLParser(remove_blank_text=True)
tree = ET.parse(f, parser)
root = tree.getroot()
except Exception as e:
log.error(f"Failed at initial parsing stage for {Path(tcx_file).name}. Reason: {e}")
return []
# --- Namespace Handling ---
ns = root.nsmap
if None in ns:
ns['default'] = ns.pop(None)
# --- Find Trackpoints ---
try:
trackpoints = root.findall('.//default:Trackpoint', namespaces=ns)
if not trackpoints:
trackpoints = root.findall('.//Trackpoint') # Fallback
except Exception as e:
log.error(f"Failed during findall operation for trackpoints. Reason: {e}")
return []
if not trackpoints:
log.warning(f"No <Trackpoint> elements found in {Path(tcx_file).name}. "
f"The file may be empty or structured unexpectedly.")
return []
# --- Extract Coordinates ---
coordinates = []
for i, trackpoint in enumerate(trackpoints):
position = trackpoint.find('default:Position', namespaces=ns)
if position is None:
position = trackpoint.find('Position') # Fallback
if position is not None:
lat_el = position.find('default:LatitudeDegrees', namespaces=ns)
if lat_el is None:
lat_el = position.find('LatitudeDegrees') # Fallback
lon_el = position.find('default:LongitudeDegrees', namespaces=ns)
if lon_el is None:
lon_el = position.find('LongitudeDegrees') # Fallback
if lat_el is not None and lon_el is not None and lat_el.text is not None and lon_el.text is not None:
try:
lat = float(lat_el.text)
lon = float(lon_el.text)
coordinates.append((lon, lat))
except (ValueError, TypeError):
log.warning(f"Skipping malformed coordinate text in trackpoint {i} in {Path(tcx_file).name}")
log.info(f"Extracted {len(coordinates)} coordinates from {len(trackpoints)} trackpoints in {Path(tcx_file).name}.")
return coordinates
def create_simplified_geojson(
tcx_path: Path,
geojson_path: Path,
tolerance: float = 10.0
):
"""
Creates a simplified GeoJSON file from a TCX file using GeoPandas.
"""
try:
coordinates = parse_tcx_for_coords(str(tcx_path))
if len(coordinates) < 2:
log.warning(f"Skipping {tcx_path.name}: not enough points ({len(coordinates)}) to form a line.")
return
line = LineString(coordinates)
gdf = gpd.GeoDataFrame(geometry=[line], crs="EPSG:4326")
# Simplify geometry
utm_crs = gdf.estimate_utm_crs(datum_name="WGS 84")
# Type Guard: Ensure utm_crs is not None before projecting
if utm_crs is None:
log.error(f"Could not estimate suitable UTM projection for {tcx_path.name}. Skipping simplification.")
return
# Fix: Convert the CRS object to WKT string to satisfy strict IDE type checking
# This resolves the 'got CRS | None instead' error at the projection call
utm_wkt = utm_crs.to_wkt()
gdf_projected = gdf.to_crs(utm_wkt)
# Explicit check to satisfy IDE type checker regarding GeoDataFrame status
if gdf_projected is not None and not gdf_projected.empty:
gdf_projected['geometry'] = gdf_projected.geometry.simplify(
tolerance=tolerance, preserve_topology=True
)
# Reproject back to original CRS (WGS84)
# We know gdf.crs is not None here because we initialized it above
original_crs = gdf.crs
if original_crs is not None:
gdf_simplified = gdf_projected.to_crs(original_crs)
# Type Guard: verify gdf_simplified exists and has data
if gdf_simplified is not None and not gdf_simplified.empty:
if not gdf_simplified.geometry.is_empty.all():
gdf_simplified.to_file(str(geojson_path), driver='GeoJSON')
log.info(f"Simplified '{tcx_path.name}' -> '{geojson_path.name}'")
else:
log.warning(f"Geometry for {tcx_path.name} became empty after simplification.")
else:
log.warning(f"Simplification resulted in an empty dataset for {tcx_path.name}.")
except Exception as e:
log.error(f"ERROR simplifying '{tcx_path.name}': {e}", exc_info=True)