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 representation.
Consult the documentation on expressions to know which operators to use and how.
import GeoJSON from 'ol/format/GeoJSON.js';
import Map from 'ol/Map.js';
import OSM from 'ol/source/OSM.js';
import TileLayer from 'ol/layer/Tile.js';
import Vector from 'ol/source/Vector.js';
import View from 'ol/View.js';
import WebGLPointsLayer from 'ol/layer/WebGLPoints.js';
const vectorSource = new Vector({
url: 'data/geojson/world-cities.geojson',
format: new GeoJSON(),
wrapX: true,
});
const predefinedStyles = {
icons: {
'icon-src': 'data/icon.png',
'icon-width': 18,
'icon-height': 28,
'icon-color': 'lightyellow',
'icon-rotate-with-view': false,
'icon-displacement': [0, 9],
},
triangles: {
'shape-points': 3,
'shape-radius': 9,
'shape-fill-color': [
'interpolate',
['linear'],
['get', 'population'],
20000,
'#5aca5b',
300000,
'#ff6a19',
],
'shape-rotate-with-view': true,
},
'triangles-latitude': {
'shape-points': 3,
'shape-radius': [
'interpolate',
['linear'],
['get', 'population'],
40000,
6,
2000000,
12,
],
'shape-fill-color': [
'interpolate',
['linear'],
['get', 'latitude'],
-60,
'#ff14c3',
-20,
'#ff621d',
20,
'#ffed02',
60,
'#00ff67',
],
'shape-opacity': 0.95,
},
circles: {
'circle-radius': [
'interpolate',
['linear'],
['get', 'population'],
40000,
4,
2000000,
14,
],
'circle-fill-color': ['match', ['get', 'hover'], 1, '#ff3f3f', '#006688'],
'circle-rotate-with-view': false,
'circle-displacement': [0, 0],
'circle-opacity': [
'interpolate',
['linear'],
['get', 'population'],
40000,
0.6,
2000000,
0.92,
],
},
'circles-zoom': {
// by using an exponential interpolation with a base of 2 we can make it so that circles will have a fixed size
// in world coordinates between zoom level 5 and 15
'circle-radius': [
'interpolate',
['exponential', 2],
['zoom'],
5,
1.5,
15,
1.5 * Math.pow(2, 10),
],
'circle-fill-color': ['match', ['get', 'hover'], 1, '#ff3f3f', '#006688'],
'circle-displacement': [0, 0],
'circle-opacity': 0.95,
},
'rotating-bars': {
'shape-rotation': ['*', ['time'], 0.13],
'shape-points': 4,
'shape-radius': 4,
'shape-radius2': 4 * Math.sqrt(2),
'shape-scale': [
'array',
1,
['interpolate', ['linear'], ['get', 'population'], 20000, 1, 300000, 7],
],
'shape-fill-color': [
'interpolate',
['linear'],
['get', 'population'],
20000,
'#ffdc00',
300000,
'#ff5b19',
],
'shape-displacement': [
'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;
let selected = null;
map.on('pointermove', function (ev) {
if (selected !== null) {
selected.set('hover', 0);
selected = null;
}
map.forEachFeatureAtPixel(ev.pixel, function (feature) {
feature.set('hover', 1);
selected = feature;
return true;
});
});
function refreshLayer(newStyle) {
const previousLayer = pointsLayer;
pointsLayer = new WebGLPointsLayer({
source: vectorSource,
style: newStyle,
});
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>
<link rel="stylesheet" href="node_modules/ol/ol.css">
<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 type="module" src="main.js"></script>
</body>
</html>
{
"name": "webgl-points-layer",
"dependencies": {
"ol": "10.3.1"
},
"devDependencies": {
"vite": "^3.2.3"
},
"scripts": {
"start": "vite",
"build": "vite build"
}
}