Simplify a GPX file or line

POST https://api.trailsplits.com/v1/gpx/simplify

Checked by a live probe Key optional · 500 credits a day without one

Fewer points with the shape kept within a tolerance in ground metres (or a point budget), the climb protected and stops (waypoints, pauses of 10 minutes or more) kept in place. Reports the largest deviation and the distance and climb before and after. Answers equal the TrailSplits GPX Simplifier. Body at most 5 MB, 50,000 points.

Example

Simplify a GPX file or line (curl)
# Without a key, leave out the Authorization line (500 credits a day per address).
curl --fail-with-body 'https://api.trailsplits.com/v1/gpx/simplify' \
  -H "Authorization: Bearer $TRAILSPLITS_API_KEY" \
  -H 'Content-Type: application/json' \
  --data '{"geometry":{"type":"LineString","coordinates":[[12.1352,46.5369],[12.13509,46.536875],[12.13498,46.53681],[12.13487,46.536785],[12.13476,46.53672],[12.13465,46.536695],[12.13454,46.53663],[12.13443,46.536605],[12.13432,46.53654],[12.13421,46.536515],[12.1341,46.53645],[12.13399,46.536425],[12.13388,46.53636],[12.13377,46.536335],[12.13366,46.53627],[12.13355,46.536245],[12.13344,46.53618],[12.13333,46.536155],[12.13322,46.53609],[12.13311,46.536065],[12.133,46.536]]},"tolerance_m":10}'

Sends a real request, with your key or the keyless allowance.

Request body

FieldTypeDescription
gpxstring
geometryobject
tolerance_mnumber
max_pointsintegerBudget mode instead of tolerance_m.
keep_climbboolean
keep_stopsboolean
indicesboolean

Response

FieldTypeDescription
schemastring: gpx-simplify/1
engineobject
settingsobject
inputobject
resultobject
gpxstring
geometryobject
Full response schema (JSON Schema)
{
  "type": "object",
  "properties": {
    "schema": {
      "type": "string",
      "enum": [
        "gpx-simplify/1"
      ]
    },
    "engine": {
      "type": "object"
    },
    "settings": {
      "type": "object"
    },
    "input": {
      "type": "object",
      "properties": {
        "format": {
          "type": "string"
        },
        "segments": {
          "type": "integer"
        },
        "points": {
          "type": "integer"
        }
      },
      "required": [
        "format",
        "segments",
        "points"
      ]
    },
    "result": {
      "type": "object",
      "properties": {
        "points": {
          "type": "integer"
        },
        "removed": {
          "type": "integer"
        },
        "max_deviation_m": {
          "type": "number",
          "nullable": true
        }
      },
      "required": [
        "points",
        "removed"
      ]
    },
    "gpx": {
      "type": "string"
    },
    "geometry": {
      "type": "object"
    }
  },
  "required": [
    "schema",
    "input",
    "result"
  ]
}

Errors

400, 413, 422, plus the gateway's 401, 429 and 503 (error conventions). A refused request costs no credits.

Contract

Two operations for files from watches, phones and apps:

  • Clean: drop repeated points and points whose coordinates cannot be read. On request, also remove the timestamps or the sensor data (heart rate, cadence, power).
  • Simplify: remove the points the route's shape and climb do not need, to fit a device's point limit or make the file small.

Both use the same rules and engine as the GPX Simplifier (src/lib/gpx-ops), so a file gives the same points here as on the page.

The file comes back as the file. Only the points removed, and on request the timestamps or sensor data, are cut out of its text. Everything else stays byte for byte: the header, metadata, waypoints, names (CDATA included), comments, the kept points with their own extras, and the indentation. A point alone on its line goes with its line, so no blank lines are left; one sharing a line goes with the spaces that separated it. An unchanged file comes back identical. Nothing is stored.

Limits

  • Body: up to 5 MB, which is larger than the other products allow, so a long recording with heart rate fits.
  • At most 50,000 points.
  • Measured on a laptop:
    • a 50,000-point recording cleans in 0.3 s and simplifies in 0.2 to 0.3 s (one with heart rate and times on every point is 7.7 MB, over the limit: send such a file in pieces, or GeoJSON to simplify);
    • 50,000 points as GeoJSON simplify in under 0.1 s.
  • A file of more than 10,000 points takes one of the worker's heavy-job slots, to clean or to simplify: either holds the worker's event loop. Beyond the worker's slots and queue the answer is 503 overloaded.

Reading a GPX

  • Track points are grouped by <trkseg>. Points placed straight in a <trk> form a segment of their own.
  • Route points (<rtept>) are grouped by <rte>, and are read only when the file has no track points.
  • Waypoints (<wpt>) are read for their position and name.
  • Prefixes: element names may carry a namespace prefix (<gpx:trkpt>).
  • Comments and CDATA are not read as markup.
  • Refused files: a TCX, a KML, a file without <gpx>, or a cut-off element is invalid_gpx.

POST /v1/gpx/clean

{ "gpx": "<gpx…>", "strip_times": true, "strip_extensions": true }
FieldDefault
gpxrequiredThe GPX document as a string
dedupetrueDrop a point that repeats the one before it: the same latitude and longitude (as numbers: 47.37 repeats 47.370000) and the same height as written
drop_badtrueDrop points whose latitude or longitude is missing, not a number or outside the world
strip_timesfalseRemove every <time> in the file: in points, in waypoints and the file's own date in <metadata>
strip_extensionsfalseRemove each point's <extensions> (heart rate, cadence, power, temperature)
callerOptional label for logs and receipts

Response:

FieldMeaning
schemagpx-clean/1
settings, settings_defaultedWhat was applied, and which settings were not given
reportchanged, point_kind (track \route), segments, points_before, points_after, duplicates, bad_coordinates, times_removed, extensions_removed, duplicates_found and bad_found (what there is to clean even with an option off), had_times, had_extensions, waypoints
gpxThe cleaned file; identical to the input when changed is false
timingtotal_ms

A time or an extension inside a point that was dropped is not counted as removed: it went with its point.

POST /v1/gpx/simplify

{ "gpx": "<gpx…>", "tolerance_m": 10 }
{ "gpx": "<gpx…>", "max_points": 1000 }
{ "geometry": { "type": "LineString", "coordinates": [[lon, lat, height?], …] }, "tolerance_m": 5 }
FieldDefault
gpx or geometryone of themA GPX document, or a GeoJSON line as for route analysis
tolerance_m10Tolerance mode: keep what lies further than this from the kept line, in ground metres; 0.5 to 1,000. The page's presets are 5 (light), 10 (medium) and 50 (strong)
max_pointsBudget mode: keep at most this many points over the whole file, the most needed first. Give it instead of tolerance_m, never both
keep_climbtrueProtect the climb as the page does. In tolerance mode, no dropped point sits more than 5 m off the kept climb. In budget mode, a metre of climb counts as 3 m of shape. Recorder noise of 5 m or less costs nothing
keep_stopstrueKeep the stops where they are: the point nearest each waypoint within 100 m of the route, and both sides of every pause of 10 minutes or more in the recording
indicesfalseAdd kept: the index of every kept point in each segment
callerOptional label

The rules, as on the page:

  • Each segment is simplified on its own and keeps both of its ends, so a segment is never left with a single point.
  • A budget smaller than the ends of every segment and every stop is refused (budget_too_small, with the minimum). The end of a route is never cut to meet the number.
  • A GPX with unreadable coordinates is refused (invalid_coordinates). Clean it first: drop_bad drops them.

Response:

FieldMeaning
schemagpx-simplify/1
settings, settings_defaultedtolerance_m or max_points, keep_climb, keep_stops, indices, and climb: the climb rule in words
inputformat (gpx \geojson), segments, points
resultpoints kept and removed; max_deviation_m, the largest distance of any original point from the kept line; worst (segment, index, deviation_m) or null; before and after, each with points, distance_m, ascent_m, descent_m, min_m, max_m and heights_share
stops[]kind (waypoint \pause), segment, index, name, minutes. Inside a segment only: a waypoint at a segment's end is not listed, because the ends are always kept
keptWith indices: true: indices per segment
gpx or geometryThe simplified file, or for GeoJSON input a LineString (for a LineString sent) or a MultiLineString, with heights where they were sent
timingtotal_ms

Measuring:

  • Distance and climb are measured within each segment, never across the gap between two.
  • Climb uses the house 5 m hysteresis, and is shown only when at least half of the points carry a height.
  • After simplification the ascent can read a few metres more or less than before, because a 5 m threshold sees a simpler line.

Errors

{ "error": { "code", "message" } }.

HTTPcodeWhen
400invalid_requestNot JSON, an unknown field, no gpx, both or neither of gpx/geometry (simplify), both tolerance_m and max_points, a value out of range
400invalid_gpxgpx is not a string, or not a GPX (a TCX, a KML, no <gpx>), or cut off
400invalid_coordinatesGeoJSON coordinates outside the world
405method_not_allowedAny method but POST (and OPTIONS); the answer carries Allow: POST
413body_too_large, too_many_pointsOver 5 MB, or over 50,000 points
422no_lineNo track or route points; GeoJSON without a line of two points
422invalid_coordinatesSimplify: GPX points whose coordinates cannot be read (clean first)
422budget_too_smallmax_points below the ends of every segment and every stop
503overloadedA file of more than 10,000 points found the worker's heavy-job slots and queue full; Retry-After

Versioning

Additive fields may appear in gpx-clean/1 and gpx-simplify/1. A change of meaning or a removed field is version 2.