maplibre-contour-pmtiles
maplibre-contour-pmtiles is a plugin to render contour lines in MapLibre GL JS from raster-dem
pmtiles sources. It was forked to support pmtiles with http support on the web and http and local supoort in node.js.
![Topographic map of Mount Washington](https://github.com/onthegomap/maplibre-contour/raw/HEAD/demo.png)
Live example #1 (Just Terrain) | Code
Live example #2 (w/ OpenMapTiles Style) | Code | Style
To use it, import the @acalcutt/maplibre-contour-pmtiles package with a script tag:
<script src="https://unpkg.com/@acalcutt/maplibre-contour-pmtiles@latest/dist/maplibre-contour-pmtiles.min.js"></script>
Or as an ES6 module: npm add @acalcutt/maplibre-contour-pmtiles
import mlcontour from "@acalcutt/maplibre-contour-pmtiles";
Then to use, first create a DemSource
and register it with maplibre:
var demSource = new mlcontour.DemSource({
url: "pmtiles://https://url/of/dem/source.pmtiles",
encoding: "mapbox",
maxzoom: 12,
worker: true,
cacheSize: 100,
timeoutMs: 10_000,
});
demSource.setupMaplibre(maplibregl);
When using scripts locally with node, url can also use the format
url: "pmtiles:///local/path/to/pmtiles/source.pmtiles",
url: "pmtiles://C://local//path//to//pmtiles//source.pmtiles",
You should also be able to use a regular non-pmtiles source with
url: "https://url/of/dem/source/{z}/{x}/{y}.png",
Then configure a new contour source and add it to your map:
map.addSource("contour-source", {
type: "vector",
tiles: [
demSource.contourProtocolUrl({
multiplier: 3.28084,
thresholds: {
11: [200, 1000],
12: [100, 500],
14: [50, 200],
15: [20, 100],
},
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%)",
"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,
});
How it works
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:
DemManager
fetches (and caches) the raster-dem image tile and its neighbors so that contours are continuous across tile boundaries.
- When
DemSource
is configured with worker: true
, it uses RemoteDemManager
to spawn worker.ts
in a web worker. The web worker runs LocalDemManager
locally and uses the Actor
utility to send cancelable requests and responses between the main and worker thread.
decode-image.ts
decodes the raster-dem image RGB values to meters above sea level for each pixel in the tile.HeightTile
stitches 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.ts
generates contour isolines from a HeightTile
using a marching-squares implementation derived from d3-contour.vtpbf.ts
encodes 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.
Why?
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.
License
maplibre-contour-pmtiles is licensed under the BSD 3-Clause License. It includes code adapted from: