Rewrite to filter-points-webgl example to use a Webgl points layer

The demonstrated features have been recreated using a literal
style (filtering, pulse animation).
This commit is contained in:
Olivier Guyot
2019-10-22 22:45:12 +02:00
parent d837166a1b
commit 948003ff27
2 changed files with 81 additions and 136 deletions

View File

@@ -3,14 +3,14 @@ layout: example.html
title: Filtering features with WebGL title: Filtering features with WebGL
shortdesc: Using WebGL to filter large quantities of features shortdesc: Using WebGL to filter large quantities of features
docs: > docs: >
This example shows how to use `ol/renderer/webgl/PointsLayer` to dynamically filter a large amount This example shows how to use `ol/layer/WebGLPoints` with a literal style to dynamically filter a large amount
of point geometries. The above map is based on a dataset from the NASA containing 45k recorded meteorite of point geometries. The above map is based on a dataset from the NASA containing 45k recorded meteorite
landing sites. Each meteorite is marked by a circle on the map (the bigger the circle, the heavier landing sites. Each meteorite is marked by a circle on the map (the bigger the circle, the heavier
the object). A pulse effect has been added, which is slightly offset by the year of the impact. the object). A pulse effect has been added, which is slightly offset by the year of the impact.
Adjusting the sliders causes the objects outside of the date range to be filtered out of the map. This is done using Adjusting the sliders causes the objects outside of the date range to be filtered out of the map. This is done
a custom fragment shader on the layer renderer, and by using the `v_opacity` attribute of the rendered objects by mutating the variables in the `style` object provided to the WebGL layer. Also note that the last snippet
to store the year of impact. of code is necessary to make sure the map refreshes itself every frame.
tags: "webgl, icon, sprite, filter, feature" tags: "webgl, icon, sprite, filter, feature"
experimental: true experimental: true

View File

