Edit

WebGL points layer


 

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 for numerical values:

  • ["get", "attributeName"] fetches a numeric attribute value for each feature
  • ["+", value, value] adds two values (which an either be numeric, or the result of another operator)
  • ["*", value, value] multiplies two values
  • ["clamp", value, min, max] outputs a value between min and max
  • ["stretch", value, low1, high1, low2, high2] outputs a value which has been mapped from the low1..high1 range to the low2..high2 range

index.html<!DOCTYPE html>
<html lang="en">
  <head>
    <title>WebGL points layer</title>
    <!-- The line below is only needed for old environments like Internet Explorer and Android 4.x -->
    <script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=requestAnimationFrame,Element.prototype.classList,URL"></script>
    <style>
      .map {
        width: 100%;
        height:400px;
      }
    </style>
  </head>
  <body>
    <div id="map" class="map"></div>
    <label>Choose a predefined style from the list below or edit it as JSON manually.</label><br>
    <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>
    </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 src="index.js"></script>
  </body>
</html>
index.jsimport 'ol/ol.css';
import Map from 'ol/Map';
import View from 'ol/View';
import TileLayer from 'ol/layer/Tile';
import WebGLPointsLayer from 'ol/layer/WebGLPoints';
import GeoJSON from 'ol/format/GeoJSON';
import Vector from 'ol/source/Vector';
import OSM from 'ol/source/OSM';

var vectorSource = new Vector({
  url: 'data/geojson/world-cities.geojson',
  format: new GeoJSON()
});

var 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
    }
  }
};

var map = new Map({
  layers: [
    new TileLayer({
      source: new OSM()
    })
  ],
  target: document.getElementById('map'),
  view: new View({
    center: [0, 0],
    zoom: 2
  })
});

var literalStyle;
var pointsLayer;
function refreshLayer(newStyle) {
  var previousLayer = pointsLayer;
  pointsLayer = new WebGLPointsLayer({
    source: vectorSource,
    style: newStyle,
    disableHitDetection: true
  });
  map.addLayer(pointsLayer);

  if (previousLayer) {
    map.removeLayer(previousLayer);
    previousLayer.dispose();
  }
  literalStyle = newStyle;
}

var spanValid = document.getElementById('style-valid');
var spanInvalid = document.getElementById('style-invalid');
function setStyleStatus(errorMsg) {
  var isError = typeof errorMsg === 'string';
  spanValid.style.display = errorMsg === null ? 'initial' : 'none';
  spanInvalid.firstElementChild.innerText = isError ? errorMsg : '';
  spanInvalid.style.display = isError ? 'initial' : 'none';
}

var editor = document.getElementById('style-editor');
editor.addEventListener('input', function() {
  var textStyle = editor.value;
  try {
    var newLiteralStyle = JSON.parse(textStyle);
    if (JSON.stringify(newLiteralStyle) !== JSON.stringify(literalStyle)) {
      refreshLayer(newLiteralStyle);
    }
    setStyleStatus(null);
  } catch (e) {
    setStyleStatus(e.message);
  }
});

var select = document.getElementById('style-select');
select.value = 'circles';
function onSelectChange() {
  var style = select.value;
  var newLiteralStyle = predefinedStyles[style];
  editor.value = JSON.stringify(newLiteralStyle, null, 2);
  try {
    refreshLayer(newLiteralStyle);
    setStyleStatus();
  } catch (e) {
    setStyleStatus(e.message);
  }
}
onSelectChange();
select.addEventListener('change', onSelectChange);
package.json{
  "name": "webgl-points-layer",
  "dependencies": {
    "ol": "6.1.1"
  },
  "devDependencies": {
    "parcel": "1.11.0"
  },
  "scripts": {
    "start": "parcel index.html",
    "build": "parcel build --experimental-scope-hoisting --public-url . index.html"
  }
}