A hiking map in MapLibre GL JS
A map for hikers needs trails with their difficulty, huts, stops and water, and the terrain. With the outdoor style, or with the trails added to a style of your own, a popup for each trail and your route on top. Tested with MapLibre GL JS 6.13.0.
const map = new maplibregl.Map({
container: "map",
style: "https://api.overpass.ch/v1/YOUR_KEY/maps/styles/outdoor.json",
center: [7.96, 46.6],
zoom: 13
});
Add ?terrain=1 for 3D, and ?lang=de or another of 41 languages for the labels.
Trails by difficulty
The colours follow the SAC scale of the Swiss Alpine Club, which OpenStreetMap stores in the sac_scale tag, and the colours of Swiss signposts:
grade | SAC scale | sac_scale in OpenStreetMap | Colour |
|---|---|---|---|
| 1 | T1 hiking | hiking | yellow |
| 2 | T2 mountain hiking | mountain_hiking | red |
| 3 | T3 demanding mountain hiking | demanding_mountain_hiking | red |
| 4 | T4 alpine hiking | alpine_hiking | blue |
| 5 | T5 demanding alpine hiking | demanding_alpine_hiking | blue |
| 6 | T6 difficult alpine hiking | difficult_alpine_hiking | blue |
| 0 | no grade in the data | none | grey, from zoom 12 |
The outdoor style also shows tracks, via ferratas, huts, campsites, viewpoints, stations and stops, drinking water, peaks with their height, rock, glaciers and cliffs. Graded trails show from zoom 11.
Trails on a style of your own
The trails come as vector tiles of their own, so you can draw them on any style. This adds them to the light style, with a popup for the trail under the mouse:
map.on("load", () => {
map.addSource("trails", {
type: "vector",
url: "https://api.overpass.ch/v1/YOUR_KEY/maps/trails.json"
});
map.addLayer({
id: "trails",
type: "line",
source: "trails",
"source-layer": "trails",
filter: [">", ["get", "grade"], 0],
paint: {
// T1 yellow, T2 and T3 red, T4 to T6 blue, as on Swiss signposts
"line-color": ["match", ["get", "grade"], 1, "#d39b00", [2, 3], "#d0312d", "#1f5fbf"],
"line-width": ["interpolate", ["linear"], ["zoom"], 11, 1, 16, 3],
"line-dasharray": [3, 1.6]
}
});
map.on("click", "trails", (e) => {
const t = e.features[0].properties;
new maplibregl.Popup()
.setLngLat(e.lngLat)
.setText(`${t.name || "Trail"} · T${t.grade}`)
.addTo(map);
});
map.on("mouseenter", "trails", () => (map.getCanvas().style.cursor = "pointer"));
map.on("mouseleave", "trails", () => (map.getCanvas().style.cursor = ""));
});
The tiles have two layers, from zoom 11 to 13; closer in, MapLibre enlarges zoom 13:
trails: lines withgrade(0 to 6, as in the table),kind,name,refandvisibility. Paths without a grade are in the tiles from zoom 12.points, from zoom 13: bus and tram stops, stations, ferry terminals, cable car stations, drinking water and springs, withkindandname.
They come from the live OpenStreetMap data of our Overpass server, and a tile is at most 7 days old. A newly graded trail shows within a week.
Your route on top
A route you planned or recorded goes on top as GeoJSON. Convert GPX files to GeoJSON first, for example with the @tmcw/togeojson package.
map.addSource("route", { type: "geojson", data: "/route.geojson" });
map.addLayer({
id: "route",
type: "line",
source: "route",
layout: { "line-cap": "round", "line-join": "round" },
paint: { "line-color": "#7b2cbf", "line-width": 4, "line-opacity": 0.8 }
});
Added last, the route lies above trails and labels. For the profile of the route, with ascent and descent, send it to the elevation API with the same key; the example elevation profile of a hiking route does this for a route from OpenStreetMap.
Limits
- The grade is what mappers entered. Many paths have none and show grey; a grade can be old or wrong. Say so in your app, and do not use the map as the only source for a mountain tour.
- The base map is updated every week, the trails and stops within 7 days.
- Contour lines and terrain come from a grid with 90 m spacing: good for mountains, too coarse for small slopes.
- Every trail, contour, terrain and vector tile counts as one map tile. The first view of the outdoor style at zoom 13.2, 1280 × 720 pixels, loaded 19 tiles. Every account includes 100'000 a month.