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:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user