Anleitung

Ein Höhenprofil für Ihre Tourenseiten

Ein Profil unter der Karte zeigt, wie weit eine Tour führt und wie viel sie steigt. Die Höhen kommen aus einer Anfrage pro Tour, die Sie einmal machen und als Datei neben der Seite speichern. Wer die Seite besucht, lädt danach nur die Karte.

Die Karte wird geladen…
Der Code unten, mit dem 5-Seenweg oberhalb von Zermatt, vorbei an fünf Seen. Zeigen Sie auf das Profil oder auf die Route.

Drei Schritte

  1. Speichern Sie die Tour als GPX-Datei. Tourenplaner, GPS-Geräte und Apps wie Komoot und Strava können eine exportieren.
  2. Schicken Sie die Datei einmal an die API, mit einem Schlüssel vom Typ «Alle APIs» aus Ihrem Dashboard (erstellen Sie zuerst ein kostenloses Konto), und speichern Sie die Antwort neben Ihrer Tourenseite:
    curl https://api.overpass.ch/v1/IHR_API_SCHLÜSSEL/elevation/profile --data-binary @5-seenweg.gpx -o 5-seenweg.json
  3. Erstellen Sie einen Schlüssel vom Typ «Nur Karten» mit Ihrer Website als erlaubtem Referer. Kopieren Sie den Code unten, setzen Sie diesen Schlüssel dort ein, wo IHR_SCHLÜSSEL steht, und ändern Sie 5-seenweg.json in den Namen Ihrer Datei.

Die Antwort hat alle 90 m einen Punkt, jeden mit seiner Höhe und seiner Distanz vom Start, und eine Zusammenfassung mit der Distanz, dem Auf- und Abstieg und dem tiefsten und höchsten Punkt. Die Seite zeichnet alles aus dieser Datei und macht selbst keine Höhenabfragen. Der Schlüssel in der Seite kann deshalb ein Schlüssel «Nur Karten» sein. Wer die Seite besucht, kann ihn sehen, aber er kann keine Höhen abfragen.

Der Code

