config-cli-gui 0.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- __init__.py +0 -0
- config_cli_gui/__init__.py +0 -0
- config_cli_gui/_version.py +21 -0
- config_cli_gui/cli_generator.py +177 -0
- config_cli_gui/config_framework.py +362 -0
- config_cli_gui/gui_generator.py +225 -0
- config_cli_gui-0.0.2.dist-info/METADATA +282 -0
- config_cli_gui-0.0.2.dist-info/RECORD +26 -0
- config_cli_gui-0.0.2.dist-info/WHEEL +5 -0
- config_cli_gui-0.0.2.dist-info/entry_points.txt +3 -0
- config_cli_gui-0.0.2.dist-info/licenses/LICENSE +24 -0
- config_cli_gui-0.0.2.dist-info/top_level.txt +4 -0
- example_project/__init__.py +0 -0
- example_project/__main__.py +8 -0
- example_project/cli/__init__.py +0 -0
- example_project/cli/__main__.py +8 -0
- example_project/cli/cli.py +132 -0
- example_project/config/__init__.py +0 -0
- example_project/config/config.py +209 -0
- example_project/core/__init__.py +0 -0
- example_project/core/base.py +634 -0
- example_project/core/logging.py +219 -0
- example_project/gui/__init__.py +0 -0
- example_project/gui/__main__.py +8 -0
- example_project/gui/gui.py +542 -0
- main.py +153 -0
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import traceback
|
|
3
|
+
import zipfile
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import gpxpy
|
|
8
|
+
from gpxpy.gpx import GPX, GPXTrackPoint, GPXWaypoint, GPXXMLSyntaxException
|
|
9
|
+
|
|
10
|
+
# Optional SRTM import with fallback
|
|
11
|
+
try:
|
|
12
|
+
import srtm
|
|
13
|
+
|
|
14
|
+
SRTM_AVAILABLE = True
|
|
15
|
+
except ImportError:
|
|
16
|
+
SRTM_AVAILABLE = False
|
|
17
|
+
srtm = None
|
|
18
|
+
|
|
19
|
+
# Optional fastkml import for KML reading
|
|
20
|
+
try:
|
|
21
|
+
from fastkml import kml, styles
|
|
22
|
+
from fastkml.features import Document, Folder, Placemark
|
|
23
|
+
from shapely.geometry import LineString, Point
|
|
24
|
+
|
|
25
|
+
KML_AVAILABLE = True
|
|
26
|
+
except ImportError:
|
|
27
|
+
KML_AVAILABLE = False
|
|
28
|
+
kml = styles = Folder = Placemark = Document = Point = LineString = None
|
|
29
|
+
|
|
30
|
+
NAME = "config_cli_gui"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class BaseGPXProcessor:
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
input_: str | Path | list[str],
|
|
37
|
+
output=None,
|
|
38
|
+
min_dist=10,
|
|
39
|
+
date_format="%Y-%m-%d",
|
|
40
|
+
elevation=True,
|
|
41
|
+
logger=None,
|
|
42
|
+
):
|
|
43
|
+
# ensure that input is converted into a list[Path]
|
|
44
|
+
if isinstance(input_, str):
|
|
45
|
+
self.input = [Path(input_)]
|
|
46
|
+
elif isinstance(input_, Path):
|
|
47
|
+
self.input = [input_]
|
|
48
|
+
elif isinstance(input_, list):
|
|
49
|
+
self.input = [Path(p) for p in input_ if isinstance(p, str | Path)]
|
|
50
|
+
else:
|
|
51
|
+
raise ValueError("Input must be a string, Path, or list of strings/Paths.")
|
|
52
|
+
|
|
53
|
+
self.output = output
|
|
54
|
+
self.min_dist = min_dist
|
|
55
|
+
self.date_format = date_format
|
|
56
|
+
self.include_elevation = elevation
|
|
57
|
+
self.logger = logger
|
|
58
|
+
|
|
59
|
+
# Initialize SRTM elevation data only if elevation is requested and SRTM is available
|
|
60
|
+
self.elevation_data = None
|
|
61
|
+
self.srtm_available = False
|
|
62
|
+
|
|
63
|
+
if self.include_elevation:
|
|
64
|
+
self._initialize_elevation_data()
|
|
65
|
+
|
|
66
|
+
def _initialize_elevation_data(self):
|
|
67
|
+
"""Initialize SRTM elevation data with proper error handling."""
|
|
68
|
+
if not SRTM_AVAILABLE:
|
|
69
|
+
self.logger.warning(
|
|
70
|
+
"SRTM library not available. "
|
|
71
|
+
"Elevation data will use original GPX values or default to 0."
|
|
72
|
+
)
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
self.logger.info("Initializing SRTM elevation data...")
|
|
77
|
+
self.elevation_data = srtm.get_data()
|
|
78
|
+
self.srtm_available = True
|
|
79
|
+
self.logger.info("SRTM elevation data initialized successfully.")
|
|
80
|
+
except AssertionError as e:
|
|
81
|
+
self.logger.error(f"SRTM initialization failed with AssertionError: {e}")
|
|
82
|
+
self.logger.debug(f"Full traceback:\n{traceback.format_exc()}")
|
|
83
|
+
self._handle_srtm_failure("SRTM assertion failed - possibly network or firewall issue")
|
|
84
|
+
except Exception as e:
|
|
85
|
+
self.logger.error(f"SRTM initialization failed: {e}")
|
|
86
|
+
self.logger.debug(f"Full traceback:\n{traceback.format_exc()}")
|
|
87
|
+
self._handle_srtm_failure(f"SRTM initialization error: {str(e)}")
|
|
88
|
+
|
|
89
|
+
def _handle_srtm_failure(self, error_msg: str):
|
|
90
|
+
"""Handle SRTM initialization failure with firewall check."""
|
|
91
|
+
self.srtm_available = False
|
|
92
|
+
self.elevation_data = None
|
|
93
|
+
|
|
94
|
+
self.logger.warning(f"SRTM elevation data unavailable: {error_msg}")
|
|
95
|
+
self.logger.info("Checking for network/firewall issues...")
|
|
96
|
+
|
|
97
|
+
def _get_output_folder(self) -> Path:
|
|
98
|
+
"""Get the output folder path, create if not exists."""
|
|
99
|
+
if self.output:
|
|
100
|
+
output_path = Path(self.output)
|
|
101
|
+
else:
|
|
102
|
+
timestamp = datetime.now().strftime(
|
|
103
|
+
f"{self.date_format}_%H%M%S"
|
|
104
|
+
) # Added seconds for uniqueness
|
|
105
|
+
output_path = Path.cwd() / f"gpx_processed_{timestamp}"
|
|
106
|
+
|
|
107
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
108
|
+
return output_path
|
|
109
|
+
|
|
110
|
+
def _get_adjusted_elevation(self, point: GPXTrackPoint) -> int | float:
|
|
111
|
+
"""Get adjusted elevation from SRTM data, fallback to original elevation."""
|
|
112
|
+
# If elevation is not requested, return None
|
|
113
|
+
if not self.include_elevation:
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
# If SRTM is available and working, try to get elevation data
|
|
117
|
+
if self.srtm_available and self.elevation_data:
|
|
118
|
+
try:
|
|
119
|
+
srtm_elevation = self.elevation_data.get_elevation(point.latitude, point.longitude)
|
|
120
|
+
if srtm_elevation is not None:
|
|
121
|
+
return round(srtm_elevation, 1)
|
|
122
|
+
except Exception as e:
|
|
123
|
+
self.logger.info(
|
|
124
|
+
f"Error getting SRTM elevation "
|
|
125
|
+
f"for point ({point.latitude}, {point.longitude}): {e}"
|
|
126
|
+
)
|
|
127
|
+
# Don't disable SRTM entirely for single point failures
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
# Fallback to original elevation or 0
|
|
131
|
+
original_elevation = (
|
|
132
|
+
point.elevation if hasattr(point, "elevation") and point.elevation is not None else 0
|
|
133
|
+
)
|
|
134
|
+
return round(original_elevation, 1)
|
|
135
|
+
|
|
136
|
+
@staticmethod
|
|
137
|
+
def _calculate_distance(point1: GPXTrackPoint, point2: GPXTrackPoint) -> float:
|
|
138
|
+
"""Calculate distance between two GPX points in meters using Haversine formula."""
|
|
139
|
+
try:
|
|
140
|
+
lat1, lon1 = math.radians(point1.latitude), math.radians(point1.longitude)
|
|
141
|
+
lat2, lon2 = math.radians(point2.latitude), math.radians(point2.longitude)
|
|
142
|
+
|
|
143
|
+
dlat = lat2 - lat1
|
|
144
|
+
dlon = lon2 - lon1
|
|
145
|
+
|
|
146
|
+
a = math.sin(dlat / 2) ** 2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2) ** 2
|
|
147
|
+
c = 2 * math.asin(math.sqrt(a))
|
|
148
|
+
|
|
149
|
+
# Earth's radius in meters
|
|
150
|
+
earth_radius = 6371000
|
|
151
|
+
return earth_radius * c
|
|
152
|
+
except (AttributeError, TypeError, ValueError):
|
|
153
|
+
# Handle cases where points might have invalid coordinates
|
|
154
|
+
return 0.0
|
|
155
|
+
|
|
156
|
+
def _optimize_track_points(
|
|
157
|
+
self, track_points: list[GPXTrackPoint] | list[GPXWaypoint]
|
|
158
|
+
) -> list[GPXTrackPoint]:
|
|
159
|
+
"""Optimize track points by removing close points and cleaning metadata."""
|
|
160
|
+
if not track_points:
|
|
161
|
+
return track_points
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
optimized_points = [track_points[0]] # Always keep first point
|
|
165
|
+
|
|
166
|
+
for point in track_points[1:]:
|
|
167
|
+
# Check distance to last kept point
|
|
168
|
+
if self._calculate_distance(optimized_points[-1], point) >= self.min_dist:
|
|
169
|
+
optimized_points.append(point)
|
|
170
|
+
|
|
171
|
+
# Always keep last point if it's different from the last kept point
|
|
172
|
+
if len(track_points) > 1 and optimized_points[-1] != track_points[-1]:
|
|
173
|
+
optimized_points.append(track_points[-1])
|
|
174
|
+
|
|
175
|
+
# Clean and optimize each point
|
|
176
|
+
for point in optimized_points:
|
|
177
|
+
try:
|
|
178
|
+
# Remove time information
|
|
179
|
+
point.time = None
|
|
180
|
+
|
|
181
|
+
# Round coordinates to 5 decimal places
|
|
182
|
+
if hasattr(point, "latitude") and point.latitude is not None:
|
|
183
|
+
point.latitude = round(point.latitude, 5)
|
|
184
|
+
if hasattr(point, "longitude") and point.longitude is not None:
|
|
185
|
+
point.longitude = round(point.longitude, 5)
|
|
186
|
+
|
|
187
|
+
# Set optimized elevation
|
|
188
|
+
point.elevation = self._get_adjusted_elevation(point)
|
|
189
|
+
|
|
190
|
+
# Remove unnecessary extensions and metadata
|
|
191
|
+
point.extensions = None
|
|
192
|
+
if hasattr(point, "symbol"):
|
|
193
|
+
point.symbol = None
|
|
194
|
+
if hasattr(point, "type"):
|
|
195
|
+
point.type = None
|
|
196
|
+
point.comment = None
|
|
197
|
+
point.description = None
|
|
198
|
+
point.source = None
|
|
199
|
+
point.link = None
|
|
200
|
+
point.link_text = None
|
|
201
|
+
point.link_type = None
|
|
202
|
+
point.horizontal_dilution = None
|
|
203
|
+
point.vertical_dilution = None
|
|
204
|
+
point.position_dilution = None
|
|
205
|
+
point.age_of_dgps_data = None
|
|
206
|
+
point.dgps_id = None
|
|
207
|
+
except Exception as e:
|
|
208
|
+
self.logger.warning(f"Error optimizing point: {e}")
|
|
209
|
+
continue
|
|
210
|
+
|
|
211
|
+
return optimized_points
|
|
212
|
+
|
|
213
|
+
except Exception as e:
|
|
214
|
+
self.logger.error(f"Error optimizing track points: {e}")
|
|
215
|
+
self.logger.debug(f"Full traceback:\n{traceback.format_exc()}")
|
|
216
|
+
return track_points # Return original points if optimization fails
|
|
217
|
+
|
|
218
|
+
def _get_input_files(self) -> list[Path]:
|
|
219
|
+
"""Get all GPX/KML files from input (file, folder, or zip)."""
|
|
220
|
+
input_files = []
|
|
221
|
+
for input_path_str in self.input:
|
|
222
|
+
try:
|
|
223
|
+
input_path = Path(input_path_str)
|
|
224
|
+
self.logger.debug(f"Input path: {input_path.absolute()}")
|
|
225
|
+
|
|
226
|
+
if input_path.is_file():
|
|
227
|
+
if input_path.suffix.lower() == ".gpx":
|
|
228
|
+
input_files.append(input_path)
|
|
229
|
+
elif input_path.suffix.lower() == ".kml":
|
|
230
|
+
input_files.append(input_path)
|
|
231
|
+
elif input_path.suffix.lower() == ".zip":
|
|
232
|
+
input_files.extend(self._extract_gpx_kml_from_zip(input_path))
|
|
233
|
+
elif input_path.is_dir():
|
|
234
|
+
# Get all GPX/KML files in directory
|
|
235
|
+
input_files.extend(input_path.glob("*.gpx"))
|
|
236
|
+
input_files.extend(input_path.glob("*.kml"))
|
|
237
|
+
|
|
238
|
+
# Get GPX/KML files from ZIP files in directory
|
|
239
|
+
for zip_file in input_path.glob("*.zip"):
|
|
240
|
+
input_files.extend(self._extract_gpx_kml_from_zip(zip_file))
|
|
241
|
+
except Exception as e:
|
|
242
|
+
self.logger.error(f"Error processing input path {input_path}: {e}")
|
|
243
|
+
self.logger.debug(f"Full traceback:\n{traceback.format_exc()}")
|
|
244
|
+
continue
|
|
245
|
+
|
|
246
|
+
return input_files
|
|
247
|
+
|
|
248
|
+
def _extract_gpx_kml_from_zip(self, zip_path: Path) -> list[Path]:
|
|
249
|
+
"""Extract GPX/KML files from ZIP archive to temporary location."""
|
|
250
|
+
extracted_files = []
|
|
251
|
+
temp_dir = Path.cwd() / "temp_extracted_files"
|
|
252
|
+
|
|
253
|
+
try:
|
|
254
|
+
temp_dir.mkdir(exist_ok=True)
|
|
255
|
+
|
|
256
|
+
with zipfile.ZipFile(zip_path, "r") as zip_ref:
|
|
257
|
+
for file_info in zip_ref.infolist():
|
|
258
|
+
if file_info.filename.lower().endswith((".gpx", ".kml")):
|
|
259
|
+
extracted_path = temp_dir / Path(file_info.filename).name
|
|
260
|
+
with open(extracted_path, "wb") as f:
|
|
261
|
+
f.write(zip_ref.read(file_info.filename))
|
|
262
|
+
extracted_files.append(extracted_path)
|
|
263
|
+
except Exception as e:
|
|
264
|
+
self.logger.error(f"Error extracting ZIP file {zip_path}: {e}")
|
|
265
|
+
self.logger.debug(f"Full traceback:\n{traceback.format_exc()}")
|
|
266
|
+
|
|
267
|
+
return extracted_files
|
|
268
|
+
|
|
269
|
+
def _load_gpx_file(self, gpx_path: Path) -> GPX | None:
|
|
270
|
+
"""Load and parse GPX file."""
|
|
271
|
+
try:
|
|
272
|
+
with open(gpx_path, "r", encoding="utf-8") as f:
|
|
273
|
+
return gpxpy.parse(f)
|
|
274
|
+
except GPXXMLSyntaxException as e:
|
|
275
|
+
self.logger.error(f"Error parsing GPX file {gpx_path}: {e}")
|
|
276
|
+
return None
|
|
277
|
+
except Exception as e:
|
|
278
|
+
self.logger.error(f"Error loading GPX file {gpx_path}: {e}")
|
|
279
|
+
self.logger.debug(f"Full traceback:\n{traceback.format_exc()}")
|
|
280
|
+
return None
|
|
281
|
+
|
|
282
|
+
def _load_kml_file(self, kml_path: Path) -> GPX | None:
|
|
283
|
+
"""Load and parse KML file, converting it to a GPX object."""
|
|
284
|
+
if not KML_AVAILABLE:
|
|
285
|
+
self.logger.error("fastkml library is not available to process KML files.")
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
try:
|
|
289
|
+
with open(kml_path, "r", encoding="utf-8") as f:
|
|
290
|
+
doc = f.read()
|
|
291
|
+
|
|
292
|
+
k = kml.KML()
|
|
293
|
+
k.from_string(doc)
|
|
294
|
+
|
|
295
|
+
gpx = gpxpy.gpx.GPX()
|
|
296
|
+
|
|
297
|
+
# Iterate through KML features and convert to GPX tracks/waypoints
|
|
298
|
+
for feature in k.features():
|
|
299
|
+
if isinstance(feature, Document) or isinstance(feature, Folder):
|
|
300
|
+
for sub_feature in feature.features():
|
|
301
|
+
self._process_kml_feature(sub_feature, gpx)
|
|
302
|
+
else:
|
|
303
|
+
self._process_kml_feature(feature, gpx)
|
|
304
|
+
|
|
305
|
+
self.logger.info(f"Successfully loaded and converted KML file {kml_path} to GPX.")
|
|
306
|
+
return gpx
|
|
307
|
+
|
|
308
|
+
except Exception as e:
|
|
309
|
+
self.logger.error(f"Error loading or converting KML file {kml_path}: {e}")
|
|
310
|
+
self.logger.debug(f"Full traceback:\n{traceback.format_exc()}")
|
|
311
|
+
return None
|
|
312
|
+
|
|
313
|
+
def _process_kml_feature(self, feature, gpx: GPX):
|
|
314
|
+
"""Recursively process KML features to extract points and add to GPX."""
|
|
315
|
+
if isinstance(feature, Placemark):
|
|
316
|
+
if feature.geometry is not None:
|
|
317
|
+
if isinstance(feature.geometry, Point):
|
|
318
|
+
waypoint = gpxpy.gpx.GPXWaypoint(
|
|
319
|
+
latitude=feature.geometry.y,
|
|
320
|
+
longitude=feature.geometry.x,
|
|
321
|
+
elevation=feature.geometry.z if feature.geometry.has_z else None,
|
|
322
|
+
name=feature.name,
|
|
323
|
+
description=feature.description,
|
|
324
|
+
)
|
|
325
|
+
gpx.waypoints.append(waypoint)
|
|
326
|
+
elif isinstance(feature.geometry, LineString):
|
|
327
|
+
gpx_track = gpxpy.gpx.GPXTrack()
|
|
328
|
+
gpx_track.name = feature.name
|
|
329
|
+
gpx_segment = gpxpy.gpx.GPXTrackSegment()
|
|
330
|
+
for coord in feature.geometry.coords:
|
|
331
|
+
point = gpxpy.gpx.GPXTrackPoint(
|
|
332
|
+
latitude=coord[1],
|
|
333
|
+
longitude=coord[0],
|
|
334
|
+
elevation=coord[2] if len(coord) > 2 else None,
|
|
335
|
+
)
|
|
336
|
+
gpx_segment.points.append(point)
|
|
337
|
+
if gpx_segment.points:
|
|
338
|
+
gpx_track.segments.append(gpx_segment)
|
|
339
|
+
if gpx_track.segments:
|
|
340
|
+
gpx.tracks.append(gpx_track)
|
|
341
|
+
elif isinstance(feature, Document) or isinstance(feature, Folder):
|
|
342
|
+
for sub_feature in feature.features():
|
|
343
|
+
self._process_kml_feature(sub_feature, gpx)
|
|
344
|
+
|
|
345
|
+
def _save_gpx_file(
|
|
346
|
+
self, gpx: GPX, output_path: Path, original_file: Path | None = None
|
|
347
|
+
) -> Path:
|
|
348
|
+
"""Save GPX object to file and return the path."""
|
|
349
|
+
try:
|
|
350
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
351
|
+
f.write(gpx.to_xml())
|
|
352
|
+
if original_file and original_file.exists():
|
|
353
|
+
self.logger.info(
|
|
354
|
+
f"Original file size: {Path(original_file).stat().st_size / 1024:.2f} KB"
|
|
355
|
+
)
|
|
356
|
+
self.logger.info(f"Processed file size: {output_path.stat().st_size / 1024:.2f} KB")
|
|
357
|
+
return output_path
|
|
358
|
+
except Exception as e:
|
|
359
|
+
self.logger.error(f"Error saving GPX file {output_path}: {e}")
|
|
360
|
+
self.logger.debug(f"Full traceback:\n{traceback.format_exc()}")
|
|
361
|
+
return None
|
|
362
|
+
|
|
363
|
+
def compress_files(self) -> list[str]:
|
|
364
|
+
"""Shrink the size of all given gpx/kml files in self.input."""
|
|
365
|
+
generated_files = []
|
|
366
|
+
try:
|
|
367
|
+
input_files = self._get_input_files()
|
|
368
|
+
output_folder = self._get_output_folder()
|
|
369
|
+
|
|
370
|
+
self.logger.info(f"Processing {len(input_files)} GPX/KML files for compression...")
|
|
371
|
+
|
|
372
|
+
for input_file in input_files:
|
|
373
|
+
try:
|
|
374
|
+
gpx = None
|
|
375
|
+
if input_file.suffix.lower() == ".gpx":
|
|
376
|
+
gpx = self._load_gpx_file(input_file)
|
|
377
|
+
elif input_file.suffix.lower() == ".kml":
|
|
378
|
+
gpx = self._load_kml_file(input_file)
|
|
379
|
+
|
|
380
|
+
if gpx is None:
|
|
381
|
+
continue
|
|
382
|
+
|
|
383
|
+
# process and clean waypoints
|
|
384
|
+
for waypoint in gpx.waypoints:
|
|
385
|
+
self._optimize_waypoint(waypoint)
|
|
386
|
+
|
|
387
|
+
# Process all tracks
|
|
388
|
+
for track in gpx.tracks:
|
|
389
|
+
for segment in track.segments:
|
|
390
|
+
segment.points = self._optimize_track_points(segment.points)
|
|
391
|
+
|
|
392
|
+
# Process all routes
|
|
393
|
+
for route in gpx.routes:
|
|
394
|
+
route.points = self._optimize_track_points(route.points)
|
|
395
|
+
|
|
396
|
+
# Clean GPX metadata
|
|
397
|
+
gpx.time = None
|
|
398
|
+
gpx.extensions = None
|
|
399
|
+
|
|
400
|
+
# Save compressed file
|
|
401
|
+
output_path = output_folder / f"compressed_{input_file.stem}.gpx"
|
|
402
|
+
saved_path = self._save_gpx_file(gpx, output_path, input_file)
|
|
403
|
+
if saved_path:
|
|
404
|
+
generated_files.append(str(saved_path))
|
|
405
|
+
self.logger.info(f"Compressed: {input_file.name} -> {output_path}")
|
|
406
|
+
|
|
407
|
+
except Exception as e:
|
|
408
|
+
self.logger.error(f"Error processing file {input_file}: {e}")
|
|
409
|
+
self.logger.debug(f"Full traceback:\n{traceback.format_exc()}")
|
|
410
|
+
continue
|
|
411
|
+
return generated_files
|
|
412
|
+
|
|
413
|
+
except Exception as e:
|
|
414
|
+
self.logger.error(f"Error in compress_files: {e}")
|
|
415
|
+
self.logger.debug(f"Full traceback:\n{traceback.format_exc()}")
|
|
416
|
+
raise
|
|
417
|
+
|
|
418
|
+
def _optimize_waypoint(self, waypoint: GPXWaypoint) -> GPXWaypoint:
|
|
419
|
+
"""Optimize waypoint with error handling."""
|
|
420
|
+
try:
|
|
421
|
+
# Round coordinates and elevation
|
|
422
|
+
if hasattr(waypoint, "latitude") and waypoint.latitude is not None:
|
|
423
|
+
waypoint.latitude = round(waypoint.latitude, 5)
|
|
424
|
+
if hasattr(waypoint, "longitude") and waypoint.longitude is not None:
|
|
425
|
+
waypoint.longitude = round(waypoint.longitude, 5)
|
|
426
|
+
|
|
427
|
+
waypoint.elevation = self._get_adjusted_elevation(waypoint)
|
|
428
|
+
|
|
429
|
+
# Clean metadata
|
|
430
|
+
waypoint.time = None
|
|
431
|
+
waypoint.extensions = None
|
|
432
|
+
if hasattr(waypoint, "symbol"):
|
|
433
|
+
waypoint.symbol = None
|
|
434
|
+
if hasattr(waypoint, "type"):
|
|
435
|
+
waypoint.type = None
|
|
436
|
+
waypoint.comment = None
|
|
437
|
+
waypoint.description = None
|
|
438
|
+
waypoint.source = None
|
|
439
|
+
waypoint.link = None
|
|
440
|
+
waypoint.link_text = None
|
|
441
|
+
waypoint.link_type = None
|
|
442
|
+
return waypoint
|
|
443
|
+
except Exception as e:
|
|
444
|
+
self.logger.warning(f"Error optimizing waypoint: {e}")
|
|
445
|
+
return waypoint
|
|
446
|
+
|
|
447
|
+
def merge_files(self) -> list[str]:
|
|
448
|
+
"""Merge all files of self.input into one gpx file with reduced resolution."""
|
|
449
|
+
generated_files = []
|
|
450
|
+
try:
|
|
451
|
+
input_files = self._get_input_files()
|
|
452
|
+
output_folder = self._get_output_folder()
|
|
453
|
+
|
|
454
|
+
if not input_files:
|
|
455
|
+
self.logger.error("No GPX/KML files found to merge.")
|
|
456
|
+
return []
|
|
457
|
+
|
|
458
|
+
self.logger.info(f"Merging {len(input_files)} GPX/KML files...")
|
|
459
|
+
|
|
460
|
+
# Create new GPX object
|
|
461
|
+
merged_gpx = gpxpy.gpx.GPX()
|
|
462
|
+
merged_gpx.name = "Merged GPX Tracks"
|
|
463
|
+
merged_gpx.description = f"Merged from {len(input_files)} GPX/KML files."
|
|
464
|
+
|
|
465
|
+
track_counter = 1
|
|
466
|
+
|
|
467
|
+
for input_file in input_files:
|
|
468
|
+
try:
|
|
469
|
+
gpx = None
|
|
470
|
+
if input_file.suffix.lower() == ".gpx":
|
|
471
|
+
gpx = self._load_gpx_file(input_file)
|
|
472
|
+
elif input_file.suffix.lower() == ".kml":
|
|
473
|
+
gpx = self._load_kml_file(input_file)
|
|
474
|
+
|
|
475
|
+
if gpx is None:
|
|
476
|
+
continue
|
|
477
|
+
|
|
478
|
+
# Add all waypoints from this file
|
|
479
|
+
for waypoint in gpx.waypoints:
|
|
480
|
+
waypoint = self._optimize_waypoint(waypoint)
|
|
481
|
+
waypoint.name = f"{waypoint.name or 'Waypoint'}_{track_counter}"
|
|
482
|
+
merged_gpx.waypoints.append(waypoint)
|
|
483
|
+
|
|
484
|
+
# Add all tracks from this file
|
|
485
|
+
for track in gpx.tracks:
|
|
486
|
+
new_track = gpxpy.gpx.GPXTrack()
|
|
487
|
+
new_track.name = (
|
|
488
|
+
f"{track.name or 'Track'}_{input_file.stem}_{track_counter}"
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
for segment in track.segments:
|
|
492
|
+
optimized_points = self._optimize_track_points(segment.points)
|
|
493
|
+
if optimized_points:
|
|
494
|
+
new_segment = gpxpy.gpx.GPXTrackSegment()
|
|
495
|
+
new_segment.points = optimized_points
|
|
496
|
+
new_track.segments.append(new_segment)
|
|
497
|
+
|
|
498
|
+
if new_track.segments:
|
|
499
|
+
merged_gpx.tracks.append(new_track)
|
|
500
|
+
track_counter += 1
|
|
501
|
+
|
|
502
|
+
# Add all routes from this file
|
|
503
|
+
for route in gpx.routes:
|
|
504
|
+
new_route = gpxpy.gpx.GPXRoute()
|
|
505
|
+
new_route.name = (
|
|
506
|
+
f"{route.name or 'Route'}_{input_file.stem}_{track_counter}"
|
|
507
|
+
)
|
|
508
|
+
new_route.points = self._optimize_track_points(route.points)
|
|
509
|
+
|
|
510
|
+
if new_route.points:
|
|
511
|
+
merged_gpx.routes.append(new_route)
|
|
512
|
+
track_counter += 1
|
|
513
|
+
|
|
514
|
+
except Exception as e:
|
|
515
|
+
self.logger.error(f"Error processing file {input_file} during merge: {e}")
|
|
516
|
+
self.logger.debug(f"Full traceback:\n{traceback.format_exc()}")
|
|
517
|
+
continue
|
|
518
|
+
|
|
519
|
+
# Save merged file
|
|
520
|
+
output_path = output_folder / "merged_tracks.gpx"
|
|
521
|
+
saved_path = self._save_gpx_file(merged_gpx, output_path)
|
|
522
|
+
if saved_path:
|
|
523
|
+
generated_files.append(str(saved_path))
|
|
524
|
+
self.logger.info(f"Merged file saved: {output_path}")
|
|
525
|
+
return generated_files
|
|
526
|
+
|
|
527
|
+
except Exception as e:
|
|
528
|
+
self.logger.error(f"Error in merge_files: {e}")
|
|
529
|
+
self.logger.debug(f"Full traceback:\n{traceback.format_exc()}")
|
|
530
|
+
raise
|
|
531
|
+
|
|
532
|
+
def extract_pois(self) -> list[str]:
|
|
533
|
+
"""Merge every starting point of each track in all files
|
|
534
|
+
into one gpx file with many pois."""
|
|
535
|
+
generated_files = []
|
|
536
|
+
try:
|
|
537
|
+
input_files = self._get_input_files()
|
|
538
|
+
output_folder = self._get_output_folder()
|
|
539
|
+
|
|
540
|
+
if not input_files:
|
|
541
|
+
self.logger.error("No GPX/KML files found to extract POIs from.")
|
|
542
|
+
return []
|
|
543
|
+
|
|
544
|
+
self.logger.info(f"Extracting POIs from {len(input_files)} GPX/KML files...")
|
|
545
|
+
|
|
546
|
+
# Create new GPX object for waypoints
|
|
547
|
+
poi_gpx = gpxpy.gpx.GPX()
|
|
548
|
+
poi_gpx.name = "Extracted Track Starting Points"
|
|
549
|
+
poi_gpx.description = f"Starting points extracted from {len(input_files)} GPX/KML files"
|
|
550
|
+
|
|
551
|
+
poi_counter = 1
|
|
552
|
+
|
|
553
|
+
for input_file in input_files:
|
|
554
|
+
try:
|
|
555
|
+
gpx = None
|
|
556
|
+
if input_file.suffix.lower() == ".gpx":
|
|
557
|
+
gpx = self._load_gpx_file(input_file)
|
|
558
|
+
elif input_file.suffix.lower() == ".kml":
|
|
559
|
+
gpx = self._load_kml_file(input_file)
|
|
560
|
+
|
|
561
|
+
if gpx is None:
|
|
562
|
+
continue
|
|
563
|
+
|
|
564
|
+
# Extract starting points from tracks
|
|
565
|
+
for track_idx, track in enumerate(gpx.tracks):
|
|
566
|
+
for _segment_idx, segment in enumerate(track.segments):
|
|
567
|
+
if segment.points:
|
|
568
|
+
start_point = segment.points[0]
|
|
569
|
+
|
|
570
|
+
# Create waypoint from starting point
|
|
571
|
+
waypoint = gpxpy.gpx.GPXWaypoint(
|
|
572
|
+
latitude=round(start_point.latitude, 5),
|
|
573
|
+
longitude=round(start_point.longitude, 5),
|
|
574
|
+
elevation=self._get_adjusted_elevation(start_point),
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
track_name = track.name or f"Track_{track_idx + 1}"
|
|
578
|
+
waypoint.name = f"POI_{poi_counter:03d}"
|
|
579
|
+
waypoint.description = (
|
|
580
|
+
f"Start of {track_name} from {input_file.name}"
|
|
581
|
+
)
|
|
582
|
+
waypoint.type = "Track Start"
|
|
583
|
+
|
|
584
|
+
poi_gpx.waypoints.append(waypoint)
|
|
585
|
+
poi_counter += 1
|
|
586
|
+
|
|
587
|
+
# Extract starting points from routes
|
|
588
|
+
for route_idx, route in enumerate(gpx.routes):
|
|
589
|
+
if route.points:
|
|
590
|
+
start_point = route.points[0]
|
|
591
|
+
|
|
592
|
+
# Create waypoint from starting point
|
|
593
|
+
waypoint = gpxpy.gpx.GPXWaypoint(
|
|
594
|
+
latitude=round(start_point.latitude, 5),
|
|
595
|
+
longitude=round(start_point.longitude, 5),
|
|
596
|
+
elevation=self._get_adjusted_elevation(start_point),
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
route_name = route.name or f"Route_{route_idx + 1}"
|
|
600
|
+
waypoint.name = f"POI_{poi_counter:03d}"
|
|
601
|
+
waypoint.description = f"Start of {route_name} from {input_file.name}"
|
|
602
|
+
waypoint.type = "Route Start"
|
|
603
|
+
|
|
604
|
+
poi_gpx.waypoints.append(waypoint)
|
|
605
|
+
poi_counter += 1
|
|
606
|
+
|
|
607
|
+
except Exception as e:
|
|
608
|
+
self.logger.error(
|
|
609
|
+
f"Error processing file {input_file} during POI extraction: {e}"
|
|
610
|
+
)
|
|
611
|
+
self.logger.debug(f"Full traceback:\n{traceback.format_exc()}")
|
|
612
|
+
continue
|
|
613
|
+
|
|
614
|
+
# Save POI file
|
|
615
|
+
output_path = output_folder / "extracted_pois.gpx"
|
|
616
|
+
saved_path = self._save_gpx_file(poi_gpx, output_path)
|
|
617
|
+
if saved_path:
|
|
618
|
+
generated_files.append(str(saved_path))
|
|
619
|
+
self.logger.info(
|
|
620
|
+
f"POI file saved with {len(poi_gpx.waypoints)} waypoints: {output_path}"
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
# Clean up temporary files
|
|
624
|
+
temp_dir = Path.cwd() / "temp_extracted_files" # Changed folder name
|
|
625
|
+
if temp_dir.exists():
|
|
626
|
+
import shutil
|
|
627
|
+
|
|
628
|
+
shutil.rmtree(temp_dir)
|
|
629
|
+
return generated_files
|
|
630
|
+
|
|
631
|
+
except Exception as e:
|
|
632
|
+
self.logger.error(f"Error in extract_pois: {e}")
|
|
633
|
+
self.logger.debug(f"Full traceback:\n{traceback.format_exc()}")
|
|
634
|
+
raise
|