@@ -3,162 +3,106 @@ import View from '../src/ol/View.js';
import TileLayer from '../src/ol/layer/Tile.js'; import TileLayer from '../src/ol/layer/Tile.js';
import Feature from '../src/ol/Feature.js'; import Feature from '../src/ol/Feature.js';
import Point from '../src/ol/geom/Point.js'; import Point from '../src/ol/geom/Point.js';
import VectorLayer from '../src/ol/layer/Vector.js';
import {Vector} from '../src/ol/source.js'; import {Vector} from '../src/ol/source.js';
import {fromLonLat} from '../src/ol/proj.js'; import {fromLonLat} from '../src/ol/proj.js';
import WebGLPointsLayerRenderer from '../src/ol/renderer/webgl/PointsLayer.js';
import {clamp} from '../src/ol/math.js';
import Stamen from '../src/ol/source/Stamen.js'; import Stamen from '../src/ol/source/Stamen.js';
import {formatColor} from '../src/ol/webgl/ShaderBuilder.js'; import WebGLPointsLayer from '../src/ol/layer/WebGLPoints.js';
const vectorSource = new Vector({ const vectorSource = new Vector({
attributions: 'NASA' attributions: 'NASA'
}); });
const oldColor = [180, 140, 140]; const oldColor = 'rgba(242,56,22,0.61)';
const newColor = [255, 80, 80]; const newColor = '#ffe52c';
const period = 12; // animation period in seconds
const animRatio =
['pow',
['/',
['mod',
['+',
['time'],
['stretch', ['get', 'year'], 1850, 2020, 0, period]
],
period
],
period
],
0.5
];
const startTime = Date.now() * 0.001; const style = {
variables: {
minYear: 1850,
maxYear: 2015
},
filter: ['between', ['get', 'year'], ['var', 'minYear'], ['var', 'maxYear']],
symbol: {
symbolType: 'circle',
size: ['*',
['stretch', ['get', 'mass'], 0, 200000, 8, 26],
['-', 1.5, ['*', animRatio, 0.5]]
],
color: ['interpolate',
animRatio,
newColor, oldColor],
opacity: ['-', 1.0, ['*', animRatio, 0.75]]
}
};
// hanle input values & events // handle input values & events
const minYearInput = document.getElementById('min-year'); const minYearInput = document.getElementById('min-year');
const maxYearInput = document.getElementById('max-year'); const maxYearInput = document.getElementById('max-year');
function updateMinYear() {
style.variables.minYear = parseInt(minYearInput.value);
updateStatusText();
}
function updateMaxYear() {
style.variables.maxYear = parseInt(maxYearInput.value);
updateStatusText();
}
function updateStatusText() { function updateStatusText() {
const div = document.getElementById('status'); const div = document.getElementById('status');
div.querySelector('span.min-year').textContent = minYearInput.value; div.querySelector('span.min-year').textContent = minYearInput.value;
div.querySelector('span.max-year').textContent = maxYearInput.value; div.querySelector('span.max-year').textContent = maxYearInput.value;
} }
minYearInput.addEventListener('input', updateStatusText);
minYearInput.addEventListener('change', updateStatusText); minYearInput.addEventListener('input', updateMinYear);
maxYearInput.addEventListener('input', updateStatusText); minYearInput.addEventListener('change', updateMinYear);
maxYearInput.addEventListener('change', updateStatusText); maxYearInput.addEventListener('input', updateMaxYear);
maxYearInput.addEventListener('change', updateMaxYear);
updateStatusText(); updateStatusText();
class WebglPointsLayer extends VectorLayer { // load data
createRenderer() { const client = new XMLHttpRequest();
return new WebGLPointsLayerRenderer(this, { client.open('GET', 'data/csv/meteorite_landings.csv');
attributes: [ client.onload = function() {
{ const csv = client.responseText;
name: 'size', const features = [];
callback: function(feature) {
return 18 * clamp(feature.get('mass') / 200000, 0, 1) + 8;
}
},
{
name: 'year',
callback: function(feature) {
return feature.get('year');
}
}
],
vertexShader: [
'precision mediump float;',
'uniform mat4 u_projectionMatrix;', let prevIndex = csv.indexOf('\n') + 1; // scan past the header line
'uniform mat4 u_offsetScaleMatrix;',
'uniform mat4 u_offsetRotateMatrix;',
'attribute vec2 a_position;',
'attribute float a_index;',
'attribute float a_size;',
'attribute float a_year;',
'varying vec2 v_texCoord;',
'varying float v_year;',
'void main(void) {', let curIndex;
' mat4 offsetMatrix = u_offsetScaleMatrix;', while ((curIndex = csv.indexOf('\n', prevIndex)) != -1) {
' float offsetX = a_index == 0.0 || a_index == 3.0 ? -a_size / 2.0 : a_size / 2.0;', const line = csv.substr(prevIndex, curIndex - prevIndex).split(',');
' float offsetY = a_index == 0.0 || a_index == 1.0 ? -a_size / 2.0 : a_size / 2.0;', prevIndex = curIndex + 1;
' vec4 offsets = offsetMatrix * vec4(offsetX, offsetY, 0.0, 0.0);',
' gl_Position = u_projectionMatrix * vec4(a_position, 0.0, 1.0) + offsets;',
' float u = a_index == 0.0 || a_index == 3.0 ? 0.0 : 1.0;',
' float v = a_index == 0.0 || a_index == 1.0 ? 0.0 : 1.0;',
' v_texCoord = vec2(u, v);',
' v_year = a_year;',
'}'
].join(' '),
fragmentShader: [
'precision mediump float;',
'uniform float u_time;', const coords = fromLonLat([parseFloat(line[4]), parseFloat(line[3])]);
'uniform float u_minYear;', if (isNaN(coords[0]) || isNaN(coords[1])) {
'uniform float u_maxYear;', // guard against bad data
'varying vec2 v_texCoord;', continue;
'varying float v_year;',
'void main(void) {',
// filter out pixels if the year is outside of the given range
' if (v_year < u_minYear || v_year > u_maxYear) {',
' discard;',
' }',
' vec2 texCoord = v_texCoord * 2.0 - vec2(1.0, 1.0);',
' float sqRadius = texCoord.x * texCoord.x + texCoord.y * texCoord.y;',
' float value = 2.0 * (1.0 - sqRadius);',
' float alpha = smoothstep(0.0, 1.0, value);',
// color is interpolated based on year
' float ratio = clamp((v_year - 1800.0) / (2013.0 - 1800.0), 0.0, 1.1);',
' vec3 color = mix(vec3(' + formatColor(oldColor) + '),',
' vec3(' + formatColor(newColor) + '), ratio);',
' float period = 8.0;',
' color.g *= 2.0 * (1.0 - sqrt(mod(u_time + v_year * 0.025, period) / period));',
' gl_FragColor = vec4(color, 1.0);',
' gl_FragColor.a *= alpha;',
' gl_FragColor.rgb *= gl_FragColor.a;',
'}'
].join(' '),
uniforms: {
u_time: function() {
return Date.now() * 0.001 - startTime;
},
u_minYear: function() {
return parseInt(minYearInput.value);
},
u_maxYear: function() {
return parseInt(maxYearInput.value);
}
}
});
}
}
function loadData() {
const client = new XMLHttpRequest();
client.open('GET', 'data/csv/meteorite_landings.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[4]), parseFloat(line[3])]);
if (isNaN(coords[0]) || isNaN(coords[1])) {
// guard against bad data
continue;
}
features.push(new Feature({
mass: parseFloat(line[1]) || 0,
year: parseInt(line[2]) || 0,
geometry: new Point(coords)
}));
} }
vectorSource.addFeatures(features); features.push(new Feature({
}; mass: parseFloat(line[1]) || 0,
client.send(); year: parseInt(line[2]) || 0,
} geometry: new Point(coords)
}));
}
loadData(); vectorSource.addFeatures(features);
};
client.send();
const map = new Map({ const map = new Map({
layers: [ layers: [
@@ -167,7 +111,8 @@ const map = new Map({
layer: 'toner' layer: 'toner'
}) })
}), }),
new WebglPointsLayer({ new WebGLPointsLayer({
style: style,
source: vectorSource source: vectorSource
}) })
], ],