Contour lines in MapLibre GL JS
Contour lines join points of the same height. You can load them as vector tiles and style them like roads, or let a plugin compute them in the browser from terrain tiles. Both, with major lines, labels and labels in feet. Tested with MapLibre GL JS 6.13.0.
contours.json and a line layer:
map.addSource("contours", { type: "vector", url: "https://api.overpass.ch/v1/YOUR_KEY/maps/contours.json" });
map.addLayer({ id: "contour-lines", type: "line", source: "contours", "source-layer": "contours" });
Lines and labels
map.on("load", () => {
map.addSource("contours", {
type: "vector",
url: "https://api.overpass.ch/v1/YOUR_KEY/maps/contours.json"
});
map.addLayer({
id: "contour-lines",
type: "line",
source: "contours",
"source-layer": "contours",
paint: {
"line-color": "rgba(124, 92, 58, 0.5)",
"line-width": ["match", ["get", "level"], 1, 1, 0.5] // major lines thicker
}
}, "roads_tunnels_other_casing");
map.addLayer({
id: "contour-labels",
type: "symbol",
source: "contours",
"source-layer": "contours",
minzoom: 12,
filter: ["==", ["get", "level"], 1], // labels on major lines only
layout: {
"symbol-placement": "line",
"text-field": ["concat", ["to-string", ["get", "ele"]], " m"],
"text-font": ["Noto Sans Regular"],
"text-size": 10
},
paint: {
"text-color": "rgba(124, 92, 58, 0.9)",
"text-halo-color": "#ffffff",
"text-halo-width": 1
}
});
});
roads_tunnels_other_casingis the first road layer above the water in our styles and the Protomaps styles. Drawn under it, contour lines stay below roads, buildings and labels. In another style, pass the id of its first road layer.symbol-placement: "line"writes the height along the line. The font must be one the style has: ours have Noto Sans Regular, Medium and Italic.- On a dark style, use a light line colour, such as
rgba(214, 196, 170, 0.55), and a dark halo.
The contour tiles
| Zoom | Interval | Major lines (level 1) |
|---|---|---|
| 10 | 200 m | every 1,000 m |
| 11 | 100 m | every 500 m |
| 12 | 50 m | every 250 m |
| 13 and 14 | 20 m | every 100 m |
The tiles have one layer, contours, with two fields: ele, the height in metres, and level, 1 on every fifth line. They go from zoom 10 to 14; closer in, MapLibre enlarges zoom 14. They are drawn from the Copernicus GLO-90 terrain, a grid with 90 m spacing, so they follow hills and mountains well and do not show small slopes. Below zoom 10 there are no contour lines; hillshading shows hills and valleys there.
Heights in feet
To label the metric lines in feet, convert in the expression:
"text-field": ["concat", ["to-string", ["round", ["*", ["get", "ele"], 3.28084]]], " ft"]
The lines stay at round metres, so the labels read 6562 ft and 6890 ft. For lines at round feet, compute them in the browser with maplibre-contour from our terrain tiles. This ran with maplibre-contour 0.1.1 and MapLibre 6.13.0:
<script src="https://unpkg.com/maplibre-contour@0.1.1/dist/index.min.js"></script>
const demSource = new mlcontour.DemSource({
url: "https://api.overpass.ch/v1/YOUR_KEY/maps/terrain/{z}/{x}/{y}.png",
encoding: "mapbox",
maxzoom: 10,
worker: true
});
demSource.setupMaplibre(maplibregl);
map.on("load", () => {
map.addSource("contours-ft", {
type: "vector",
tiles: [demSource.contourProtocolUrl({
multiplier: 3.28084, // feet
thresholds: { 10: [500, 2500], 11: [200, 1000], 12: [100, 500], 13: [40, 200] }
})],
maxzoom: 15
});
// the same two layers as above, with source "contours-ft" and the label
// "text-field": ["concat", ["number-format", ["get", "ele"], {}], " ft"]
});
The thresholds give the minor and major interval for each zoom, and the plugin sets level to 1 on major lines, so the layers above work unchanged. Each terrain tile it reads counts as one map tile, like a contour tile.
Tiles
Every contour tile counts as one map tile. Only zooms 10 to 14 load them, and tiles without lines, over the sea or flat land, answer HTTP 204 and are free. The first view of the outdoor style at zoom 13.2, 1280 × 720 pixels, with vector, terrain, contour and trail tiles, loaded 19 tiles. Every account includes 100'000 tiles a month.