Skip to main content

gpx-player

CI codecov PyPI version Python versions PyPI downloads License

GPX Race Visualizer

gpx-player is a Python package (with two command-line tools and a small public API) that visualises the progression of one or more GPS tracks, for example from a race, on a 2D map. It takes one or more GPX files and produces either a rendered animation or an interactive HTML map that you can play back in a browser. This is a simple, open-source alternative to features like Strava's Flyby, which require an account and can have privacy issues.

See CHANGELOG.md for the release history.

Modes

The player supports two modes.

1. "Video" mode (gpx-player)

Produces an MP4 or a GIF file showing how the situation developed. For sailing races, it also calculates the distance covered after the 'start' signal and the current speed.

The file is written to the current directory and named after --title (slugified), so --title "Race 1" produces race-1.mp4. Without a title you get untitled.mp4. MP4 output requires ffmpeg on your PATH. Without it matplotlib falls back to its Pillow writer, which cannot encode MP4, and the run dies with ValueError: unknown file extension: .mp4 after rendering every frame, leaving no output file. --gif works without ffmpeg.

Example:

Example output

2. Map mode (python -m gpx_player.openseamap)

Writes a self-contained, playable HTML page on top of OpenStreetMap with an OpenSeaMap seamark overlay. The page shows:

  • the full track of every participant, colour-coded by speed;
  • a play/pause button and a time slider to replay the tracks;
  • a directional arrow marker per participant that rotates to the current heading;
  • a moving tail behind each marker, drawn as a dark outline with a speed-coloured core, with a configurable length;
  • a speed legend, and a live legend with each boat's distance travelled (nautical miles), current speed (knots) and average speed (knots);
  • a per-participant visibility control (top-right) to switch each track between the full speed-coloured track, the moving tail only, or off.

Displayed speeds are smoothed over a short trailing time window (10 s), which removes the "zero, then jump" artefacts typical of noisy GPS samples while still showing genuine stationary periods.

Example:

▶ Open the live interactive demo

GitHub sanitises Markdown and strips <script> and <iframe>, so a live playable map cannot be embedded in this page. The screenshot below links to the hosted, fully interactive version:

OpenSeaMap example

The demo is built from the GPX files in example-data/ by scripts/build_demo.py and redeployed by the Pages workflow on every push to main, so it always reflects the current code. Build it yourself with:

python scripts/build_demo.py --output-dir site
python -m http.server -d site 8000    # then open http://localhost:8000

Installation

Requires Python 3.9 or newer.

Install from PyPI:

pip install gpx-player

To work on the code, clone the repository and install it in editable mode. This installs the runtime dependencies declared in pyproject.toml; requirements.txt additionally pins the test tooling used by CI:

git clone https://github.com/kirienko/gpx-player.git
cd gpx-player
pip install -e .
pip install -r requirements.txt   # optional: adds pytest / pytest-cov
pytest

Usage

Video mode

Pass one or more GPX file paths as positional arguments:

python -m gpx_player.main example-data/track1.gpx example-data/track2.gpx

