Jak połączyć i oczyścić pliki GPX w Pythonie — kompletny przewodnik
Format GPX (GPS Exchange Format) to popularny standard wymiany danych GPS, wykorzystywany w urządzeniach Garmin, Suunto, Wahoo, w aplikacjach sportowych, trackerach outdoorowych i mapach online. Często zachodzi potrzeba:
- połączenia kilku śladów GPX w jedną trasę
- posortowania punktów według czasu
- usunięcia duplikatów
- usunięcia błędnych punktów o nienaturalnej prędkości
- wykrycia przerw i stworzenia kilku segmentów
W tym artykule pokazuję krok po kroku, jak to zrobić w Pythonie — od najprostszej wersji, aż po w pełni wyposażony skrypt do czyszczenia i scalania danych GPS.
Instalacja biblioteki GPX
Wszystkie przykłady korzystają z gpxpy:
pip install gpxpy
1. Najprostsze łączenie kilku plików GPX w jeden
Pierwsza wersja scala GPX-y tak, jak są — bez sortowania ani filtrowania. Jest to najprostszy wariant, idealny gdy chcesz po prostu połączyć pliki.
merge_gpx.py — proste łączenie GPX
#!/usr/bin/env python3
import argparse
import gpxpy
import gpxpy.gpx
def merge_gpx_files(input_files, output_file):
merged_gpx = gpxpy.gpx.GPX()
for file_path in input_files:
print(f"Ładowanie: {file_path}")
with open(file_path, "r", encoding="utf-8") as f:
gpx = gpxpy.parse(f)
# Dodaj trasy
for track in gpx.tracks:
merged_gpx.tracks.append(track)
# Dodaj ścieżki (routes)
for route in gpx.routes:
merged_gpx.routes.append(route)
# Dodaj punkty (waypoints)
for waypoint in gpx.waypoints:
merged_gpx.waypoints.append(waypoint)
with open(output_file, "w", encoding="utf-8") as f:
f.write(merged_gpx.to_xml())
print(f"Zapisano połączony plik: {output_file}")
def main():
parser = argparse.ArgumentParser(description="Łączenie wielu plików GPX w jeden.")
parser.add_argument("input_files", nargs="+", help="Lista plików GPX do połączenia.")
parser.add_argument("-o", "--output", default="merged.gpx",
help="Nazwa pliku wynikowego (domyślnie merged.gpx).")
args = parser.parse_args()
merge_gpx_files(args.input_files, args.output)
if __name__ == "__main__":
main()
2. Łączenie GPX według czasu — tworzenie jednego chronologicznego śladu
Ten wariant:
- zbiera wszystkie punkty (trackpointy)
- sortuje je według
timestamp - zapisuje je jako jedną trasę GPX
Idealny, gdy masz kilka fragmentów trasy z różnych urządzeń lub przerw w nagrywaniu.
merge_gpx_time.py — łączenie według czasu
#!/usr/bin/env python3
import argparse
import gpxpy
import gpxpy.gpx
from datetime import datetime
def merge_gpx_by_time(input_files, output_file):
all_points = []
# Wczytaj wszystkie pliki i zbierz trackpointy
for file_path in input_files:
print(f"Ładowanie: {file_path}")
with open(file_path, "r", encoding="utf-8") as f:
gpx = gpxpy.parse(f)
for track in gpx.tracks:
for segment in track.segments:
for point in segment.points:
if point.time is None:
print(f"⚠️ Punkt w {file_path} nie ma czasu – pomijam")
continue
all_points.append(point)
if not all_points:
print("Brak trackpointów z czasem. Nie można utworzyć połączonej trasy.")
return
# Sortuj po czasie
all_points.sort(key=lambda p: p.time)
# Utwórz nowy plik GPX
new_gpx = gpxpy.gpx.GPX()
new_track = gpxpy.gpx.GPXTrack()
new_segment = gpxpy.gpx.GPXTrackSegment()
new_gpx.tracks.append(new_track)
new_track.segments.append(new_segment)
for p in all_points:
new_segment.points.append(p)
# Zapisz wynik
with open(output_file, "w", encoding="utf-8") as f:
f.write(new_gpx.to_xml())
print(f"Zapisano połączony plik: {output_file}")
def main():
parser = argparse.ArgumentParser(description="Łączenie wielu plików GPX w jeden według czasu.")
parser.add_argument("input_files", nargs="+", help="Lista plików GPX do połączenia.")
parser.add_argument("-o", "--output", default="merged_time.gpx",
help="Nazwa pliku wynikowego (domyślnie merged_time.gpx).")
args = parser.parse_args()
merge_gpx_by_time(args.input_files, args.output)
if __name__ == "__main__":
main()
3. Łączenie z filtrowaniem duplikatów i nienaturalnej prędkości
Ten wariant czyści trasę poprzez:
usuwanie duplikatów
- ten sam timestamp
- zbyt blisko (np. < 3 m od poprzedniego)
usuwanie punktów o nielogicznej prędkości
(np. > 200 km/h)
Idealne do czyszczenia tras nagranych smartfonem lub urządzeniem sportowym.
merge_gpx_clean.py — łączenie + filtrowanie jakości
#!/usr/bin/env python3
import argparse
import gpxpy
import gpxpy.gpx
import math
from datetime import datetime
def haversine(lat1, lon1, lat2, lon2):
R = 6371000
phi1 = math.radians(lat1)
phi2 = math.radians(lat2)
dphi = math.radians(lat2 - lat1)
dlambda = math.radians(lon2 - lon1)
a = (math.sin(dphi/2)**2 +
math.cos(phi1)*math.cos(phi2)*math.sin(dlambda/2)**2)
return 2 * R * math.atan2(math.sqrt(a), math.sqrt(1-a))
def speed_kmh(p1, p2):
dist = haversine(p1.latitude, p1.longitude, p2.latitude, p2.longitude)
dt = (p2.time - p1.time).total_seconds()
if dt <= 0:
return 0
return (dist / dt) * 3.6
def merge_gpx_clean(input_files, output_file, max_speed_kmh, duplicate_distance):
all_points = []
for file_path in input_files:
print(f"Ładowanie: {file_path}")
with open(file_path, "r", encoding="utf-8") as f:
gpx = gpxpy.parse(f)
for track in gpx.tracks:
for segment in track.segments:
for point in segment.points:
if point.time:
all_points.append(point)
if not all_points:
print("Brak punktów.")
return
all_points.sort(key=lambda p: p.time)
cleaned = []
last = None
for p in all_points:
if last:
if p.time == last.time:
continue
dist = haversine(last.latitude, last.longitude, p.latitude, p.longitude)
if dist < duplicate_distance:
continue
if speed_kmh(last, p) > max_speed_kmh:
continue
cleaned.append(p)
last = p
new_gpx = gpxpy.gpx.GPX()
new_track = gpxpy.gpx.GPXTrack()
new_segment = gpxpy.gpx.GPXTrackSegment()
new_gpx.tracks.append(new_track)
new_track.segments.append(new_segment)
for p in cleaned:
new_segment.points.append(p)
with open(output_file, "w", encoding="utf-8") as f:
f.write(new_gpx.to_xml())
print(f"Zapisano: {output_file}")
4. Wykrywanie przerw czasowych i tworzenie segmentów
To najważniejsze dla aktywności sportowych:
jeśli między punktami jest przerwa > X minut → twórz nowy segment.
Segmenty pozwalają np. rozróżnić etapy wycieczki lub wyciszyć przestoje urządzenia.
merge_gpx_clean_segments.py — finalna, kompletna wersja
Poniżej znajduje się pełny, scalony skrypt, który:
- łączy wiele plików
- sortuje według czasu
- usuwa duplikaty
- filtruje nienaturalne prędkości
- wykrywa przerwy i tworzy wiele segmentów
To jest najbardziej kompletna wersja.
#!/usr/bin/env python3
import argparse
import gpxpy
import gpxpy.gpx
import math
from datetime import datetime, timedelta
def haversine(lat1, lon1, lat2, lon2):
R = 6371000
phi1 = math.radians(lat1)
phi2 = math.radians(lat2)
dphi = math.radians(lat2 - lat1)
dlambda = math.radians(lon2 - lon1)
a = (math.sin(dphi/2)**2 +
math.cos(phi1)*math.cos(phi2)*math.sin(dlambda/2)**2)
return 2 * R * math.atan2(math.sqrt(a), math.sqrt(1-a))
def speed_kmh(p1, p2):
dist = haversine(p1.latitude, p1.longitude, p2.latitude, p2.longitude)
dt = (p2.time - p1.time).total_seconds()
if dt <= 0:
return 0
return (dist / dt) * 3.6
def merge_gpx_clean_segments(input_files, output_file, max_speed_kmh,
duplicate_distance, gap_minutes):
all_points = []
for file_path in input_files:
print(f"Ładowanie: {file_path}")
with open(file_path, "r", encoding="utf-8") as f:
gpx = gpxpy.parse(f)
for track in gpx.tracks:
for segment in track.segments:
for point in segment.points:
if point.time:
all_points.append(point)
if not all_points:
print("Brak punktów.")
return
all_points.sort(key=lambda p: p.time)
cleaned = []
last = None
for p in all_points:
if last:
if p.time == last.time:
continue
dist = haversine(last.latitude, last.longitude, p.latitude, p.longitude)
if dist < duplicate_distance:
continue
if speed_kmh(last, p) > max_speed_kmh:
continue
cleaned.append(p)
last = p
new_gpx = gpxpy.gpx.GPX()
new_track = gpxpy.gpx.GPXTrack()
new_gpx.tracks.append(new_track)
current_segment = gpxpy.gpx.GPXTrackSegment()
new_track.segments.append(current_segment)
gap = timedelta(minutes=gap_minutes)
last = None
for p in cleaned:
if last and p.time - last.time > gap:
print(f"⏸️ Przerwa {p.time - last.time}, nowy segment")
current_segment = gpxpy.gpx.GPXTrackSegment()
new_track.segments.append(current_segment)
current_segment.points.append(p)
last = p
with open(output_file, "w", encoding="utf-8") as f:
f.write(new_gpx.to_xml())
print(f"Zapisano wynik: {output_file}")
print(f"Liczba segmentów: {len(new_track.segments)}")
def main():
parser = argparse.ArgumentParser(
description="Zaawansowane łączenie GPX z filtrami i wykrywaniem przerw."
)
parser.add_argument("input_files", nargs="+", help="Pliki GPX.")
parser.add_argument("-o", "--output", default="merged_clean_segments.gpx",
help="Plik wynikowy.")
parser.add_argument("--max-speed", type=float, default=200,
help="Maksymalna prędkość km/h.")
parser.add_argument("--dup-dist", type=float, default=3,
help="Odległość uznawana za duplikat (m).")
parser.add_argument("--gap-minutes", type=float, default=5,
help="Przerwa czasowa dla nowego segmentu (min).")
args = parser.parse_args()
merge_gpx_clean_segments(
args.input_files,
args.output,
args.max_speed,
args.dup_dist,
args.gap_minutes
)
if __name__ == "__main__":
main()
5. Podsumowanie
W artykule przedstawiłem cztery poziomy scalania GPX:
| Zadanie | Skrypt |
|---|---|
| Proste łączenie | merge_gpx.py |
| Łączenie według czasu | merge_gpx_time.py |
| Łączenie + filtrowanie | merge_gpx_clean.py |
| Łączenie + filtrowanie + segmenty | merge_gpx_clean_segments.py |
Ostatni z nich to kompletne narzędzie, które: