Fix: Clean up map_iframe_single.html, remove debug overlay, ensure clean map rendering.
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
"""
|
||||
GPX file processing utilities for extracting route statistics
|
||||
GPX file processing utilities for extracting route statistics and creating map routes
|
||||
"""
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
import math
|
||||
import os
|
||||
from typing import Dict, Optional, Tuple
|
||||
import json
|
||||
import gpxpy
|
||||
from typing import Dict, Optional, Tuple, List
|
||||
from app import db
|
||||
|
||||
|
||||
def calculate_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
|
||||
@@ -174,3 +177,344 @@ def process_gpx_file(gpx_file_record) -> bool:
|
||||
gpx_file_record.total_points = stats['total_points']
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def simplify_coordinates(coordinates: List[List[float]], tolerance: float = 0.0001) -> List[List[float]]:
|
||||
"""
|
||||
Simplify coordinates using Douglas-Peucker algorithm
|
||||
Returns a reduced set of points while maintaining the general shape
|
||||
"""
|
||||
if len(coordinates) <= 2:
|
||||
return coordinates
|
||||
|
||||
def perpendicular_distance(point, line_start, line_end):
|
||||
"""Calculate perpendicular distance from point to line"""
|
||||
if line_start == line_end:
|
||||
return ((point[0] - line_start[0]) ** 2 + (point[1] - line_start[1]) ** 2) ** 0.5
|
||||
|
||||
# Calculate the perpendicular distance
|
||||
A = point[0] - line_start[0]
|
||||
B = point[1] - line_start[1]
|
||||
C = line_end[0] - line_start[0]
|
||||
D = line_end[1] - line_start[1]
|
||||
|
||||
dot = A * C + B * D
|
||||
len_sq = C * C + D * D
|
||||
|
||||
if len_sq == 0:
|
||||
return (A * A + B * B) ** 0.5
|
||||
|
||||
param = dot / len_sq
|
||||
|
||||
if param < 0:
|
||||
xx = line_start[0]
|
||||
yy = line_start[1]
|
||||
elif param > 1:
|
||||
xx = line_end[0]
|
||||
yy = line_end[1]
|
||||
else:
|
||||
xx = line_start[0] + param * C
|
||||
yy = line_start[1] + param * D
|
||||
|
||||
dx = point[0] - xx
|
||||
dy = point[1] - yy
|
||||
return (dx * dx + dy * dy) ** 0.5
|
||||
|
||||
def douglas_peucker(points, tolerance):
|
||||
"""Douglas-Peucker algorithm implementation"""
|
||||
if len(points) <= 2:
|
||||
return points
|
||||
|
||||
# Find the point with maximum distance
|
||||
max_dist = 0
|
||||
index = 0
|
||||
for i in range(1, len(points) - 1):
|
||||
dist = perpendicular_distance(points[i], points[0], points[-1])
|
||||
if dist > max_dist:
|
||||
index = i
|
||||
max_dist = dist
|
||||
|
||||
# If max distance is greater than tolerance, recursively simplify
|
||||
if max_dist > tolerance:
|
||||
# Recursive call
|
||||
rec_results1 = douglas_peucker(points[:index + 1], tolerance)
|
||||
rec_results2 = douglas_peucker(points[index:], tolerance)
|
||||
|
||||
# Build the result list
|
||||
result = rec_results1[:-1] + rec_results2
|
||||
return result
|
||||
else:
|
||||
return [points[0], points[-1]]
|
||||
|
||||
return douglas_peucker(coordinates, tolerance)
|
||||
|
||||
|
||||
def calculate_bounds(coordinates: List[List[float]]) -> Optional[Dict]:
|
||||
"""Calculate bounding box for coordinates"""
|
||||
if not coordinates:
|
||||
return None
|
||||
|
||||
lats = [coord[0] for coord in coordinates]
|
||||
lngs = [coord[1] for coord in coordinates]
|
||||
|
||||
return {
|
||||
'north': max(lats),
|
||||
'south': min(lats),
|
||||
'east': max(lngs),
|
||||
'west': min(lngs)
|
||||
}
|
||||
|
||||
|
||||
def create_map_route_from_gpx(gpx_file_id: int) -> bool:
|
||||
"""
|
||||
Process a GPX file and create/update corresponding MapRoute entry
|
||||
"""
|
||||
try:
|
||||
from app.models import MapRoute, GPXFile, Post
|
||||
from flask import current_app
|
||||
|
||||
# Get the GPX file record
|
||||
gpx_file = GPXFile.query.get(gpx_file_id)
|
||||
if not gpx_file:
|
||||
print(f"GPX file with ID {gpx_file_id} not found")
|
||||
return False
|
||||
|
||||
# Get the file path
|
||||
if gpx_file.post.media_folder:
|
||||
file_path = os.path.join(
|
||||
current_app.root_path, 'static', 'media', 'posts',
|
||||
gpx_file.post.media_folder, 'gpx', gpx_file.filename
|
||||
)
|
||||
else:
|
||||
file_path = os.path.join(
|
||||
current_app.root_path, 'static', 'uploads', 'gpx', gpx_file.filename
|
||||
)
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
print(f"GPX file not found: {file_path}")
|
||||
return False
|
||||
|
||||
# Parse GPX file using gpxpy for better coordinate extraction
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
gpx = gpxpy.parse(f)
|
||||
except Exception as e:
|
||||
print(f"Error parsing GPX file with gpxpy: {e}")
|
||||
return False
|
||||
|
||||
# Extract coordinates from all tracks and segments
|
||||
all_coordinates = []
|
||||
total_distance = 0
|
||||
elevations = []
|
||||
|
||||
for track in gpx.tracks:
|
||||
for segment in track.segments:
|
||||
prev_point = None
|
||||
|
||||
for point in segment.points:
|
||||
coord = [point.latitude, point.longitude]
|
||||
all_coordinates.append(coord)
|
||||
|
||||
if point.elevation is not None:
|
||||
elevations.append(point.elevation)
|
||||
|
||||
# Calculate distance
|
||||
if prev_point:
|
||||
distance = prev_point.distance_2d(point)
|
||||
if distance:
|
||||
total_distance += distance
|
||||
prev_point = point
|
||||
|
||||
# If no track points, try waypoints
|
||||
if not all_coordinates:
|
||||
for waypoint in gpx.waypoints:
|
||||
coord = [waypoint.latitude, waypoint.longitude]
|
||||
all_coordinates.append(coord)
|
||||
if waypoint.elevation is not None:
|
||||
elevations.append(waypoint.elevation)
|
||||
|
||||
if not all_coordinates:
|
||||
print("No coordinates found in GPX file")
|
||||
return False
|
||||
|
||||
# Calculate statistics
|
||||
elevation_gain = 0
|
||||
if elevations:
|
||||
for i in range(1, len(elevations)):
|
||||
gain = elevations[i] - elevations[i-1]
|
||||
if gain > 0:
|
||||
elevation_gain += gain
|
||||
|
||||
# Simplify coordinates for overview map
|
||||
simplified_coords = simplify_coordinates(all_coordinates, tolerance=0.001)
|
||||
|
||||
# Calculate bounds
|
||||
bounds = calculate_bounds(all_coordinates)
|
||||
if not bounds:
|
||||
print("Could not calculate bounds for coordinates")
|
||||
return False
|
||||
|
||||
# Create or update MapRoute
|
||||
existing_route = MapRoute.query.filter_by(post_id=gpx_file.post_id).first()
|
||||
|
||||
if existing_route:
|
||||
# Update existing route
|
||||
map_route = existing_route
|
||||
else:
|
||||
# Create new route
|
||||
map_route = MapRoute(post_id=gpx_file.post_id, gpx_file_id=gpx_file_id)
|
||||
|
||||
# Set route data
|
||||
map_route.coordinates = json.dumps(all_coordinates)
|
||||
map_route.simplified_coordinates = json.dumps(simplified_coords)
|
||||
|
||||
# Set start and end points
|
||||
map_route.start_latitude = all_coordinates[0][0]
|
||||
map_route.start_longitude = all_coordinates[0][1]
|
||||
map_route.end_latitude = all_coordinates[-1][0]
|
||||
map_route.end_longitude = all_coordinates[-1][1]
|
||||
|
||||
# Set bounds
|
||||
map_route.bounds_north = bounds['north']
|
||||
map_route.bounds_south = bounds['south']
|
||||
map_route.bounds_east = bounds['east']
|
||||
map_route.bounds_west = bounds['west']
|
||||
|
||||
# Set statistics
|
||||
map_route.total_distance = total_distance / 1000 if total_distance else 0 # Convert to kilometers
|
||||
map_route.elevation_gain = elevation_gain
|
||||
map_route.max_elevation = max(elevations) if elevations else 0
|
||||
map_route.min_elevation = min(elevations) if elevations else 0
|
||||
map_route.total_points = len(all_coordinates)
|
||||
map_route.simplified_points = len(simplified_coords)
|
||||
|
||||
# Save to database
|
||||
if not existing_route:
|
||||
db.session.add(map_route)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
print(f"Successfully created map route for GPX file {gpx_file.filename} (Post {gpx_file.post_id})")
|
||||
print(f"- Total points: {len(all_coordinates)}")
|
||||
print(f"- Simplified points: {len(simplified_coords)}")
|
||||
print(f"- Distance: {map_route.total_distance:.2f} km")
|
||||
print(f"- Elevation gain: {map_route.elevation_gain:.0f} m")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
print(f"Error creating map route for GPX file {gpx_file_id}: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
def process_post_approval(post_id: int) -> bool:
|
||||
"""
|
||||
Process all GPX files for a post when it gets approved
|
||||
Creates map routes for efficient map loading
|
||||
"""
|
||||
try:
|
||||
from app.models import Post, GPXFile
|
||||
|
||||
post = Post.query.get(post_id)
|
||||
if not post:
|
||||
print(f"Post with ID {post_id} not found")
|
||||
return False
|
||||
|
||||
# Get all GPX files for this post
|
||||
gpx_files = GPXFile.query.filter_by(post_id=post_id).all()
|
||||
|
||||
if not gpx_files:
|
||||
print(f"No GPX files found for post {post_id}")
|
||||
return True # Not an error if no GPX files
|
||||
|
||||
# Process the first GPX file (assuming one route per post)
|
||||
# If multiple files exist, you might want to merge them or process separately
|
||||
gpx_file = gpx_files[0]
|
||||
|
||||
success = create_map_route_from_gpx(gpx_file.id)
|
||||
|
||||
if success:
|
||||
print(f"Successfully processed post {post_id} approval - map route created")
|
||||
else:
|
||||
print(f"Failed to create map route for post {post_id}")
|
||||
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error processing post approval for post {post_id}: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
def get_all_map_routes() -> List[Dict]:
|
||||
"""
|
||||
Get all map routes for the community map
|
||||
Returns simplified data optimized for map display
|
||||
"""
|
||||
try:
|
||||
from app.models import MapRoute, Post
|
||||
|
||||
routes = MapRoute.query.join(Post).filter(Post.published == True).all()
|
||||
|
||||
map_data = []
|
||||
for route in routes:
|
||||
try:
|
||||
map_data.append({
|
||||
'id': route.id,
|
||||
'post_id': route.post_id,
|
||||
'post_title': route.post.title,
|
||||
'post_author': route.post.author.nickname,
|
||||
'coordinates': route.get_simplified_coordinates_json(),
|
||||
'start_point': route.get_start_point(),
|
||||
'end_point': route.get_end_point(),
|
||||
'bounds': route.get_bounds(),
|
||||
'stats': {
|
||||
'distance': route.total_distance,
|
||||
'elevation_gain': route.elevation_gain,
|
||||
'max_elevation': route.max_elevation
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"Error processing route {route.id}: {e}")
|
||||
continue
|
||||
|
||||
return map_data
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting map routes: {str(e)}")
|
||||
return []
|
||||
|
||||
|
||||
def get_post_route_details(post_id: int) -> Optional[Dict]:
|
||||
"""
|
||||
Get detailed route data for a specific post
|
||||
Returns full coordinate data for detailed view
|
||||
"""
|
||||
try:
|
||||
from app.models import MapRoute
|
||||
|
||||
route = MapRoute.query.filter_by(post_id=post_id).first()
|
||||
if not route:
|
||||
return None
|
||||
|
||||
return {
|
||||
'id': route.id,
|
||||
'post_id': route.post_id,
|
||||
'coordinates': route.get_coordinates_json(),
|
||||
'simplified_coordinates': route.get_simplified_coordinates_json(),
|
||||
'start_point': route.get_start_point(),
|
||||
'end_point': route.get_end_point(),
|
||||
'bounds': route.get_bounds(),
|
||||
'stats': {
|
||||
'distance': route.total_distance,
|
||||
'elevation_gain': route.elevation_gain,
|
||||
'max_elevation': route.max_elevation,
|
||||
'min_elevation': route.min_elevation,
|
||||
'total_points': route.total_points,
|
||||
'simplified_points': route.simplified_points
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting route details for post {post_id}: {str(e)}")
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user