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:
ske087
2025-07-24 21:36:42 +03:00
parent 58e5d1b83d
commit 187254beca
16 changed files with 13626 additions and 119 deletions

176
app/utils/gpx_processor.py Normal file
View 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