Przejdź do głównej treści
Grafika przedstawia ukryty obrazek

Jak połączyć i oczyścić pliki GPX w Pythonie — kompletny przewodnik

Grafika SVG Trasa

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:

15 listopada 2025 10

Kategorie

programowanie

Tagi

gpx python

Dziękujemy!
()

Powiązane wpisy

Obraz ilustrujacy Formaty GPX KML i KMZ  jak zapisywa i wywietla trasy GPS na mapach
15 października 2025 4 min 46

Formaty GPX, KML i KMZ – jak zapisywać i wyświetlać trasy GPS na mapach

gpx
Czytaj więcej
Zdjecie zwiazane z Jak stworzy przegldarkow nawigacj rowerow GPS w HTML JavaScript i Bootstrap
18 października 2025 9 min 4

Jak stworzyć przeglądarkową nawigację rowerową GPS w HTML, JavaScript i Bootstrap

gpx
Czytaj więcej
Wymiana doświadczeń

Masz podobne doświadczenia?

Chętnie poznam Twoją perspektywę i porozmawiam o tym temacie szerzej.

Napisz do mnie

Każda perspektywa może wnieść coś wartościowego do dyskusji.

Twoja prywatność i pliki cookies

  1. Ta strona internetowa wykorzystuje wyłącznie niezbędne pliki cookies, które są wymagane do jej prawidłowego działania – m.in. do poprawnego wyświetlania treści, zapamiętania podstawowych ustawień przeglądarki oraz zapewnienia stabilności serwisu.
  2. Nie stosuję plików cookies w celach marketingowych, reklamowych ani analitycznych.
  3. Strona ma charakter wyłącznie informacyjny i nie zawiera formularzy kontaktowych, rejestracyjnych ani zakupowych, przez które dane mogłyby być przesyłane na serwer.
  4. Nie zbieram danych osobowych podczas zwykłego korzystania z witryny.
  5. Serwis nie korzysta z certyfikatu SSL, jednak ze względu na informacyjny charakter strony nie jest wymagane przesyłanie poufnych danych. Zalecam jednak, aby nigdy nie wpisywać haseł ani danych osobowych na stronach bez szyfrowanego połączenia.
  6. Korzystając z tej strony, wyrażasz zgodę na używanie wyłącznie niezbędnych plików cookies.

Więcej informacji znajdziesz w mojej polityce prywatności.