maplibre-contour is a plugin to render contour lines in MapLibre GL JS from raster-dem sources that powers the terrain mode for onthegomap.com.
Topographic map of Mount Washington
To use it, import the maplibre-contour package with a script tag:
<script src="https://unpkg.com/maplibre-contour@0.1.0/dist/index.min.js"></script>
Or as an ES6 module: npm add maplibre-contour
import mlcontour from "maplibre-contour";
Then to use, first create a DemSource and register it with maplibre:
var demSource = new mlcontour.DemSource({ url: "https://url/of/dem/source/{z}/{x}/{y}.png", encoding: "terrarium", // "mapbox" or "terrarium" default="terrarium" maxzoom: 13, worker: true, // offload isoline computation to a web worker to reduce jank cacheSize: 100, // number of most-recent tiles to cache timeoutMs: 10_000, // timeout on fetch requests }); demSource.setupMaplibre(maplibregl);
Then configure a new contour source and add it to your map:
map.addSource("contour-source", { type: "vector", tiles: [ demSource.contourProtocolUrl({ // convert meters to feet, default=1 for meters multiplier: 3.28084, thresholds: { // zoom: [minor, major] 11: [200, 1000], 12: [100, 500], 14: [50, 200], 15: [20, 100], }, // optional, override vector tile parameters: contourLayer: "contours", elevationKey: "ele", levelKey: "level", extent: 4096, buffer: 1, }), ], maxzoom: 15, });
Then add contour line and label layers:
map.addLayer({ id: "contour-lines", type: "line", source: "contour-source", "source-layer": "contours", paint: { "line-color": "rgba(0,0,0, 50%)", // level = highest index in thresholds array the elevation is a multiple of "line-width": ["match", ["get", "level"], 1, 1, 0.5], }, }); map.addLayer({ id: "contour-labels", type: "symbol", source: "contour-source", "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, }, });
You can also share the cached tiles with other maplibre sources that need elevation data:
map.addSource("dem", { type: "raster-dem", encoding: "terrarium", tiles: [demSource.sharedDemProtocolUrl], maxzoom: 13, tileSize: 256, });
This is an alternative method, which reduces bundle size and also supports other protocols, but it's a bit more complicated to setup. It's similar to how the RTL plugin works.
Import the script from a CDN:
maplibregl.importScriptInWorkers("https://unpkg.com/maplibre-contour@0.1.0/dist/add-protocol-worker.js"); const dispatcher = maplibregl.getGlobalDispatcher(); dispatcher.registerMessageHandler("contour-worker" as any, async () => { // when the script finishes loading in the workers it will send out this message. await dispatcher.broadcast("contour-worker" as any, { // this is to configure the worker with the relevant parameters demUrlPattern: "https://url/of/dem/source/{z}/{x}/{y}.png", // this can also support other protocols such as pmtiles:// encoding: "terrarium", maxzoom: 13 }); // once this is configured, you can add a source with the following address for example, and layers as described above. map.addSource("contour-source", { type: "vector", tiles: ["dem-contour://{z}/{x}/{y}?contourLayer=contours&elevationKey=ele&levelKey=level&multiplier=3.28084&overzoom=1&thresholds=11*200*1000~12*10*100~13*10*100~14*10*100~15*10*100"], }); });
DemSource.setupMaplibre uses MapLibre's addProtocol utility to register a callback to provide vector tile for the contours source. Each time maplibre requests a vector tile:
DemManagerfetches (and caches) the raster-dem image tile and its neighbors so that contours are continuous across tile boundaries.- When
DemSourceis configured withworker: true, it usesRemoteDemManagerto spawnworker.tsin a web worker. The web worker runsLocalDemManagerlocally and uses theActorutility to send cancelable requests and responses between the main and worker thread.
- When
decode-image.tsdecodes the raster-dem image RGB values to meters above sea level for each pixel in the tile.HeightTilestitches those raw DEM tiles into a "virtual tile" that contains the border of neighboring tiles, aligns elevation measurements to the tile grid, and smooths the elevation measurements.isoline.tsgenerates contour isolines from aHeightTileusing a marching-squares implementation derived from d3-contour.vtpbf.tsencodes the contour isolines as mapbox vector tile bytes.
MapLibre sends that vector tile to its own worker, decodes it, and renders as if it had been generated by a server.
There are a lot of parameters you can tweak when generating contour lines from elevation data like units, thresholds, and smoothing parameters. Pre-generated contour vector tiles require 100+gb of storage for each variation you want to generate and host. Generating them on-the-fly in the browser gives infinite control over the variations you can use on a map from the same source of raw elevation data that maplibre uses to render hillshade.
maplibre-contour is licensed under the BSD 3-Clause License. It includes code adapted from:
- d3-contour (ISC license)
- vt-pbf (MIT license)