Using a WebGL-optimized layer to render a large quantities of points
This example shows how to use a WebGLPointsLayer
to show a large amount of points on the map. The layer is given a style in JSON format which allows a certain level of customization of the final reprensentation.
The following operators can be used:
Reading operators:
['get', 'attributeName']
fetches a feature attribute (it will be prefixed by a_
in the shader)
Note: those will be taken from the attributes provided to the renderer['var', 'varName']
fetches a value from the style variables, or 0 if undefined['time']
returns the time in seconds since the creation of the layer['zoom']
returns the current zoom level['resolution']
returns the current resolutionMath operators:
['*', value1, value2]
multiplies value1
by value2
['/', value1, value2]
divides value1
by value2
['+', value1, value2]
adds value1
and value2
['-', value1, value2]
subtracts value2
from value1
['clamp', value, low, high]
clamps value
between low
and high
['%', value1, value2]
returns the result of value1 % value2
(modulo)['^', value1, value2]
returns the value of value1
raised to the value2
powerTransform operators:
['case', condition1, output1, ...conditionN, outputN, fallback]
selects the first output whose corresponding
condition evaluates to true
. If no match is found, returns the fallback
value.
All conditions should be boolean
, output and fallback can be any kind.['match', input, match1, output1, ...matchN, outputN, fallback]
compares the input
value against all
provided matchX
values, returning the output associated with the first valid match. If no match is found,
returns the fallback
value.
input
and matchX
values must all be of the same type, and can be number
or string
. outputX
and
fallback
values must be of the same type, and can be of any kind.['interpolate', interpolation, input, stop1, output1, ...stopN, outputN]
returns a value by interpolating between
pairs of inputs and outputs; interpolation
can either be ['linear']
or ['exponential', base]
where base
is
the rate of increase from stop A to stop B (i.e. power to which the interpolation ratio is raised); a value
of 1 is equivalent to ['linear']
.
input
and stopX
values must all be of type number
. outputX
values can be number
or color
values.
Note: input
will be clamped between stop1
and stopN
, meaning that all output values will be comprised
between output1
and outputN
.Logical operators:
['<', value1, value2]
returns true
if value1
is strictly lower than value2
, or false
otherwise.['<=', value1, value2]
returns true
if value1
is lower than or equals value2
, or false
otherwise.['>', value1, value2]
returns true
if value1
is strictly greater than value2
, or false
otherwise.['>=', value1, value2]
returns true
if value1
is greater than or equals value2
, or false
otherwise.['==', value1, value2]
returns true
if value1
equals value2
, or false
otherwise.['!=', value1, value2]
returns true
if value1
does not equal value2
, or false
otherwise.['!', value1]
returns false
if value1
is true
or greater than 0
, or true
otherwise.['between', value1, value2, value3]
returns true
if value1
is contained between value2
and value3
(inclusively), or false
otherwise.Conversion operators:
['array', value1, ...valueN]
creates a numerical array from number
values; please note that the amount of
values can currently only be 2, 3 or 4.['color', red, green, blue, alpha]
creates a color
value from number
values; the alpha
parameter is
optional; if not specified, it will be set to 1.
Note: red
, green
and blue
components must be values between 0 and 255; alpha
between 0 and 1.
Values can either be literals or another operator, as they will be evaluated recursively.
Literal values can be of the following types:boolean
number
string
import 'ol/ol.css';
import GeoJSON from 'ol/format/GeoJSON';
import Map from 'ol/Map';
import OSM from 'ol/source/OSM';
import TileLayer from 'ol/layer/Tile';
import Vector from 'ol/source/Vector';
import View from 'ol/View';
import WebGLPointsLayer from 'ol/layer/WebGLPoints';
const vectorSource = new Vector({
url: 'data/geojson/world-cities.geojson',
format: new GeoJSON(),
});
const predefinedStyles = {
'icons': {
symbol: {
symbolType: 'image',
src: 'data/icon.png',
size: [18, 28],
color: 'lightyellow',
rotateWithView: false,
offset: [0, 9],
},
},
'triangles': {
symbol: {
symbolType: 'triangle',
size: 18,
color: [
'interpolate',
['linear'],
['get', 'population'],
20000,
'#5aca5b',
300000,
'#ff6a19',
],
rotateWithView: true,
},
},
'triangles-latitude': {
symbol: {
symbolType: 'triangle',
size: [
'interpolate',
['linear'],
['get', 'population'],
40000,
12,
2000000,
24,
],
color: [
'interpolate',
['linear'],
['get', 'latitude'],
-60,
'#ff14c3',
-20,
'#ff621d',
20,
'#ffed02',
60,
'#00ff67',
],
offset: [0, 0],
opacity: 0.95,
},
},
'circles': {
symbol: {
symbolType: 'circle',
size: [
'interpolate',
['linear'],
['get', 'population'],
40000,
8,
2000000,
28,
],
color: '#006688',
rotateWithView: false,
offset: [0, 0],
opacity: [
'interpolate',
['linear'],
['get', 'population'],
40000,
0.6,
2000000,
0.92,
],
},
},
'circles-zoom': {
symbol: {
symbolType: 'circle',
size: ['interpolate', ['exponential', 2.5], ['zoom'], 2, 1, 14, 32],
color: '#240572',
offset: [0, 0],
opacity: 0.95,
},
},
'rotating-bars': {
symbol: {
symbolType: 'square',
rotation: ['*', ['time'], 0.1],
size: [
'array',
4,
[
'interpolate',
['linear'],
['get', 'population'],
20000,
4,
300000,
28,
],
],
color: [
'interpolate',
['linear'],
['get', 'population'],
20000,
'#ffdc00',
300000,
'#ff5b19',
],
offset: [
'array',
0,
[
'interpolate',
['linear'],
['get', 'population'],
20000,
2,
300000,
14,
],
],
},
},
};
const map = new Map({
layers: [
new TileLayer({
source: new OSM(),
}),
],
target: document.getElementById('map'),
view: new View({
center: [0, 0],
zoom: 2,
}),
});
let literalStyle;
let pointsLayer;
function refreshLayer(newStyle) {
const previousLayer = pointsLayer;
pointsLayer = new WebGLPointsLayer({
source: vectorSource,
style: newStyle,
disableHitDetection: true,
});
map.addLayer(pointsLayer);
if (previousLayer) {
map.removeLayer(previousLayer);
previousLayer.dispose();
}
literalStyle = newStyle;
}
const spanValid = document.getElementById('style-valid');
const spanInvalid = document.getElementById('style-invalid');
function setStyleStatus(errorMsg) {
const isError = typeof errorMsg === 'string';
spanValid.style.display = errorMsg === null ? 'initial' : 'none';
spanInvalid.firstElementChild.innerText = isError ? errorMsg : '';
spanInvalid.style.display = isError ? 'initial' : 'none';
}
const editor = document.getElementById('style-editor');
editor.addEventListener('input', function () {
const textStyle = editor.value;
try {
const newLiteralStyle = JSON.parse(textStyle);
if (JSON.stringify(newLiteralStyle) !== JSON.stringify(literalStyle)) {
refreshLayer(newLiteralStyle);
}
setStyleStatus(null);
} catch (e) {
setStyleStatus(e.message);
}
});
const select = document.getElementById('style-select');
select.value = 'circles';
function onSelectChange() {
const style = select.value;
const newLiteralStyle = predefinedStyles[style];
editor.value = JSON.stringify(newLiteralStyle, null, 2);
try {
refreshLayer(newLiteralStyle);
setStyleStatus();
} catch (e) {
setStyleStatus(e.message);
}
}
onSelectChange();
select.addEventListener('change', onSelectChange);
// animate the map
function animate() {
map.render();
window.requestAnimationFrame(animate);
}
animate();
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebGL points layer</title>
<!-- Pointer events polyfill for old browsers, see https://caniuse.com/#feat=pointer -->
<script src="https://unpkg.com/elm-pep"></script>
<!-- The line below is only needed for old environments like Internet Explorer and Android 4.x -->
<script src="https://cdn.polyfill.io/v3/polyfill.min.js?features=fetch,requestAnimationFrame,Element.prototype.classList,URL,TextDecoder,Number.isInteger"></script>
<style>
.map {
width: 100%;
height:400px;
}
</style>
</head>
<body>
<div id="map" class="map"></div>
Choose a predefined style from the list below or edit it as JSON manually.
<select id="style-select">
<option value="icons">Icons</option>
<option value="triangles">Triangles, color related to population</option>
<option value="triangles-latitude">Triangles, color related to latitude</option>
<option value="circles">Circles, size related to population</option>
<option value="circles-zoom">Circles, size related to zoom</option>
<option value="rotating-bars">Rotating bars</option>
</select>
<textarea style="width: 100%; height: 20rem; font-family: monospace; font-size: small;" id="style-editor"></textarea>
<small>
<span id="style-valid" style="display: none; color: forestgreen">✓ style is valid</span>
<span id="style-invalid" style="display: none; color: grey">✗ <span>style not yet valid...</span></span>
</small>
<script src="main.js"></script>
</body>
</html>
{
"name": "webgl-points-layer",
"dependencies": {
"ol": "6.6.1"
},
"devDependencies": {
"parcel": "^2.0.0-beta.1"
},
"scripts": {
"start": "parcel index.html",
"build": "parcel build --public-url . index.html"
}
}