Known issue. gpx_player/main.py has no main() function (the whole script runs at import time), but pyproject.toml declares the entry point as gpx_player.main:main. The gpx-player command therefore renders the file correctly and then fails with ImportError: cannot import name 'main', exiting with status 1 (#21). Until that is fixed, prefer the module form, which behaves identically and exits 0. The examples below use it for that reason:

python -m gpx_player.main example-data/track1.gpx example-data/track2.gpx

A more sophisticated example, which produced the video above:

python -m gpx_player.main example-data/track1.gpx example-data/track2.gpx example-data/track3.gpx \
       --start 2023-07-01T10:53:00+0000 \
       --names "Mr. Pommeroy" "Miss Sophie" "Sir Toby²" \
       --title "Elbe-Damm Regatta (01.07.2023), Race 1" \
       --race_start 2023-07-01T10:58:00+0000 --marks example-data/marks.txt -g

Video mode options

Option Description
files (positional) One or more GPX files to process.
--title, -t Title of the video. Also determines the output filename.
--start, -s Start time, all points before it are dropped.
--end, -e End time, all points after it are dropped.
--race_start, -r Race start time, used for the "distance since the start signal" readout.
--names, -n Names of the participants (file names are used in the legend otherwise).
--marks, -m File with static marks to put onto the map, one coordinate pair per line, see Marks.
--gif, -g Save as an animated GIF instead of MP4.
--timezone, -tz Local timezone for displayed timestamps, e.g. America/Los_Angeles, see the tz database list (default: Europe/Berlin).

In video mode --start, --end and --race_start are parsed strictly as %Y-%m-%dT%H:%M:%S%z, e.g. 2023-06-30T12:53:00+0200. The UTC offset is mandatory and Z is not accepted here.

Map mode

Note that in map mode the GPX files are passed via the required --files option, not as positional arguments:

python -m gpx_player.openseamap --title 'Gin Sul Regatta 2024' --names Alex Yury Richard \
     --files example-data/osm-demo-Alex.gpx example-data/osm-demo-Richard.gpx \
             example-data/osm-demo-Yury.gpx

This always writes the map to boat_tracks.html in the current directory, overwriting any existing file of that name.

Restrict the map to a specific time window with --start / --end. Speed, distance, map bounds and the animation slider all reflect only the filtered segment:

python -m gpx_player.openseamap --files example-data/osm-demo-Alex.gpx \
     --start 2024-06-15T17:00:00+0200 --end 2024-06-15T17:30:00+0200

Map mode options

Option Description
--files (required) One or more GPX files to process.
--names, -n Names of the participants (track names, or Track N, are used otherwise).
--title, -t Title of the page. Also becomes the HTML document title.
--start, -s Only render points at or after this time.
--end, -e Only render points at or before this time.
--max-speed, -ms Plausibility cut-off in knots (default: 12), see the note below.
--tail-length Length of the moving tail: short (30 points), normal (60, default) or long (120).

Map mode accepts any ISO 8601 timestamp with a timezone, so 2024-06-15T17:00:00+0200, 2024-06-15T17:00:00+02:00 and 2024-06-15T15:00:00Z are all valid. This is more permissive than video mode.

Video-mode options that do not exist in map mode: --race_start, --marks, --gif and --timezone. Map-mode timestamps are shown in UTC.

About --max-speed. It is a dirty-data filter, not a display cap: any segment whose computed speed exceeds it is treated as a GPS glitch and recorded as 0 knots. The colour scale is then rescaled to the highest speed that actually survived the filter. Set it above the fastest speed you expect, otherwise your quickest segments will silently be flattened to zero.

Python API

The same playable OpenSeaMap can be created from Python:

from gpx_player.openseamap import create_playback_map

folium_map = create_playback_map(
    [
        "example-data/osm-demo-Alex.gpx",
        "example-data/osm-demo-Richard.gpx",
        "example-data/osm-demo-Yury.gpx",
    ],
    names=["Alex", "Richard", "Yury"],
    max_speed=12,
    title="Gin Sul Regatta 2024",
    slider_active_color="#6e6e6e",
    slider_inactive_color="#d0d0d0",
    tail_length="normal",
)
folium_map.save("boat_tracks.html")

create_playback_map(gpx_files, names=None, *, max_speed=12, title=None, start_time=None, end_time=None, slider_active_color="#6e6e6e", slider_inactive_color="#d0d0d0", tail_length="normal") returns a folium.Map, so you can add your own layers before saving, or render it into an existing page. start_time / end_time are timezone-aware datetime objects and are the programmatic equivalent of --start / --end. slider_active_color and slider_inactive_color are Python-only arguments for theming the played and unplayed sections of the playback slider; they default to the built-in greys shown above, and passing None selects those same defaults.

Lower-level building blocks are public too:

Function Purpose
openseamap.create_map(files, names, max_speed, ...) Build the base map and parsed track data without any playback UI.
openseamap.add_playback_controls(folium_map, all_tracks, ...) Attach the playback UI, legends and data to a map you already have.
gpx_utils.trim_track(track, start, end) / trim_tracks(...) Trim already-parsed tracks to a time window without mutating the input.
gpx_utils.remove_extensions_tags(path, overwrite=False) Strip <extensions> blocks from a GPX file.
validator.validate_gpx(path, strict=False) Validate a GPX file, see GPX Validation.

Playback templates and JavaScript are bundled as package assets, so downstream applications can call this API from any current working directory after pip install gpx-player; no source checkout or local asset copies are needed.

OpenStreetMap tile access

The generated HTML uses live raster tiles from tile.openstreetmap.org for the base map and an OpenSeaMap seamark overlay. OpenStreetMap data is open, but the public OSM tile servers are a shared service with a tile usage policy. In particular, browser requests from web pages must send a valid HTTP Referer header.

If you open a generated map directly as a local file://.../boat_tracks.html file, many browsers will not send a valid HTTP Referer for tile requests. OSM may then return "blocked" placeholder tiles linking to https://osm.wiki/blocked. This can happen even for an old HTML file that works normally when hosted on a website.

For local viewing, serve the directory over HTTP instead:

python3 -m http.server 8000

Then open http://localhost:8000/boat_tracks.html in the browser.

For public or production applications, do not rely on the community tile.openstreetmap.org service as an application tile backend. Host the HTML on a normal web origin and use a tile provider, self-hosted tiles, or vector tiles whose terms fit your traffic and offline/static distribution needs.

Marks

Video mode also supports visualising predefined marks on the map, which is useful for events like sailing regattas. The marks are given as one latitude, longitude pair per line in a plain text file, passed via --marks:

53.542484632728, 9.801163896918299
53.542997846049374, 9.80611324310303
53.54823800356785, 9.812614917755129
53.54921647691311, 9.807373881340029
53.54508251196638, 9.80433225631714

Getting GPX Files

Most GPS-tracking services can export GPX. The exact menu wording changes over time, so treat the following as a hint rather than a click-by-click recipe:

  • Strava: open the activity, use the "..." (more options) menu and choose "Export GPX".
  • Garmin Connect: open the activity, use the gear / "..." menu and choose "Export to GPX".
  • Komoot: open the tour and choose "Export GPX" (a free account is enough for your own tours).
  • Wikiloc, Suunto App, Polar Flow, COROS: all offer a per-activity GPX export from the activity page.
  • Apple Health / Fitness: no direct GPX export; use a third-party app or the full Health data export.

(Endomondo, previously listed here, was shut down at the end of 2020.)

GPX Validation

For gpx-player to work properly, it needs correct GPX files. You can check a file with the validator included in this package.

gpx_player.validator is a command-line utility and a module. It checks XML schema conformance (against the bundled GPX 1.0 / 1.1 XSDs), coordinate and elevation ranges, and timestamp consistency, in either strict or lenient mode. As a CLI tool:

gpx-validate path/to/yourfile.gpx --strict

It exits with 0 if the file is valid and 1 otherwise, printing the reason to stderr.

--strict is optional. In most cases you do not need it, because files that strictly correspond to the GPX schema are rare. For example, almost all modern files contain coordinates, elevations and timestamps with more decimal places than originally planned.

Use as a Python module

from gpx_player.validator import validate_gpx, GPXValidationError

try:
    validate_gpx("path/to/yourfile.gpx", strict=True)
    print("GPX file is valid")
except GPXValidationError as e:
    print("GPX validation failed:", e)

Caveat. Most failures raise GPXValidationError, but a missing or unsupported version attribute on the root <gpx> element currently calls sys.exit(1) instead of raising. If you embed the validator in a long-running process, guard against SystemExit as well.

Also, to better understand your GPX file, you can use the gpxinfo console command that comes with gpxpy. If you are already using the player, you have it:

$ gpxinfo example-data/osm_track1.gpx
File: example-data/osm_track1.gpx
    Waypoints: 0
    Routes: 0
    Length 2D: 9.621km
    Length 3D: 9.648km
    Moving time: 01:05:22
    Stopped time: n/a
    Max speed: 3.12m/s = 11.22km/h
    Avg speed: 2.46m/s = 8.85km/h
    Total uphill: 97.20m
    Total downhill: 98.40m
    Started: 2024-07-24 15:59:05+00:00
    Ended: 2024-07-24 17:04:27+00:00
    Points: 776
    Avg distance between points: 12.40m

GPX Cleanup

For convenience, the package provides gpx_player.clean_gpx. This utility first validates a GPX file using the validator and then removes all <extensions> blocks using remove_extensions_tags from gpx_player.gpx_utils. By default the cleaned file is saved alongside the original with _noext appended to its name. With the optional --overwrite flag the original file is modified in place.

python -m gpx_player.clean_gpx path/to/yourfile.gpx [--overwrite]

If validation fails, the command exits with an error message. The output reports how many extension blocks were removed.

For AI agents

This section is a compact contract for LLM agents and automated pipelines that drive gpx-player. Humans can skip it.

What this package does: turns GPX track files into either a rendered animation (MP4/GIF) or a single-file interactive HTML map. Generation itself is offline and deterministic: the track data is inlined into the page and nothing is uploaded.

The generated page is not self-contained at view time. Folium references Leaflet, jQuery and Bootstrap from cdn.jsdelivr.net, cdnjs.cloudflare.com, code.jquery.com and netdna.bootstrapcdn.com, on top of the map tiles. In a browser that cannot reach those hosts the map does not initialise at all, so do not treat the output as an offline artifact or promise a viewer it will work air-gapped.

Recommended workflow

  1. Validate first. Call validate_gpx(path) (or gpx-validate path) on every input before rendering. Most rendering failures are bad input, and the validator gives a specific reason where the renderer gives a traceback.
  2. Clean if needed. clean_gpx_file(path) strips vendor <extensions>, but it runs validate_gpx(path, strict=True) first and raises GPXValidationError if that fails, so it can only clean files that already validate. To strip extensions from a file that does not validate, call gpx_utils.remove_extensions_tags(path) directly, which does no validation.
  3. Render via the Python API, not the CLI. create_playback_map() returns a folium.Map, so you choose the output path. The map-mode CLI always writes boat_tracks.html into the current working directory and overwrites it, which makes concurrent or repeated runs collide.
  4. Report the artifact path you saved to, not "the file was created".

Minimal end-to-end recipe

import datetime as dt
from gpx_player.validator import validate_gpx, GPXValidationError
from gpx_player.openseamap import create_playback_map

files = ["example-data/osm-demo-Alex.gpx", "example-data/osm-demo-Yury.gpx"]

for f in files:
    try:
        validate_gpx(f)                     # lenient mode; strict=True is usually too strict
    except (GPXValidationError, SystemExit) as e:
        raise SystemExit(f"unusable input {f}: {e}")

folium_map = create_playback_map(
    files,
    names=["Alex", "Yury"],
    max_speed=20,                           # see the --max-speed caveat below
    title="Race 1",
    start_time=dt.datetime.fromisoformat("2024-06-15T17:00:00+02:00"),
    end_time=dt.datetime.fromisoformat("2024-06-15T17:30:00+02:00"),
    tail_length="normal",
)
out = "/tmp/race1.html"
folium_map.save(out)
print(out)

Interface summary

Entry point Kind Inputs Produces
python -m gpx_player.main FILES... CLI positional GPX paths <slug(title)>.mp4 or .gif in the CWD
python -m gpx_player.openseamap --files FILES... CLI --files is required boat_tracks.html in the CWD (always this name)
gpx-validate FILE CLI one GPX path exit 0 valid / 1 invalid
python -m gpx_player.clean_gpx FILE CLI one GPX path FILE_noext.gpx, or in place with --overwrite
openseamap.create_playback_map(...) API list of paths folium.Map, caller chooses the path
openseamap.create_map(...) API list of paths (folium.Map, tracks, max_speed, map_id)
validator.validate_gpx(...) API one path True, or raises GPXValidationError (also SystemExit on a bad version, see below)
gpx_utils.trim_track(...) API parsed track dict trimmed copy, input untouched

Failure modes to expect

  • Naive timestamps. Every --start / --end / start_time / end_time value must carry a UTC offset. Video mode is strict (%Y-%m-%dT%H:%M:%S%z, no Z); map mode accepts general ISO 8601, including Z.
  • max_speed silently zeroes fast segments. It is a plausibility filter, not a display cap. The default of 12 knots is tuned for sailing; for cycling, driving or running, raise it, or your fastest segments will be recorded as 0.
  • Empty time window. Tracks with no points in the window are skipped with a warning; if all tracks are empty the map CLI prints a message and writes nothing. Check that the output file exists rather than assuming it does.
  • Headless rendering. Video mode uses matplotlib; set MPLBACKEND=Agg in the environment. MP4 output additionally requires ffmpeg on PATH: without it the run renders every frame and then dies with ValueError: unknown file extension: .mp4, writing nothing. Check for ffmpeg before choosing MP4, or use --gif, which does not need it.
  • The gpx-player command exits 1 even on success. Its entry point is broken (#21, see the note under Video mode), so exit status is not a usable success signal there. Call python -m gpx_player.main ... instead, and in either case verify that the expected output file exists rather than trusting the return code.
  • Blocked map tiles. A generated HTML opened over file:// may show "blocked" tiles. Serve it over HTTP, see OpenStreetMap tile access.
  • Blocked CDNs. If the viewer cannot reach the JS/CSS hosts listed above, the page loads but the map never initialises, and it fails quietly: the static legend still renders, so "the HTML looks fine" is not evidence the map works.
  • Track-to-name mapping. Names are matched to tracks in file order, and a single GPX file may contain several <trk> elements. If a file has more than one track, supply one name per track, not per file.

Untrusted input

GPX files are XML from arbitrary sources. Track names and descriptions are HTML-escaped and playback data is JSON-escaped before being written into the generated page, so a hostile track name cannot break out of the HTML or the inline script. Still, treat file paths and titles you pass in as data, and do not interpolate agent-controlled text into the shell; call the Python API instead of building command strings.

Support

Now you can buy me a coffee to encourage further development!

"Buy Me A Coffee"

Release files for gpx-player 0.5.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for gpx-player 0.5.2
File Size Uploaded
gpx_player-0.5.2.tar.gz 58.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for gpx-player 0.5.2
File Interpreter ABI Platform
gpx_player-0.5.2-py3-none-any.whl Python 3 none any Details

Total release size: 101.9 kB

Release files / gpx_player-0.5.2.tar.gz

Download URL gpx_player-0.5.2.tar.gz
Size 58.7 kB
Tags Source
SHA-256 checksum
How to use checksums
547f01eb9779cf15264bff1565f7b332fa72ef489a595df7f66f62e8ceae2af9
BLAKE2b-256 checksum
How to use checksums
37dae9822cec9818a51d54c8b78eb37f87b83fd6dc1f656308afb8c80f29f51a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 1, 2026.

Transparency log

Release files / gpx_player-0.5.2-py3-none-any.whl

Download URL gpx_player-0.5.2-py3-none-any.whl
Size 43.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e1d3ecbcc29e88b9df8a34e6d0e708716da216d933f04966febc7fa4ad3dd307
BLAKE2b-256 checksum
How to use checksums
cbe147b51136bbfb51a9b06e08a229eadaa58ebc6fb27ebe95bfa5cb75d4aa85
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 1, 2026.

Transparency log

Release history Release notifications | RSS feed

0.5.3

2 release files

This release

0.5.2 This release

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.2

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page