Files
FitTrack_GarminSync/examples/GarminSync/garminsync/parsers/gpx_parser.py

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