<link rel="stylesheet" href="https://unpkg.com/maplibre-gl@6.13.0/dist/maplibre-gl.css">
<style>
  .tour-facts { display: flex; flex-wrap: wrap; gap: 4px 20px; margin: 0 0 10px; }
  .tour-facts b { display: block; font-size: 1.25em; }
  #map { height: 380px; border-radius: 8px 8px 0 0; }
  #profile { display: block; width: 100%; height: 180px; background: #f4f6f8; border-radius: 0 0 8px 8px; touch-action: pan-y; }
  #profile text { font: 11px system-ui, sans-serif; fill: #5b6670; }
  .tour-credit { font-size: 11px; color: #6b7480; margin-top: 4px; }
</style>

<div class="tour-facts" id="facts"></div>
<div id="map"></div>
<svg id="profile" role="img" aria-label="Höhenprofil der Tour"></svg>
<p class="tour-credit">Höhen: 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.</p>

<script type="module">
import * as maplibregl from "https://unpkg.com/maplibre-gl@6.13.0/dist/maplibre-gl.mjs";

// Die Antwort von /elevation/profile für diese Tour, neben der Seite gespeichert
const tour = await (await fetch("5-seenweg.json")).json();
const points = tour.results; // ein Punkt alle 90 m, mit Distanz und Höhe
const sum = tour.profile;

// Wanderzeit nach der Regel von Naismith: 5 km in der Stunde, dazu eine Stunde je 600 m Aufstieg
const hours = sum.distance_m / 5000 + sum.ascent_m / 600;
const time = `${Math.floor(hours)} h ${String(Math.round((hours % 1) * 60)).padStart(2, "0")} min`;
const m = (v) => `${Math.round(v).toLocaleString("de-CH")} m`;
const km = (v) => (v / 1000).toFixed(1).replace(".", ",") + " km";
const facts = [["Strecke", km(sum.distance_m)], ["Aufstieg", m(sum.ascent_m)],
  ["Abstieg", m(sum.descent_m)], ["Höchster Punkt", m(sum.max_m)], ["Wanderzeit", time]];
document.getElementById("facts").innerHTML = facts.map(([k, v]) => `<div>${k}<b>${v}</b></div>`).join("");

const line = points.map((p) => [p.location.lng, p.location.lat]);
const map = new maplibregl.Map({
  container: "map",
  style: "https://api.overpass.ch/v1/IHR_SCHLÜSSEL/maps/styles/outdoor.json?lang=de",
  bounds: line.reduce((b, c) => b.extend(c), new maplibregl.LngLatBounds(line[0], line[0])),
  fitBoundsOptions: { padding: 40 },
  cooperativeGestures: true // die Seite scrollt an der Karte vorbei; Strg + Mausrad oder zwei Finger bewegen sie
});
map.addControl(new maplibregl.NavigationControl({ showCompass: false }));
map.on("load", () => {
  map.addSource("tour", { type: "geojson", data: { type: "LineString", coordinates: line } });
  map.addLayer({ id: "tour-casing", type: "line", source: "tour", paint: { "line-color": "#fff", "line-width": 7 },
    layout: { "line-join": "round", "line-cap": "round" } });
  map.addLayer({ id: "tour", type: "line", source: "tour", paint: { "line-color": "#c2185b", "line-width": 4 },
    layout: { "line-join": "round", "line-cap": "round" } });
});
new maplibregl.Marker({ color: "#2b8a3e", scale: 0.8 }).setLngLat(line[0]).addTo(map);
new maplibregl.Marker({ color: "#c2185b", scale: 0.8 }).setLngLat(line.at(-1)).addTo(map);
const here = document.createElement("div");
here.style.cssText = "width:14px;height:14px;border-radius:50%;background:#c2185b;border:3px solid #fff;box-shadow:0 0 3px #0006";
const hereMarker = new maplibregl.Marker({ element: here });

// Das Profil: in der Breite der Seite gezeichnet, neu, wenn sie sich ändert
const svg = document.getElementById("profile");
const NS = "http://www.w3.org/2000/svg";
let x, y;
function draw() {
  const W = svg.clientWidth, H = svg.clientHeight, left = 58, bottom = 22;
  svg.setAttribute("viewBox", `0 0 ${W} ${H}`);
  const lo = sum.min_m, hi = sum.max_m, step = hi - lo > 600 ? 200 : 100;
  const y0 = Math.floor(lo / step) * step, y1 = Math.ceil(hi / step) * step;
  x = (d) => left + (d / sum.distance_m) * (W - left - 12);
  y = (h) => 10 + (1 - (h - y0) / (y1 - y0)) * (H - 10 - bottom);
  const path = points.map((p, i) => `${i ? "L" : "M"}${x(p.distance).toFixed(1)},${y(p.elevation).toFixed(1)}`).join("");
  let out = `<path d="${path}L${x(sum.distance_m)},${y(y0)}L${x(0)},${y(y0)}Z" fill="#c2185b" fill-opacity=".15"/>`
    + `<path d="${path}" fill="none" stroke="#c2185b" stroke-width="2"/>`;
  for (let h = y0; h <= y1; h += step) out += `<line x1="${left}" x2="${W - 12}" y1="${y(h)}" y2="${y(h)}" stroke="#0002"/>`
    + `<text x="${left - 6}" y="${y(h) + 4}" text-anchor="end">${h.toLocaleString("de-CH")} m</text>`;
  const kmStep = sum.distance_m > 20000 ? 5 : sum.distance_m > 8000 ? 2 : 1;
  for (let k = 0; k * 1000 <= sum.distance_m; k += kmStep) out += `<text x="${x(k * 1000)}" y="${H - 6}" text-anchor="middle">${k} km</text>`;
  out += `<g id="cursor" visibility="hidden"><line y1="10" y2="${H - bottom}" stroke="#333"/><circle r="4" fill="#c2185b" stroke="#fff" stroke-width="2"/>`
    + `<text id="cursor-text" y="22" style="fill:#222;font-weight:600"></text></g>`;
  svg.innerHTML = out;
}
draw();
new ResizeObserver(draw).observe(svg);

// Auf das Profil oder die Route zeigen: beide zeigen dieselbe Stelle
function show(i) {
  const p = points[i], c = document.getElementById("cursor"), X = x(p.distance), W = svg.clientWidth;
  c.setAttribute("visibility", "visible");
  c.querySelector("line").setAttribute("transform", `translate(${X},0)`);
  c.querySelector("circle").setAttribute("transform", `translate(${X},${y(p.elevation)})`);
  const t = c.querySelector("text");
  t.textContent = `${km(p.distance)} · ${m(p.elevation)}`;
  t.setAttribute("x", X > W - 130 ? X - 8 : X + 8);
  t.setAttribute("text-anchor", X > W - 130 ? "end" : "start");
  hereMarker.setLngLat([p.location.lng, p.location.lat]).addTo(map);
}
function hide() {
  document.getElementById("cursor").setAttribute("visibility", "hidden");
  hereMarker.remove();
}
svg.addEventListener("pointermove", (e) => {
  const d = ((e.offsetX - x(0)) / (x(sum.distance_m) - x(0))) * sum.distance_m;
  show(points.reduce((best, p, i) => (Math.abs(p.distance - d) < Math.abs(points[best].distance - d) ? i : best), 0));
});
svg.addEventListener("pointerleave", hide);
map.on("mousemove", "tour", (e) => {
  const near = (p) => (p.location.lng - e.lngLat.lng) ** 2 + (p.location.lat - e.lngLat.lat) ** 2;
  show(points.reduce((best, p, i) => (near(p) < near(points[best]) ? i : best), 0));
});
map.on("mouseleave", "tour", hide);
</script>

Wanderzeit

Der Code schätzt die Wanderzeit mit der Regel von Naismith: eine Stunde pro 5 km, dazu eine Stunde pro 600 m Aufstieg. Für den 5-Seenweg ergibt das 2 h 38 min. Wenn Sie mit einer anderen Regel arbeiten, ändern Sie die Zeile, die hours berechnet. Bei Velotouren nehmen Sie die Wanderzeit aus facts heraus.

In WordPress und Website-Baukästen

Fügen Sie den Code in einen Block «Individuelles HTML» ein, wie bei der Karte für ein Geschäft. WordPress nimmt keine JSON-Dateien in die Mediathek auf. Laden Sie die Datei mit dem Dateimanager Ihres Hostings hoch, oder setzen Sie ihren Inhalt in den Code ein: Ersetzen Sie await (await fetch("5-seenweg.json")).json() damit. Eine Tour von 10 km hat etwa 17 KB.

Was es kostet

Das Profil kostet eine Höhenabfrage pro Punkt, einmal: etwa 120 für die 10,6 km des 5-Seenwegs. Ein neues Profil braucht es nur, wenn sich die Route ändert. Die Besuche kosten Kartenkacheln. Die erste Ansicht der Demo lädt auf dem Handy 7 Kacheln und auf einem grossen Bildschirm 13; wer zweimal herauszoomt und die Karte viermal verschiebt, lädt etwa 45. Ein Browser behält die Kacheln einen Tag. Wir haben das mit MapLibre GL JS 6.13.0 in Fenstern von 1280 × 720 und 390 × 700 Pixeln gemessen.

TarifHöhenabfragen im MonatKartenkacheln im MonatBesuche mit 45 KachelnFür eine Firmenwebsite
Jedes Konto, kostenlos1 Million100'000etwa 2'200✗ Nein, nur für nicht kommerzielle Websites
Elevation, $7 im Monat50 Millionen100'000etwa 2'200✓ Ja
Maps Small, $12 im Monat50 Millionen2 Millionenetwa 44'000✓ Ja

Preise zuzüglich Steuern. Alle Tarife: Preise.

Gut zu wissen

  • Die Höhen kommen aus einem Raster mit einem Punkt alle 90 m. Über eine ganze Tour liegen die Summen nahe bei denen von Tourenplanern: Für den 5-Seenweg erhalten wir 301 m Aufstieg und 600 m Abstieg. Ein paar enge Kehren in einem steilen Hang glätten sich, und jeder Planer zählt den Aufstieg etwas anders, die Zahlen weichen also zwischen Apps um einige Prozent ab.
  • Wo Sie Höhen zeigen, nennen Sie die Quelle der Höhendaten: die Zeile unter dem Profil im Code. Die Karte nennt ihre eigenen Daten in einer Ecke.
  • Der Stil Outdoor zeigt Wanderwege nach Schwierigkeit, Hütten und Haltestellen. Für eine Velotour passt light oder driving vielleicht besser; alle neun Stile funktionieren.
  • Markierte Wanderwege sind in OpenStreetMap. Das Beispiel Profil eines Wanderwegs holt eine Route mit einer Overpass-Abfrage, mit ihren Höhen.