Edit

WebGL points layer

webgl17 point2 layer4 feature12

Choose a predefined style from the list below or edit it as JSON manually.  

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.

main.js
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-radius1': 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();
index.html
<!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>
      &nbsp;
    </small>

    <script type="module" src="main.js"></script>
  </body>
</html>
package.json
{
  "name": "webgl-points-layer",
  "dependencies": {
    "ol": "8.2.0"
  },
  "devDependencies": {
    "vite": "^3.2.3"
  },
  "scripts": {
    "start": "vite",
    "build": "vite build"
  }
}