Skip to content

Add contour lines

Replicates the MapLibre example Add contour lines. The YAML declares the DEM, a hillshade over it, and contour line and label layers reading a vector source at contours://{z}/{x}/{y}. That protocol is the maplibre-contour plugin, registered by page JS on the maplibre module <ml-map> renders with. The plugin is built on the first contour tile request from the document’s own dem source, so the DEM is declared once.

Contours in feet over the Inn valley, labelled on the major lines
add-contour-lines.yaml
# yaml-language-server: $schema=https://docs.maplibre-yaml.org/schema/latest/map.schema.json
# Gallery twin of https://maplibre.org/maplibre-gl-js/docs/examples/add-contour-lines/
# The YAML half: the DEM, a hillshade over it, and contour line + label
# layers reading a vector source served by the `contours://` protocol.
# That protocol is the maplibre-contour plugin, registered by page JS on
# the maplibre module <ml-map> renders with — see add-contour-lines.js.
# The DEM is declared once, here: the plugin reads its URL and encoding
# from this document's `dem` source.
#
# NOTE for the live sweep: renders only with the page's protocol
# registration; scripts/verify-docs-gallery.mjs drives it through the
# hatch harness, which runs that JS.
type: map
id: add-contour-lines
config:
center: [11.3229, 47.2738]
zoom: 13
mapStyle: "https://demotiles.maplibre.org/style.json"
sources:
dem:
type: raster-dem
tiles: ["https://demotiles.maplibre.org/terrain-tiles/{z}/{x}/{y}.png"]
encoding: mapbox
tileSize: 256
maxzoom: 12
contours:
type: vector
tiles: ["contours://{z}/{x}/{y}"]
maxzoom: 15
layers:
- id: hills
type: hillshade
source: dem
paint:
hillshade-exaggeration: 0.25
- id: contours
type: line
source: contours
source-layer: contours
paint:
line-opacity: 0.5
# "major" contours have level 1, "minor" level 0
line-width: ["match", ["get", "level"], 1, 1, 0.5]
- id: contour-text
type: symbol
source: contours
source-layer: contours
filter: [">", ["get", "level"], 0]
layout:
symbol-placement: line
text-size: 10
text-field: ["concat", ["number-format", ["get", "ele"], {}], "'"]
text-font: ["Noto Sans Bold"]
paint:
text-halo-color: white
text-halo-width: 1
add-contour-lines.js
// The JavaScript half: a protocol plugin on the maplibre module <ml-map>
// renders with. maplibre-contour turns DEM tiles into vector contour
// tiles; registering `contours://` through @maplibre-yaml/core/maplibre
// guarantees it lands on the same module the document's source requests
// go through. The plugin is built lazily, on the first contour tile, from
// the `dem` source the YAML declares — so the DEM lives in one place.
import { addProtocol } from "@maplibre-yaml/core/maplibre";
import mlcontour from "maplibre-contour";
const mapEl = document.querySelector("ml-map");
const contourOptions = {
multiplier: 3.28084, // metres to feet
overzoom: 1,
thresholds: {
// zoom: [minor, major] interval, in feet
11: [200, 1000],
12: [100, 500],
13: [100, 500],
14: [50, 200],
15: [20, 100],
},
elevationKey: "ele",
levelKey: "level",
contourLayer: "contours",
};
let demSource;
function dem() {
if (!demSource) {
const spec = mapEl.getMap().getStyle().sources.dem;
const template = spec.tiles[0];
demSource = new mlcontour.DemSource({
// The worker resolves URLs against nothing — make a site path absolute.
url: template.startsWith("/") ? location.origin + template : template,
encoding: spec.encoding ?? "mapbox",
maxzoom: spec.maxzoom ?? 12,
worker: true, // compute contours off the main thread
});
}
return demSource;
}
addProtocol("contours", (request, abortController) => {
const [z, x, y] = request.url.slice("contours://".length).split("/");
const url = dem()
.contourProtocolUrl(contourOptions)
.replace("{z}", z)
.replace("{x}", x)
.replace("{y}", y);
return dem().contourProtocolV4({ ...request, url }, abortController);
});
  • Contour intervals per zoom live in the JS (thresholds); the line styling — major lines thicker — stays in YAML, reading the level property the plugin writes.
  • worker: true computes contours off the main thread.
  • Terrain: MapLibre demo tiles.