Недавно проехал сокращенный маршрут Уральской байкпакинговой гонки 2026. Вышло 538 км и 2,5 суток в седле. Трек записывал велокомпьютером Coros Dura. После заезда хотел посмотреть такой параметр, как “время в движении”, но его не оказалось в приложении COROS.
Первая мысль — скачаю трек и загружу в любой сторонний сервис для анализа тренировок. Но, как оказалось, что не каждый ресурс переваривает такие объемы. А точнее я не нашел ресурса, который переварил бы мои метрики с гонки. Например, Strava принимает файлы до 25 МБ, а мой gpx файл весил целых 35 МБ.
Что хотелось узнать?
- Время в движении
- Время остановок:
- Длинные на ночлег: более 2 часов
- Короткие на отдых/перекусы/магазины: от 5 минут до 2 часов
Накидал простенький скрипт на python:
import xml.etree.ElementTree as ET
from datetime import datetime
import math
import os
# --- FILTER SETTINGS ---
# Minimum speed to consider as moving (in km/h).
MIN_MOVING_SPEED_KMH = 0.5
# Minimum distance between points to consider as moving (in meters).
MIN_MOVING_DISTANCE_M = 1.5
# Maximum pause between points (in seconds).
MAX_TIME_GAP_SEC = 30
# Micro-stop threshold (in seconds). Stops shorter than this are considered as part of riding.
# 5 minutes = 300 seconds
MICRO_STOP_THRESHOLD_SEC = 5 * 60
# Sleep threshold (in seconds). Stops longer than this are considered as sleep.
# 2 hours = 7200 seconds
SLEEP_THRESHOLD_SEC = 2 * 60 * 60
def haversine(lat1, lon1, lat2, lon2):
"""Calculates distance between two coordinates in meters."""
R = 6371000.0
phi1 = math.radians(lat1)
phi2 = math.radians(lat2)
dphi = math.radians(lat2 - lat1)
dlambda = math.radians(lon2 - lon1)
a = math.sin(dphi/2.0)**2 + math.cos(phi1) * math.cos(phi2) * math.sin(dlambda/2.0)**2
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))
return R * c
def parse_gpx(file_path):
print(f"Читаем файл: {file_path} ({os.path.getsize(file_path) / (1024*1024):.2f} МБ)...")
tree = ET.parse(file_path)
root = tree.getroot()
points = []
for trkpt in root.iter('{http://www.topografix.com/GPX/1/1}trkpt'):
lat = float(trkpt.get('lat'))
lon = float(trkpt.get('lon'))
time_str = trkpt.find('{http://www.topografix.com/GPX/1/1}time').text
time_str = time_str.replace('Z', '+00:00')
dt = datetime.fromisoformat(time_str)
points.append({'lat': lat, 'lon': lon, 'time': dt})
print(f"Найдено точек: {len(points)}")
return points
def calculate_stats(points):
total_distance = 0.0
total_time_sec = 0.0
moving_time_sec = 0.0
short_stops_sec = 0.0 # Stops from 5 minutes to 2 hours
sleep_time_sec = 0.0 # Stops 2 hours or more
current_stop_duration = 0.0
for i in range(1, len(points)):
p1 = points[i-1]
p2 = points[i]
dist = haversine(p1['lat'], p1['lon'], p2['lat'], p2['lon'])
time_delta = (p2['time'] - p1['time']).total_seconds()
if time_delta <= 0:
continue
total_distance += dist
total_time_sec += time_delta
speed_ms = dist / time_delta
speed_kmh = speed_ms * 3.6
is_moving = (dist >= MIN_MOVING_DISTANCE_M) and (speed_kmh >= MIN_MOVING_SPEED_KMH) and (time_delta <= MAX_TIME_GAP_SEC)
if is_moving:
moving_time_sec += time_delta
# If we are moving, the previous stop has ended. Sorting it:
if current_stop_duration > 0:
if current_stop_duration >= SLEEP_THRESHOLD_SEC:
sleep_time_sec += current_stop_duration
elif current_stop_duration > MICRO_STOP_THRESHOLD_SEC:
short_stops_sec += current_stop_duration
else:
# Stops less than 5 minutes are merged into moving time
moving_time_sec += current_stop_duration
current_stop_duration = 0.0
else:
current_stop_duration += time_delta
# Check the final stop at the end of the track
if current_stop_duration > 0:
if current_stop_duration >= SLEEP_THRESHOLD_SEC:
sleep_time_sec += current_stop_duration
elif current_stop_duration > MICRO_STOP_THRESHOLD_SEC:
short_stops_sec += current_stop_duration
else:
moving_time_sec += current_stop_duration
return total_distance, total_time_sec, moving_time_sec, short_stops_sec, sleep_time_sec
def format_time(seconds):
"""Converts seconds to HH:MM:SS format"""
hours = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
secs = int(seconds % 60)
return f"{hours:02d}:{minutes:02d}:{secs:02d}"
if __name__ == "__main__":
# Specify your file name here:
GPX_FILE = "my_ride.gpx"
if not os.path.exists(GPX_FILE):
print(f"ОШИБКА: Файл '{GPX_FILE}' не найден!")
else:
pts = parse_gpx(GPX_FILE)
dist, total_t, mov_t, short_t, sleep_t = calculate_stats(pts)
print("\n" + "="*50)
print(f"Общая дистанция: {dist / 1000:.2f} км")
print(f"Общее время: {format_time(total_t)} (от старта до финиша)")
print("-" * 50)
print(f"ВРЕМЯ В ДВИЖЕНИИ: {format_time(mov_t)}")
print(f"Остановки: {format_time(short_t)} (от 5 мин до 2 часов)")
print(f"Время остановки на ночлег: {format_time(sleep_t)} (более 2 часов)")
print("="*50)
if mov_t > 0:
print(f"Средняя скорость (в движении): {(dist/1000) / (mov_t/3600):.2f} км/ч")
if total_t > 0:
print(f"Средняя скорость (общая): {(dist/1000) / (total_t/3600):.2f} км/ч")
Вывод работы скрипта:
python3 gpx_calc.py
Читаем файл: my_ride.gpx (34.30 МБ)...
Найдено точек: 134273
==================================================
Общая дистанция: 538.94 км
Общее время: 61:52:21 (от старта до финиша)
--------------------------------------------------
ВРЕМЯ В ДВИЖЕНИИ: 39:26:49
Остановки: 07:20:54 (от 5 мин до 2 часов)
Время остановки на ночлег: 15:04:38 (более 2 часов)
==================================================
Средняя скорость (в движении): 13.66 км/ч
Средняя скорость (общая): 8.71 км/ч