-
-
Notifications
You must be signed in to change notification settings - Fork 93
Description
Design Proposal: Contour Line Source from Raster DEM Tiles
Motivation
Give users a built-in way to render contour lines in maplibre from the same DEM tiles that are already used for terrain and hillshading, like this:
Proposed Change
Create a new contour source type in maplibre style spec that takes a raster-dem source as input and generates contour isolines as output that can be styled using line layers, for example:
sources: {
dem: {
type: "raster-dem",
encoding: "terrarium",
tiles: ["https://elevation-tiles-prod.s3.amazonaws.com/terrarium/{z}/{x}/{y}.png"],
maxzoom: 13,
tileSize: 256,
},
contours: {
type: "contour",
source: "dem",
unit: "feet" | "meters" | number, // default=meters, for custom unit use length of the unit like unit: 1.8288 for fathoms
// similar syntax to ["step", ["zoom"], ...] style expression
// to define contour interval by zoom level
intervals: [
200, // 200m interval <=z11
12, 100, // 100m interval at z12 and z13
14, 50, // 50m interval at z14
15, 20 // 20m interval >= z15
},
// put a "major=false/true" tag on every Nth line by zoom so styles
// can highlight major/minor lines differently
majorMultiplier: [
5, // every 5th line at < z14
14, 4, // every 4th line at z14
15, 5 // every 5th line for >= z15
],
// minzoom inferred from raster-dem source and maxzoom determined automatically by maplibre
// overzoom z10 tiles to generate z11 contour lines, z11 to make z12, etc...
overzoom: 1,
},
},The generated isolines will have these attributes:
eleelevation above sea level in the unit specifiedintervalthe fixed interval between isolines at this zoom level in the unit specifiedmajortrue if this is a major isoline based on majorMultiplier at this zoom level, false otherwise
Layers can refer to the contours with source: contours but they can omit sourceLayer.
This offloads details about how to retrieve and parse DEM tiles to the DEM source definition, and gives style layers the flexibility to render any number of visible lines derived from that contour source.
I've already prototyped this in the maplibre-contour plugin which I'm using for contour lines on onthegomap.com. Here are some of the issues I had to work through to get these contours to look nice:
DEM "overzooming" (smoothing)
The contour lines look blocky when you zoom in much further past the maximum zoom for a DEM source, but they can look nice and smooth if you "overzoom" the DEM tiles by applying iterative bilinear interpolation before generating isolines. For example for onthegomap I use 512px z11 tiles, but overzoom the z11 tiles up to z15 so that the contour lines look smooth even at high zooms. This is why the proposal lets you specify a maxzoom that is higher than the maxzoom of the raster-dem source.
Also to generate smooth contour lines at the border between tiles, the algorithm needs to look at adjacent tiles. This means you need 9 DEM tiles to render a single contour tile. To mitigate this, the overzoom parameter lets you use overzoomed DEM tiles from a lower zoom level to generate contours at the current zoom level, for example overzoom=1 means use the top-left, top-right, botom-left, or bottom-right z10 tile to render a z11 contour line tile. This means you only need 4 DEM tiles to render a single contour tile:
Contour levels and units
The user needs to be able to choose what elevations to draw contour lines at, which changes by zoom level (rendering every contour would get too expensive at low zooms in hilly areas). They may also designate "major" and "minor" levels, for example generate thin contour lines every 200m but bold every 1000m. For now we will push this to layers that use the style, but in the future we can either add a major/minor designation to ticks, or pass-through the level and interval so styles can highlight every 5th or 10th line or something.
The unit attribute multiplies raw meter values by a certain amount to change the unit, for example unit=feet changes from meters to feet. When you click the distance indicator on onthegomap, it toggles between unit=meters and unit=feet. You could also set unit to a custom value for less common units like unit=1.8288 for fathoms.
Performance and Bundle Size
I've already implemented the smoothing logic and isoline generation in the maplibre-contour plugin so we would just need to bring that into maplibre-gl-js and port into the native projects. The overall plugin is 33kb (11kb gzipped) but most of that is replicating the web worker communication, cancelable message passing, and vector tile encoding that maplibre-gl-js already has. The actual smoothing+isoline business logic is only 3.7kb (1.6kb gzipped).
The isoline generation algorithm was derived from d3-contour but is much more efficient because it generates isolines in a single pass through the DEM tile instead of using a pass per contour level. For onthegomap users on a range of devices (mostly mobile phones) overzooming a 512px dem tile and generating isolines takes:
- <10ms 40% of the time
- >50ms 10% of the time
- >100ms 2% of the time
- >200ms 0.5% of the time
- >500ms 0.05% of the time
- >1s 0.006% of the time
API Modifications
This should only change the style spec, but shouldn't require any js or native API changes, unless we wanted to expose the default contour layer, elevation or level key as constants?
Migration Plan and Compatibility
This is new functionality, so no migration is necessary.
Rejected Alternatives
Build a plugin for this
I maintain the maplibre-contour plugin which already lets you do this by using the addProtocol integration, but it has a few downsides:
- It's an extra step to install: rendering contour lines is a common use-case that users should be able to do by default
- 90% of the plugin is duplicating things that maplibre already does like spawning a web worker, communicating with it using cancelable messages, and decoding DEM tiles. The actual code for computing the contours is a small fraction of the overall plugin.
- It has to do a wasteful extra step of encoding the result as vector tile bytes only for maplibre to decode immediately after in its own web workers (see https://github.com/onthegomap/maplibre-contour/blob/main/architecture.png)
- It can't make use of other registered maplibre request interceptors or protocols like DEM tiles served out of a pmtiles archive
- It doesn't work in maplibre-native
Pre-render contour lines
You can render contour line vector tiles ahead of time and serve those for the planet, this will save some browser CPU cycles but rendering them on the fly from DEM tiles has a few advantages:
- 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 terrain and hillshade.
- You're likely already downloading DEM tiles for hillshading and terrain, so this eliminates the extra bandwidth used to download those vector tiles.
Implement as a new layer type
We could implement this as a new layer type instead of a source type, but that would tightly couple the display parameters to the logic for how contour lines are generated, and potentially require us to generate the contours in multiple passes over the source DEM data. It seems cleaner to generate contour lines so you can generate as many layers as you want from them afterwards.
Take a DEM tile source URL as input
A contour layer could take as input tiles: ["server.com/{z}/{x}/{y}.png"], but there are a lot of different knobs to tune for how these are interpreted, so by depending on a DEM source we re-use the DEM source control all of those parameters.
