feat: Add interactive map functionality with Leaflet.js
- 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
This commit is contained in:
176
app/utils/gpx_processor.py
Normal file
176
app/utils/gpx_processor.py
Normal file
@@ -0,0 +1,176 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user