mirror of
https://github.com/sstent/FitTrack_GarminSync.git
synced 2026-01-26 09:01:53 +00:00
154 lines
5.5 KiB
Python
154 lines
5.5 KiB
Python
import xml.etree.ElementTree as ET
|
|
from datetime import datetime
|
|
import math
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
def parse_gpx_file(file_path):
|
|
"""
|
|
Parse GPX file to extract activity metrics.
|
|
Returns: Dictionary of activity metrics or None if parsing fails
|
|
"""
|
|
try:
|
|
tree = ET.parse(file_path)
|
|
root = tree.getroot()
|
|
|
|
# GPX namespace
|
|
ns = {'gpx': 'http://www.topografix.com/GPX/1/1'}
|
|
|
|
# Extract metadata
|
|
metadata = root.find('gpx:metadata', ns)
|
|
if metadata is not None:
|
|
time_elem = metadata.find('gpx:time', ns)
|
|
if time_elem is not None:
|
|
start_time = datetime.fromisoformat(time_elem.text.replace('Z', '+00:00'))
|
|
else:
|
|
# Fallback to first track point time
|
|
trkpt = root.find('.//gpx:trkpt', ns)
|
|
if trkpt is not None:
|
|
time_elem = trkpt.find('gpx:time', ns)
|
|
if time_elem is not None:
|
|
start_time = datetime.fromisoformat(time_elem.text.replace('Z', '+00:00'))
|
|
else:
|
|
logger.error(f"No track points found in GPX file: {file_path}")
|
|
return None
|
|
|
|
# Get all track points
|
|
track_points = root.findall('.//gpx:trkpt', ns)
|
|
if not track_points:
|
|
logger.warning(f"No track points found in GPX file: {file_path}")
|
|
return None
|
|
|
|
# Activity metrics
|
|
total_distance = 0.0
|
|
start_elevation = None
|
|
min_elevation = float('inf')
|
|
max_elevation = float('-inf')
|
|
elevations = []
|
|
heart_rates = []
|
|
cadences = []
|
|
|
|
prev_point = None
|
|
for point in track_points:
|
|
# Parse coordinates
|
|
lat = float(point.get('lat'))
|
|
lon = float(point.get('lon'))
|
|
|
|
# Parse elevation
|
|
ele_elem = point.find('gpx:ele', ns)
|
|
ele = float(ele_elem.text) if ele_elem is not None else None
|
|
if ele is not None:
|
|
elevations.append(ele)
|
|
if start_elevation is None:
|
|
start_elevation = ele
|
|
min_elevation = min(min_elevation, ele)
|
|
max_elevation = max(max_elevation, ele)
|
|
|
|
# Parse time
|
|
time_elem = point.find('gpx:time', ns)
|
|
time = datetime.fromisoformat(time_elem.text.replace('Z', '+00:00')) if time_elem else None
|
|
|
|
# Parse extensions (heart rate, cadence, etc.)
|
|
extensions = point.find('gpx:extensions', ns)
|
|
if extensions is not None:
|
|
# Garmin TrackPointExtension
|
|
tpe = extensions.find('gpx:TrackPointExtension', ns)
|
|
if tpe is not None:
|
|
hr_elem = tpe.find('gpx:hr', ns)
|
|
if hr_elem is not None:
|
|
heart_rates.append(int(hr_elem.text))
|
|
|
|
cad_elem = tpe.find('gpx:cad', ns)
|
|
if cad_elem is not None:
|
|
cadences.append(int(cad_elem.text))
|
|
|
|
# Calculate distance from previous point
|
|
if prev_point:
|
|
prev_lat, prev_lon = prev_point
|
|
total_distance += haversine(prev_lat, prev_lon, lat, lon)
|
|
|
|
prev_point = (lat, lon)
|
|
|
|
# Calculate duration
|
|
if 'start_time' in locals() and time is not None:
|
|
duration = (time - start_time).total_seconds()
|
|
else:
|
|
duration = None
|
|
|
|
# Calculate elevation gain/loss
|
|
elevation_gain = 0
|
|
elevation_loss = 0
|
|
if elevations:
|
|
prev_ele = elevations[0]
|
|
for ele in elevations[1:]:
|
|
if ele > prev_ele:
|
|
elevation_gain += ele - prev_ele
|
|
else:
|
|
elevation_loss += prev_ele - ele
|
|
prev_ele = ele
|
|
|
|
# Calculate averages
|
|
avg_heart_rate = sum(heart_rates) / len(heart_rates) if heart_rates else None
|
|
avg_cadence = sum(cadences) / len(cadences) if cadences else None
|
|
|
|
return {
|
|
"activityType": {"typeKey": "other"},
|
|
"summaryDTO": {
|
|
"startTime": start_time.isoformat() if 'start_time' in locals() else None,
|
|
"duration": duration,
|
|
"distance": total_distance,
|
|
"elevationGain": elevation_gain,
|
|
"elevationLoss": elevation_loss,
|
|
"minElevation": min_elevation,
|
|
"maxElevation": max_elevation,
|
|
"maxHR": max(heart_rates) if heart_rates else None,
|
|
"avgHR": avg_heart_rate,
|
|
"cadence": avg_cadence,
|
|
"calories": None # Calories not typically in GPX files
|
|
}
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error parsing GPX file {file_path}: {str(e)}")
|
|
return None
|
|
|
|
def haversine(lat1, lon1, lat2, lon2):
|
|
"""
|
|
Calculate the great circle distance between two points
|
|
on the earth (specified in decimal degrees)
|
|
Returns distance in meters
|
|
"""
|
|
# Convert decimal degrees to radians
|
|
lon1, lat1, lon2, lat2 = map(math.radians, [lon1, lat1, lon2, lat2])
|
|
|
|
# Haversine formula
|
|
dlon = lon2 - lon1
|
|
dlat = lat2 - lat1
|
|
a = math.sin(dlat/2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon/2)**2
|
|
c = 2 * math.asin(math.sqrt(a))
|
|
|
|
# Radius of earth in meters
|
|
r = 6371000
|
|
return c * r
|