Skip to main content
Back to blog

Working with GPX files: parsing, rendering, and the messy reality

·5 min readWeb Dev

GPX looks simple until you try to build something with it. I have been working on Routemade, a project that turns GPX routes into custom apparel, and the format has been both a blessing and a headache. The spec is clean. The real-world files people upload are not.

What GPX actually is

GPX (GPS Exchange Format) is XML. That is both its strength and its weakness. It is human-readable, widely supported, and every GPS device and fitness app can export it. The structure breaks down into three main elements:

  • Tracks (<trk>): A recorded path, made up of track segments (<trkseg>) containing ordered trackpoints (<trkpt>). This is what you get when you record a hike or bike ride.
  • Routes (<rte>): A planned path, defined by route points (<rtept>). Think of it as a set of waypoints the GPS should navigate between.
  • Waypoints (<wpt>): Individual points of interest. A campsite, a summit, a parking spot.

Each point has latitude and longitude as attributes, and optionally elevation (<ele>) and a timestamp (<time>). A minimal trackpoint looks like this:

<trkpt lat="47.3769" lon="10.8986">
  <ele>1034.5</ele>
  <time>2023-06-15T08:30:12Z</time>
</trkpt>

Simple enough. The problems start when you open files from actual devices.

The mess: device and app quirks

Every GPS device and fitness app has its own interpretation of the GPX spec. Some of the things I have run into building Routemade:

Garmin devices love to add extension namespaces with heart rate, cadence, temperature, and power data. The files are verbose. A day-long hike can produce a 15MB GPX file because Garmin records a point every second and packs each one with sensor data.

Strava exports are cleaner but sometimes strip elevation data entirely if the activity was recorded on a phone without a barometric altimeter. You get lat/lon and timestamps, but <ele> is just missing.

Komoot planned routes come as route points rather than trackpoints, which means the path between points is not defined. You get the turn-by-turn waypoints, but not the actual road or trail geometry. If you are trying to draw the route on a map, you need to either snap those points to a road network or just connect them with straight lines and accept the visual inaccuracy.

Then there are the timestamp issues. Some apps export timestamps in UTC, others in local time with no timezone indicator. Some files have timestamps on every point, others only on the first and last. I have seen files where the timestamps go backwards mid-track, probably from a GPS glitch or a device restart.

File sizes are another problem. A multi-day bikepacking trip recorded at one-second intervals can be 30MB or more of XML. That is a lot of data to parse, process, and render in a browser.

Parsing: libraries vs. doing it yourself

For Python, gpxpy is the standard choice. It handles the common quirks, parses extensions, and gives you clean objects to work with. For a backend processing pipeline, it is the right call.

import gpxpy
 
with open("ride.gpx", "r") as f:
    gpx = gpxpy.parse(f)
 
for track in gpx.tracks:
    for segment in track.segments:
        for point in segment.points:
            print(f"{point.latitude}, {point.longitude}, {point.elevation}")

On the JavaScript side, options are thinner. Libraries like gpx-parser-builder exist, but I ended up writing a lightweight parser using the DOMParser API for browser-side work. GPX is just XML, so parsing it is straightforward if you only need tracks and waypoints. The tricky part is handling the edge cases: missing elevation, multiple track segments, extensions you did not expect.

Rolling your own parser is tempting because the format is simple. But you will keep finding new edge cases from new devices for months. Using a maintained library saves time in the long run.

Rendering on a map

Once you have an array of coordinates, putting them on a map is the easy part. Leaflet with a polyline, or Mapbox GL with a GeoJSON line layer. Connect the dots and you have a route.

The hard part is performance. A track with 50,000 points does not need all of them to look good on a map at most zoom levels. This is where simplification algorithms come in. The Douglas-Peucker algorithm is the classic choice. It reduces the number of points while preserving the shape of the line. Libraries like Turf.js include it, or you can use simplify-js by Vladimir Agafonkin (the same person behind Leaflet).

For Routemade, I simplify tracks aggressively for the preview map but keep the full resolution data for the final product rendering. A 50,000-point track can often be simplified to 2,000 points with no visible difference at typical zoom levels.

Elevation profiles

Elevation data is where things get really inconsistent. Phone GPS elevation is notoriously bad, often off by 20-50 meters. Even dedicated GPS devices produce noisy elevation data that needs smoothing before you can draw a usable elevation profile.

I apply a simple moving average to smooth the elevation data before rendering profiles. A window of 5-10 points works well for most activities. Without smoothing, your elevation profile looks like a seismograph reading.

Some services like the Google Elevation API or open alternatives like Open-Elevation can replace GPS elevation with DEM (Digital Elevation Model) data. This gives you much more accurate profiles but adds an external dependency and API calls for every file.

What I would tell someone starting out

If you are building something that accepts GPX files from users, plan for chaos. Validate aggressively, handle missing fields gracefully, and test with files from as many different sources as you can find. The GPX spec is clean, but the ecosystem around it is not.

Use an established parsing library. Simplify tracks before rendering. Smooth elevation data before displaying profiles. And never assume a GPX file will have all the fields the spec says it should.

Sources

Enjoying the blog? Subscribe via RSS to get new posts in your reader.

Subscribe via RSS