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
import Feature from 'ol/Feature.js';
import ImageTile from 'ol/source/ImageTile.js';
import Map from 'ol/Map.js';
import Point from 'ol/geom/Point.js';
import TileLayer from 'ol/layer/WebGLTile.js';
import VectorSource from 'ol/source/Vector.js';
import View from 'ol/View.js';
import WebGLPointsLayer from 'ol/layer/WebGLPoints.js';
import {fromLonLat} from 'ol/proj.js';
const key = 'Get your own API key at https://www.maptiler.com/cloud/';
const attributions =
'<a href="https://www.maptiler.com/copyright/" target="_blank">© MapTiler</a> ' +
'<a href="https://www.openstreetmap.org/copyright" target="_blank">© OpenStreetMap contributors</a>';
const map = new Map({
layers: [
new TileLayer({
source: new ImageTile({
attributions: attributions,
url:
'https://api.maptiler.com/maps/satellite/{z}/{x}/{y}.jpg?key=' + key,
}),
}),
],
target: document.getElementById('map'),
view: new View({
center: [0, 4000000],
zoom: 2,
}),
});
const oldColor = [255, 160, 110];
const newColor = [180, 255, 200];
const style = {
variables: {
filterShape: 'all',
},
filter: [
'any',
['==', ['var', 'filterShape'], 'all'],
['==', ['var', 'filterShape'], ['get', 'shape']],
],
'icon-src': 'data/ufo_shapes.png',
'icon-width': 128,
'icon-height': 64,
'icon-color': [
'interpolate',
['linear'],
['get', 'year'],
1950,
oldColor,
2013,
newColor,
],
'icon-offset': [
'match',
['get', 'shape'],
'light',
[0, 0],
'sphere',
[32, 0],
'circle',
[32, 0],
'disc',
[64, 0],
'oval',
[64, 0],
'triangle',
[96, 0],
'fireball',
[0, 32],
[96, 32],
],
'icon-size': [32, 32],
'icon-scale': 0.5,
};
const shapeSelect = document.getElementById('shape-filter');
shapeSelect.addEventListener('input', function () {
style.variables.filterShape = shapeSelect.value;
map.render();
});
function fillShapeSelect(shapeTypes) {
Object.keys(shapeTypes)
.sort(function (a, b) {
return shapeTypes[b] - shapeTypes[a];
})
.forEach(function (shape) {
const option = document.createElement('option');
const sightings = shapeTypes[shape];
option.text = `${shape} (${sightings} sighting${
sightings === 1 ? '' : 's'
})`;
option.value = shape;
shapeSelect.appendChild(option);
});
}
const client = new XMLHttpRequest();
client.open('GET', 'data/csv/ufo_sighting_data.csv');
client.addEventListener('load', function () {
const csv = client.responseText;
// key is shape name, value is sightings count
const shapeTypes = {};
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.substring(prevIndex, curIndex).split(',');
prevIndex = curIndex + 1;
const coords = [parseFloat(line[5]), parseFloat(line[4])];
const shape = line[2];
shapeTypes[shape] = (shapeTypes[shape] || 0) + 1;
features.push(
new Feature({
datetime: line[0],
year: parseInt(/[0-9]{4}/.exec(line[0])[0], 10), // extract the year as int
shape: shape,
duration: line[3],
geometry: new Point(fromLonLat(coords)),
}),
);
}
shapeTypes['all'] = features.length;
map.addLayer(
new WebGLPointsLayer({
source: new VectorSource({
features: features,
attributions: 'National UFO Reporting Center',
}),
style: style,
}),
);
fillShapeSelect(shapeTypes);
});
client.send();
const info = document.getElementById('info');
map.on('pointermove', function (evt) {
if (map.getView().getInteracting() || map.getView().getAnimating()) {
return;
}
const text = map.forEachFeatureAtPixel(evt.pixel, function (feature) {
const datetime = feature.get('datetime');
const duration = feature.get('duration');
const shape = feature.get('shape');
return `On ${datetime}, lasted ${duration} seconds and had a "${shape}" shape.`;
});
info.innerText = text || '';
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Icon Sprites with WebGL</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>
<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 type="module" src="main.js"></script>
</body>
</html>
{
"name": "icon-sprite-webgl",
"dependencies": {
"ol": "10.3.0"
},
"devDependencies": {
"vite": "^3.2.3"
},
"scripts": {
"start": "vite",
"build": "vite build"
}
}