contourmap2.mp4
This MaplibreGL JS plugin allows to generate a real-time, client-side, contour map (isolines, isopleths, whatever you wanna call it 😅) based on the data provided by a scattered points vector (tiled or geoJSON) layer.
One of the main advantages of the approeach used here is that source points don't need to be regularly placed in a grid, as we are using the meandering triangles variation of the marching squares algorithm, that allows us to use scattered points and work on a TIN defined by those points
You can play with the example in the video above at
https://abelvm.github.io/maplibre-contourmap/example/
As the points are arbitrarily scattered, we can't foresee the placement of the points in the tiles. That tiny detail forces us to process the whole available data at once every time the user moves around the map, and refrains us from using custom protocols to process data on the fly, tile by tile, before rendering. So we need to trigger the generation of the contour map once new data is loaded for the linked points layer.
To improve the performance and avoid UI jerkyness, we've used two common techniques here:
- Webworkers, so everything is computed out of the main thread
- Memoization. Due to the nature of the algorithm itself and the need to regenerate the whole contour map as new data is gathered, there is a lot of cells and segments that would be processed multiple times, wasting CPU time. So we memoize both processes (cells and segments) within the worker itself, so it doesn't leak. One of the main potential caveats of memoization is the chance of a huge impact on consumed memory, but the approach used here keeps it under reasonable limits
First of all
npm install
npm run build
Now you have the everything you need in the ./dist
folder.
In order to add this plugin to your Maplibre app, you need to import the module and initialize it, passing the mapping library object (usually, maplibregl
)
import init from 'maplibre-contourmap.js';
init(maplibregl);
Now we have two new maplibregl.Map
methods, that needs the name of the points layer to be targeted:
addContourSource
(input_layer_name, options_object): once added, a new geoJSON multilinestring source is added to the map, calledcontour-source-input_layer_name
removeContourSource
(input_layer_name) : to remove the contour map source
The options_object
contain:
measure
: the name of the numerical property of input_layer_name to be used to generate the contour mapbreaks
: array of numeric values that will define the classes of the isolinesfilter
[optional, defaults to empty]: layer filter according to specs, to limit the points of the source layer that will be used for the contour mapdebug
[optional, defaults to false]: if sets totrue
some extra validations are run and debug messages will be sent to console log
Options yet to be implemented
type
[optional, defaults to "MultiLineString"]: Geometry type of the output. As of today, onlyMultiLineString
is supportedmax_workers
[optional, defaults to((!!navigator.hardwareConcurrency) ? navigator.hardwareConcurrency - 1 : 3) - maplibregl.getWorkerCount()
]: size of the workers pool. As of today, there is no pool management, just one lonely worker
Once the map is loaded, and the target points layer is added, we can attach to the points layer to generate its contour map
map.on('load', () => {
// Raw points source
map.addSource('points_source', {
type: 'vector',
...
});
// Raw points layer
map.addLayer({
'id': 'points_layer',
'type': 'circle',
'source': 'points_source',
'source-layer': 'points_source_layer',
..
});
// Contour map source,
// linked to the former points layer and its COTAS property
map.addContourSource('points_layer', {
'measure': 'my_measure',
'breaks': breaks
});
// Line layer the for contour map
map.addLayer({
'id': 'isolines',
'type': 'line',
'source': 'contour-source-points_layer',
...
});
});
The original points layer must be visible, otherwise its source data won't be loaded. If you want to hide the points layer, use paint property opacity
instead of layout property visible
The only external dependency is @turf/tin, that is side-loaded and embedded within the worker in development time.
The whole plugin is self-contained in a single 8.6kB file
- If the source is GeoJSON, add the option to process the whole data just once
- Verify geometry are points and get centroids if not
- dynamic worker pool
- separete workers for cells and segments
- corner cases in tiles' boundaries when panning