- Implemented interactive map card with expand functionality - Added Leaflet.js integration with OpenStreetMap tiles - Created expandable map modal (80% screen coverage) - Fixed cover image display on community page - Enhanced post detail page with interactive route visualization - Added proper error handling and fallback content - Cleaned up JavaScript structure and removed duplicate code - Updated community index template to use cover images - Added GPX file processing utilities - Fixed indentation error in run.py Map features: - Country-level positioning (Romania default) - Zoom controls and interactive navigation - Test marker with popup functionality - Expandable full-screen view with X button - Clean console logging for debugging - Responsive design with Tailwind CSS styling
177 lines
6.1 KiB
Python
177 lines
6.1 KiB
Python
"""
|
|
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
|