Skip to content

Navigation Menu

Sign in
Sign up

Repository files navigation

maplibre-contour

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

Live example | Code

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,
});

Import it to a maplibre worker

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"],
 });
});

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 is licensed under the BSD 3-Clause License. It includes code adapted from:

About

Render contour lines from raster DEM tiles in maplibre-gl-js

Resources

Stars

285 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

AltStyle によって変換されたページ (->オリジナル) /