Overpass query example

Elevation profile of a hiking route

Distance, ascent and descent of a hiking route from OpenStreetMap, with its elevation profile. Example: the Matterhorn Trail in Zermatt.

Hiking routes are relations tagged route=hiking that group the paths of a trail. Joined into one line and sent to the elevation profile endpoint, they give the distance, the ascent and descent, and a profile to draw.

The Matterhorn Trail runs from Schwarzsee at the foot of the Matterhorn down to Furi above Zermatt.

The query

[out:json][timeout:25];
rel["route"="hiking"]["name"="Matterhorn-Trail"](45.95,7.65,46.05,7.80);
out geom;

Run it

Running the query…

Live result from overpass.ch, cached for up to 12 hours. Map data © OpenStreetMap contributors (ODbL). Heights produced using Copernicus WorldDEM™-90 © DLR e.V. 2010-2014 and © Airbus Defence and Space GmbH 2014-2018 provided under COPERNICUS by the European Union and ESA; all rights reserved. Sea depths: GEBCO Compilation Group (2026) GEBCO 2026 Grid.

How it works

  1. The bounding box makes the name unambiguous: many trails share a name.
  2. out geom; returns the relation with the geometry of each path.
  3. Join the members with an empty, forward or backward role, in order. Leave out alternative and excursion members, which are side trips.
  4. The profile endpoint samples the line every 90 m and adds up the climb and the descent. In this data the trail is about 7.3 km long and descends about 750 m, from Schwarzsee at about 2,580 m to Furi at about 1,860 m.

Variations

  • Joining works only if the members are listed in order along the trail. Some relations are not: check that the joined line is about as long as all ways together, or sort the ways by their shared end points first.
  • Ascent and descent leave out changes of less than 2 m to filter noise. A 90 m grid also smooths short ups and downs, so on a winding trail the real climb is somewhat higher.
  • Find the routes of an area first: hiking routes in a region lists them with their names. Then select one by name, or by its ID: rel(19448726); out geom;.

Run it from your code

Two requests: the query returns the route, and its joined geometry goes to /elevation/profile.

import math
import requests

API = "https://api.overpass.ch/v1/YOUR_API_KEY"
query = open("query.overpassql").read()
r = requests.post(f"{API}/interpreter", data={"data": query}, timeout=120)
r.raise_for_status()
route = r.json()["elements"][0]

def gap(a, b):  # distance in degrees, enough to compare
    return math.hypot(a[0] - b[0], (a[1] - b[1]) * math.cos(math.radians(a[0])))

ways = [[[p["lat"], p["lon"]] for p in m["geometry"]] for m in route["members"]
        if m["type"] == "way" and m["role"] in ("", "forward", "backward")]
path = list(ways[0])
for i, way in enumerate(ways[1:]):
    near = lambda p: min(gap(p, way[0]), gap(p, way[-1]))
    if i == 0 and near(path[0]) < near(path[-1]):
        path.reverse()      # the first way points backwards
    if gap(path[-1], way[-1]) < gap(path[-1], way[0]):
        way = way[::-1]     # so does this one
    path += way

r = requests.post(f"{API}/elevation/profile", json={"path": path}, timeout=60)
r.raise_for_status()
answer = r.json()
print(answer["profile"])    # distance_m, ascent_m, descent_m, min_m, max_m, …
for p in answer["results"][:5]:
    print(p["distance"], p["elevation"])

Save the query as query.overpassql. It works the same in Overpass Turbo, QGIS and JOSM.

More examples

All examples →