import gpxpy import folium import os import math import glob import html import json from datetime import datetime # 定义可用的地图源 MAP_TILES = { "高德卫星图": "http://webst02.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}", "高德街道图": "http://webrd02.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}" } # 地图源的属性信息 MAP_ATTRIBUTION = { "高德卫星图": '© 高德地图', "高德街道图": '© 高德地图' } # 性能配置:超过此点数不生成逐点悬停层,避免浏览器卡顿 # MAX_POINTS_FOR_HOVER = 500 def wgs84_to_gcj02(lng, lat): """ WGS84转GCJ02(火星坐标系) 将GPS的WGS84坐标转换为高德地图使用的GCJ02坐标 """ a = 6378245.0 # 长半轴 ee = 0.00669342162296594323 # 扁率 # 判断是否在国内 if (lng < 72.004 or lng > 137.8347) or (lat < 0.8293 or lat > 55.8271): return lng, lat dlat = _transform_lat(lng - 105.0, lat - 35.0) dlng = _transform_lng(lng - 105.0, lat - 35.0) radlat = lat / 180.0 * math.pi magic = math.sin(radlat) magic = 1 - ee * magic * magic sqrtmagic = math.sqrt(magic) dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * math.pi) dlng = (dlng * 180.0) / (a / sqrtmagic * math.cos(radlat) * math.pi) mglat = lat + dlat mglng = lng + dlng return mglng, mglat def _transform_lat(lng, lat): ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat + 0.2 * math.sqrt(abs(lng)) ret += (20.0 * math.sin(6.0 * lng * math.pi) + 20.0 * math.sin(2.0 * lng * math.pi)) * 2.0 / 3.0 ret += (20.0 * math.sin(lat * math.pi) + 40.0 * math.sin(lat / 3.0 * math.pi)) * 2.0 / 3.0 ret += (160.0 * math.sin(lat / 12.0 * math.pi) + 320 * math.sin(lat * math.pi / 30.0)) * 2.0 / 3.0 return ret def _transform_lng(lng, lat): ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * math.sqrt(abs(lng)) ret += (20.0 * math.sin(6.0 * lng * math.pi) + 20.0 * math.sin(2.0 * lng * math.pi)) * 2.0 / 3.0 ret += (20.0 * math.sin(lng * math.pi) + 40.0 * math.sin(lng / 3.0 * math.pi)) * 2.0 / 3.0 ret += (150.0 * math.sin(lng / 12.0 * math.pi) + 300.0 * math.sin(lng / 30.0 * math.pi)) * 2.0 / 3.0 return ret def format_datetime(dt): """ 格式化日期时间,去掉时区信息 """ if dt: return dt.strftime("%Y-%m-%d %H:%M:%S") return "未知" def format_duration(seconds): """ 格式化持续时间 """ if seconds is None: return "未知" hours = int(seconds // 3600) minutes = int((seconds % 3600) // 60) secs = int(seconds % 60) if hours > 0: return f"{hours}小时{minutes}分{secs}秒" elif minutes > 0: return f"{minutes}分{secs}秒" else: return f"{secs}秒" def parse_gpx_file(gpx_file_path): """ 解析GPX文件,优化了速度平滑处理和运动时间计算(自动剔除暂停时间) """ # 配置参数 MAX_SPEED_THRESHOLD = 50.0 # 最高时速上限 (km/h),超过此值可能是漂移 PAUSE_THRESHOLD_SECONDS = 3 # 采样间隔超过3秒视为暂停 SPEED_WINDOW_SIZE = 3 # 速度平滑窗口大小 try: # 尝试多种编码,避免非UTF-8文件导致崩溃 gpx_content = None for encoding in ['utf-8', 'utf-8-sig', 'gbk', 'gb2312', 'latin-1']: try: with open(gpx_file_path, 'r', encoding=encoding) as f: gpx_content = f.read() break except (UnicodeDecodeError, UnicodeError): continue if gpx_content is None: print(f"无法解码文件: {gpx_file_path}") return None gpx = gpxpy.parse(gpx_content) points = [] total_distance = 0 moving_time_seconds = 0 # 净运动时间 point_moving_time = 0 # 每个轨迹点的累计运动时间(排除暂停) start_time = None end_time = None # 临时存储速度用于平滑处理 recent_speeds = [] for track in gpx.tracks: for segment in track.segments: previous_point = None for i, point in enumerate(segment.points): # 坐标转换 gcj_lng, gcj_lat = wgs84_to_gcj02(point.longitude, point.latitude) segment_distance = 0 raw_speed = 0 time_diff = 0 if previous_point: # 计算距离 (使用WGS84原始坐标计算距离更准确,再转换用于显示) segment_distance = gpxpy.geo.haversine_distance( previous_point['raw_lat'], previous_point['raw_lng'], point.latitude, point.longitude ) total_distance += segment_distance # 计算时间差 if point.time and previous_point['time']: time_diff = (point.time - previous_point['time']).total_seconds() # --- 改进2:暂停检测逻辑 --- # 如果时间间隔在阈值内,计入运动时间 if 0 < time_diff < PAUSE_THRESHOLD_SECONDS: moving_time_seconds += time_diff point_moving_time += time_diff # 计算原始速度 if time_diff > 0: raw_speed = (segment_distance / time_diff) * 3.6 # km/h # --- 改进1:速度平滑处理 --- # 剔除极端错误的数字,用0替代而非前一个值(防止异常值传播) if raw_speed > MAX_SPEED_THRESHOLD: raw_speed = 0.0 recent_speeds.append(raw_speed) if len(recent_speeds) > SPEED_WINDOW_SIZE: recent_speeds.pop(0) # 取窗口平均值作为当前点的瞬时速度 smoothed_speed_kmh = sum(recent_speeds) / len(recent_speeds) if recent_speeds else 0.0 # 记录时间 if i == 0 and point.time and not start_time: start_time = point.time point_data = { 'latitude': gcj_lat, 'longitude': gcj_lng, 'raw_lat': point.latitude, # 保留原始坐标用于距离计算 'raw_lng': point.longitude, 'elevation': point.elevation, 'time': point.time, 'cumulative_distance': total_distance, 'instantaneous_speed': smoothed_speed_kmh / 3.6, # 转回 m/s 保持统一 'duration_from_start': point_moving_time, 'segment_distance': segment_distance } points.append(point_data) previous_point = point_data if not points: return None # 安全获取结束时间(点可能没有时间戳) end_time = None for p in reversed(points): if p['time'] is not None: end_time = p['time'] break # 如果仍未找到开始时间,从第一个有时间戳的点获取 if start_time is None: for p in points: if p['time'] is not None: start_time = p['time'] break # 计算总时长(包含暂停),只有两者都不为None才计算 total_duration_hours = 0.0 if start_time and end_time: total_duration_hours = (end_time - start_time).total_seconds() / 3600 # 计算运动时长(剔除暂停) moving_duration_hours = moving_time_seconds / 3600 # 其他统计逻辑 (海拔等) 保持不变... elevations = [p['elevation'] for p in points if p['elevation'] is not None] elevation_gain = 0 if len(elevations) > 1: for i in range(1, len(elevations)): if elevations[i] > elevations[i-1]: elevation_gain += elevations[i] - elevations[i-1] speeds_kmh = [p['instantaneous_speed'] * 3.6 for p in points] max_speed = max(speeds_kmh) if speeds_kmh else 0 avg_speed = (total_distance / 1000) / moving_duration_hours if moving_duration_hours > 0 else 0 return { 'points': points, 'total_distance': total_distance / 1000, 'start_time': format_datetime(start_time), 'end_time': format_datetime(end_time), 'num_points': len(points), 'min_elevation': min(elevations) if elevations else None, 'max_elevation': max(elevations) if elevations else None, 'avg_elevation': sum(elevations)/len(elevations) if elevations else None, 'elevation_gain': elevation_gain, 'duration_hours': moving_duration_hours, # 这里改为返回运动时间 'total_elapsed_hours': total_duration_hours, # 保留一个总耗时 'avg_speed': avg_speed, 'max_speed': max_speed } except Exception as e: print(f"解析GPX文件时出错: {e}") return None def create_map_with_track(gpx_data, output_file='gpx_track_map.html'): """ 使用folium创建带有轨迹的地图,默认提供多个地图源选项 添加鼠标悬停显示路程、速度和时间功能 轨迹始终显示,没有选择框控制 """ if not gpx_data or not gpx_data['points']: print("没有轨迹数据可显示") return points = gpx_data['points'] # 计算地图中心点 center_lat = sum(p['latitude'] for p in points) / len(points) center_lon = sum(p['longitude'] for p in points) / len(points) # 创建地图 - 使用高德街道图作为默认地图 m = folium.Map( location=[center_lat, center_lon], zoom_start=13, tiles=None # 不设置默认瓦片 ) # 添加所有地图源作为可选图层 for tile_name, tile_url in MAP_TILES.items(): folium.TileLayer( tiles=tile_url, attr=MAP_ATTRIBUTION[tile_name], name=tile_name ).add_to(m) # 创建轨迹线的坐标列表和对应的工具提示数据 track_coordinates = [] tooltip_data = [] for point in points: track_coordinates.append((point['latitude'], point['longitude'])) # 准备工具提示信息(使用 html.escape 防止 XSS) distance_km = point['cumulative_distance'] / 1000 speed_kmh = point['instantaneous_speed'] * 3.6 # 转换为km/h tooltip = ( f'