""" GPX file processing utilities for extracting route statistics """ import xml.etree.ElementTree as ET import math import os from typing import Dict, Optional, Tuple def calculate_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float: """ Calculate the great circle distance between two points on the earth (specified in decimal degrees) Returns distance in kilometers """ # Convert decimal degrees to radians lat1, lon1, lat2, lon2 = map(math.radians, [lat1, lon1, lat2, lon2]) # Haversine formula dlat = lat2 - lat1 dlon = lon2 - lon1 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 kilometers r = 6371 return c * r def extract_gpx_statistics(file_path: str) -> Optional[Dict]: """ Extract statistics from a GPX file Returns: Dictionary with keys: total_distance, elevation_gain, max_elevation, min_elevation, total_points, or None if file cannot be processed """ if not os.path.exists(file_path): return None try: # Parse GPX file tree = ET.parse(file_path) root = tree.getroot() # Handle GPX namespace namespace = {'gpx': 'http://www.topografix.com/GPX/1/1'} if not root.tag.endswith('gpx'): # Try without namespace namespace = {} # Find track points track_points = [] # Look for track points in tracks tracks = root.findall('.//gpx:trk', namespace) if namespace else root.findall('.//trk') for track in tracks: segments = track.findall('.//gpx:trkseg', namespace) if namespace else track.findall('.//trkseg') for segment in segments: points = segment.findall('.//gpx:trkpt', namespace) if namespace else segment.findall('.//trkpt') for point in points: lat = float(point.get('lat')) lon = float(point.get('lon')) # Get elevation if available ele_elem = point.find('gpx:ele', namespace) if namespace else point.find('ele') elevation = float(ele_elem.text) if ele_elem is not None and ele_elem.text else 0.0 track_points.append({ 'lat': lat, 'lon': lon, 'elevation': elevation }) # Also look for waypoints if no track points found if not track_points: waypoints = root.findall('.//gpx:wpt', namespace) if namespace else root.findall('.//wpt') for point in waypoints: lat = float(point.get('lat')) lon = float(point.get('lon')) ele_elem = point.find('gpx:ele', namespace) if namespace else point.find('ele') elevation = float(ele_elem.text) if ele_elem is not None and ele_elem.text else 0.0 track_points.append({ 'lat': lat, 'lon': lon, 'elevation': elevation }) if not track_points: return { 'total_distance': 0.0, 'elevation_gain': 0.0, 'max_elevation': 0.0, 'min_elevation': 0.0, 'total_points': 0 } # Calculate statistics total_distance = 0.0 elevation_gain = 0.0 elevations = [point['elevation'] for point in track_points if point['elevation'] > 0] # Calculate distance and elevation gain for i in range(1, len(track_points)): current = track_points[i] previous = track_points[i-1] # Distance distance = calculate_distance( previous['lat'], previous['lon'], current['lat'], current['lon'] ) total_distance += distance # Elevation gain (only uphill) if current['elevation'] > 0 and previous['elevation'] > 0: elevation_diff = current['elevation'] - previous['elevation'] if elevation_diff > 0: elevation_gain += elevation_diff # Elevation statistics max_elevation = max(elevations) if elevations else 0.0 min_elevation = min(elevations) if elevations else 0.0 return { 'total_distance': round(total_distance, 2), 'elevation_gain': round(elevation_gain, 1), 'max_elevation': round(max_elevation, 1), 'min_elevation': round(min_elevation, 1), 'total_points': len(track_points) } except Exception as e: print(f"Error processing GPX file {file_path}: {e}") return None def process_gpx_file(gpx_file_record) -> bool: """ Process a GPXFile record and update its statistics Args: gpx_file_record: GPXFile model instance Returns: True if processing was successful, False otherwise """ from flask import current_app # Build file path if gpx_file_record.post.media_folder: file_path = os.path.join( current_app.root_path, 'static', 'media', 'posts', gpx_file_record.post.media_folder, 'gpx', gpx_file_record.filename ) else: file_path = os.path.join( current_app.root_path, 'static', 'uploads', 'gpx', gpx_file_record.filename ) # Extract statistics stats = extract_gpx_statistics(file_path) if stats is None: return False # Update the record gpx_file_record.total_distance = stats['total_distance'] gpx_file_record.elevation_gain = stats['elevation_gain'] gpx_file_record.max_elevation = stats['max_elevation'] gpx_file_record.min_elevation = stats['min_elevation'] gpx_file_record.total_points = stats['total_points'] return True