Edit

Icon Sprites with WebGL

webgl9 icon8 sprite2 point2 ufo1

Current sighting:

Rendering many icons with WebGL

This example shows how to use ol/layer/WebGLPoints to render a very large amount of sprites. The above map is based on a dataset from the National UFO Reporting Center: each icon marks a UFO sighting according to its reported shape (disk, light, fireball...). The older the sighting, the redder the icon.

A very simple sprite atlas is used in the form of a PNG file containing all icons on a grid. Then, the style object given to the ol/layer/WebGLPoints constructor is used to specify which sprite to use according to the sighting shape.

The dataset contains around 80k points and can be found here: https://www.kaggle.com/NUFORC/ufo-sightings

main.js
import 'ol/ol.css';
import Feature from 'ol/Feature';
import Map from 'ol/Map';
import Point from 'ol/geom/Point';
import TileLayer from 'ol/layer/Tile';
import View from 'ol/View';
import WebGLPointsLayer from 'ol/layer/WebGLPoints';
import XYZ from 'ol/source/XYZ';
import {Vector} from 'ol/source';
import {fromLonLat} from 'ol/proj';

const key = 'Get your own API key at https://www.maptiler.com/cloud/';
const attributions =
  '<a href="https://www.maptiler.com/copyright/" target="_blank">&copy; MapTiler</a> ' +
  '<a href="https://www.openstreetmap.org/copyright" target="_blank">&copy; OpenStreetMap contributors</a>';

const map = new Map({
  layers: [
    new TileLayer({
      source: new XYZ({
        attributions: attributions,
        url:
          'https://api.maptiler.com/tiles/satellite/{z}/{x}/{y}.jpg?key=' + key,
        tileSize: 512,
      }),
    }),
  ],
  target: document.getElementById('map'),
  view: new View({
    center: [0, 4000000],
    zoom: 2,
  }),
});

const vectorSource = new Vector({
  features: [],
  attributions: 'National UFO Reporting Center',
});

const oldColor = [255, 160, 110];
const newColor = [180, 255, 200];
const size = 16;

const style = {
  variables: {
    filterShape: 'all',
  },
  filter: [
    'case',
    ['!=', ['var', 'filterShape'], 'all'],
    ['==', ['get', 'shape'], ['var', 'filterShape']],
    true,
  ],
  symbol: {
    symbolType: 'image',
    src: 'data/ufo_shapes.png',
    size: size,
    color: [
      'interpolate',
      ['linear'],
      ['get', 'year'],
      1950,
      oldColor,
      2013,
      newColor,
    ],
    rotateWithView: false,
    offset: [0, 0],
    textureCoord: [
      'match',
      ['get', 'shape'],
      'light',
      [0, 0, 0.25, 0.5],
      'sphere',
      [0.25, 0, 0.5, 0.5],
      'circle',
      [0.25, 0, 0.5, 0.5],
      'disc',
      [0.5, 0, 0.75, 0.5],
      'oval',
      [0.5, 0, 0.75, 0.5],
      'triangle',
      [0.75, 0, 1, 0.5],
      'fireball',
      [0, 0.5, 0.25, 1],
      [0.75, 0.5, 1, 1],
    ],
  },
};

// key is shape name, value is sightings count
const shapeTypes = {
  all: 0,
};
const shapeSelect = document.getElementById('shape-filter');
shapeSelect.addEventListener('input', function () {
  style.variables.filterShape =
    shapeSelect.options[shapeSelect.selectedIndex].value;
  map.render();
});
function fillShapeSelect() {
  Object.keys(shapeTypes)
    .sort(function (a, b) {
      return shapeTypes[b] - shapeTypes[a];
    })
    .forEach(function (shape) {
      const option = document.createElement('option');
      option.text = `${shape} (${shapeTypes[shape]} sightings)`;
      option.value = shape;
      shapeSelect.appendChild(option);
    });
}

const client = new XMLHttpRequest();
client.open('GET', 'data/csv/ufo_sighting_data.csv');
client.onload = function () {
  const csv = client.responseText;
  const features = [];

  let prevIndex = csv.indexOf('\n') + 1; // scan past the header line

  let curIndex;
  while ((curIndex = csv.indexOf('\n', prevIndex)) != -1) {
    const line = csv.substr(prevIndex, curIndex - prevIndex).split(',');
    prevIndex = curIndex + 1;

    const coords = fromLonLat([parseFloat(line[5]), parseFloat(line[4])]);

    // only keep valid points
    if (isNaN(coords[0]) || isNaN(coords[1])) {
      continue;
    }

    const shape = line[2];
    shapeTypes[shape] = (shapeTypes[shape] ? shapeTypes[shape] : 0) + 1;
    shapeTypes['all']++;

    features.push(
      new Feature({
        datetime: line[0],
        year: parseInt(/[0-9]{4}/.exec(line[0])[0]), // extract the year as int
        shape: shape,
        duration: line[3],
        geometry: new Point(coords),
      })
    );
  }
  vectorSource.addFeatures(features);
  fillShapeSelect();
};
client.send();

map.addLayer(
  new WebGLPointsLayer({
    source: vectorSource,
    style: style,
  })
);

const info = document.getElementById('info');
map.on('pointermove', function (evt) {
  if (map.getView().getInteracting() || map.getView().getAnimating()) {
    return;
  }
  const pixel = evt.pixel;
  info.innerText = '';
  map.forEachFeatureAtPixel(pixel, function (feature) {
    const datetime = feature.get('datetime');
    const duration = feature.get('duration');
    const shape = feature.get('shape');
    info.innerText =
      'On ' +
      datetime +
      ', lasted ' +
      duration +
      ' seconds and had a "' +
      shape +
      '" shape.';
  });
});
index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Icon Sprites with WebGL</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>
    <div>Current sighting: <span id="info"></span></div>
    <div>
      <label for="shape-filter">Filter by UFO shape:</label>
      <select id="shape-filter"></select>
    </div>
    <script src="main.js"></script>
  </body>
</html>
package.json
{
  "name": "icon-sprite-webgl",
  "dependencies": {
    "ol": "6.9.0"
  },
  "devDependencies": {
    "parcel": "^2.0.0-beta.1"
  },
  "scripts": {
    "start": "parcel index.html",
    "build": "parcel build --public-url . index.html"
  }
}