Edit

WebGL points layer

webgl9 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 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 resolution
  • Math 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 power
  • Transform 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

main.js
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();
index.html
<!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>
      &nbsp;
    </small>
    <script src="main.js"></script>
  </body>
</html>
package.json
{
  "name": "webgl-points-layer",
  "dependencies": {
    "ol": "6.9.0"
  },
  "devDependencies": {
    "parcel": "^2.0.0-beta.1"
  },
  "scripts": {
    "start": "parcel index.html",
    "build": "parcel build --public-url . index.html"
  }
}