Compare commits

..

1 Commits

Author SHA1 Message Date
ahocevar
e0381bfd6a Tag for v6.0.0-beta.4 2019-04-03 15:17:02 +02:00
14 changed files with 36 additions and 46026 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +0,0 @@
---
layout: example.html
title: Filtering features with WebGL
shortdesc: Using WebGL to filter large quantities of features
docs: >
This example shows how to use `ol/renderer/webgl/PointsLayer` to dynamically filter a large amount
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
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
a custom fragment shader on the layer renderer, and by using the `v_opacity` attribute of the rendered objects
to store the year of impact.
tags: "webgl, icon, sprite, filter, feature"
---
<div id="map" class="map"></div>
<form>
<div id="status">Show impacts between <span class="min-year"></span> and <span class="max-year"></span></div>
<label>Minimum year:</label>
<input id="min-year" type="range" min="1850" max="2015" step="1" value="1850"/>
<label>Maximum year:</label>
<input id="max-year" type="range" min="1850" max="2015" step="1" value="2015"/>
</form>

View File

@@ -1,162 +0,0 @@
import Map from '../src/ol/Map.js';
import View from '../src/ol/View.js';
import TileLayer from '../src/ol/layer/Tile.js';
import Feature from '../src/ol/Feature';
import Point from '../src/ol/geom/Point';
import VectorLayer from '../src/ol/layer/Vector';
import {Vector} from '../src/ol/source';
import {fromLonLat} from '../src/ol/proj';
import WebGLPointsLayerRenderer from '../src/ol/renderer/webgl/PointsLayer';
import {clamp, lerp} from '../src/ol/math';
import Stamen from '../src/ol/source/Stamen';
const features = [];
const vectorSource = new Vector({
features: [],
attributions: 'NASA'
});
const oldColor = [180, 140, 140];
const newColor = [255, 80, 80];
const startTime = Date.now() * 0.001;
// hanle input values & events
const minYearInput = document.getElementById('min-year');
const maxYearInput = document.getElementById('max-year');
function updateStatusText() {
const div = document.getElementById('status');
div.querySelector('span.min-year').textContent = minYearInput.value;
div.querySelector('span.max-year').textContent = maxYearInput.value;
}
minYearInput.addEventListener('input', updateStatusText);
minYearInput.addEventListener('change', updateStatusText);
maxYearInput.addEventListener('input', updateStatusText);
maxYearInput.addEventListener('change', updateStatusText);
updateStatusText();
class WebglPointsLayer extends VectorLayer {
createRenderer() {
return new WebGLPointsLayerRenderer(this, {
colorCallback: function(feature, vertex, component) {
// component at index 3 is alpha
if (component === 3) {
return 1;
}
// color is interpolated based on year
const ratio = clamp((feature.get('year') - 1800) / (2013 - 1800), 0, 1);
return lerp(oldColor[component], newColor[component], ratio) / 255;
},
sizeCallback: function(feature) {
return 18 * clamp(feature.get('mass') / 200000, 0, 1) + 8;
},
fragmentShader: [
'precision mediump float;',
'uniform float u_time;',
'uniform float u_minYear;',
'uniform float u_maxYear;',
'varying vec2 v_texCoord;',
'varying float v_opacity;',
'varying vec4 v_color;',
'void main(void) {',
' float impactYear = v_opacity;',
// filter out pixels if the year is outside of the given range
' if (impactYear < u_minYear || v_opacity > 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);',
' vec3 color = v_color.rgb;',
' float period = 8.0;',
' color.g *= 2.0 * (1.0 - sqrt(mod(u_time + impactYear * 0.025, period) / period));',
' gl_FragColor = vec4(color, v_color.a);',
' gl_FragColor.a *= alpha;',
' gl_FragColor.rgb *= gl_FragColor.a;',
'}'
].join(' '),
opacityCallback: function(feature) {
// here the opacity channel of the vertices is used to store the year of impact
return feature.get('year');
},
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;
let curIndex;
let prevIndex = 0;
let line;
while ((curIndex = csv.indexOf('\n', prevIndex)) > 0) {
line = csv.substr(prevIndex, curIndex - prevIndex).split(',');
prevIndex = curIndex + 1;
// skip header
if (prevIndex === 0) {
continue;
}
const coords = fromLonLat([parseFloat(line[4]), parseFloat(line[3])]);
features.push(new Feature({
mass: parseFloat(line[1]) || 0,
year: parseInt(line[2]) || 0,
geometry: new Point(coords)
}));
}
vectorSource.addFeatures(features);
};
client.send();
}
loadData();
const map = new Map({
layers: [
new TileLayer({
source: new Stamen({
layer: 'toner'
})
}),
new WebglPointsLayer({
source: vectorSource
})
],
target: document.getElementById('map'),
view: new View({
center: [0, 0],
zoom: 2
})
});
// animate the map
function animate() {
map.render();
window.requestAnimationFrame(animate);
}
animate();

View File

@@ -1,6 +1,6 @@
{ {
"name": "ol", "name": "ol",
"version": "6.0.0-beta.5", "version": "6.0.0-beta.4",
"description": "OpenLayers mapping library", "description": "OpenLayers mapping library",
"keywords": [ "keywords": [
"map", "map",
@@ -73,7 +73,7 @@
"karma-sourcemap-loader": "^0.3.7", "karma-sourcemap-loader": "^0.3.7",
"karma-webpack": "^4.0.0-rc.2", "karma-webpack": "^4.0.0-rc.2",
"loglevelnext": "^3.0.0", "loglevelnext": "^3.0.0",
"marked": "0.6.2", "marked": "0.6.1",
"mocha": "6.0.2", "mocha": "6.0.2",
"ol-mapbox-style": "^4.2.1", "ol-mapbox-style": "^4.2.1",
"pixelmatch": "^4.0.2", "pixelmatch": "^4.0.2",

View File

@@ -170,9 +170,6 @@ class Tile extends EventTarget {
// cleaned up by refreshInterimChain) // cleaned up by refreshInterimChain)
do { do {
if (tile.getState() == TileState.LOADED) { if (tile.getState() == TileState.LOADED) {
// Show tile immediately instead of fading it in after loading, because
// the interim tile is in place already
this.transition_ = 0;
return tile; return tile;
} }
tile = tile.interimTile; tile = tile.interimTile;

View File

@@ -284,6 +284,8 @@ class View extends BaseObject {
*/ */
this.updateAnimationKey_; this.updateAnimationKey_;
this.updateAnimations_ = this.updateAnimations_.bind(this);
/** /**
* @private * @private
* @const * @const
@@ -450,17 +452,6 @@ class View extends BaseObject {
* @api * @api
*/ */
animate(var_args) { animate(var_args) {
if (this.isDef() && !this.getAnimating()) {
this.resolveConstraints(0);
}
this.animate_.apply(this, arguments);
}
/**
* @private
* @param {...(AnimationOptions|function(boolean): void)} var_args Animation options.
*/
animate_(var_args) {
let animationCount = arguments.length; let animationCount = arguments.length;
let callback; let callback;
if (animationCount > 1 && typeof arguments[animationCount - 1] === 'function') { if (animationCount > 1 && typeof arguments[animationCount - 1] === 'function') {
@@ -650,7 +641,11 @@ class View extends BaseObject {
// prune completed series // prune completed series
this.animations_ = this.animations_.filter(Boolean); this.animations_ = this.animations_.filter(Boolean);
if (more && this.updateAnimationKey_ === undefined) { if (more && this.updateAnimationKey_ === undefined) {
this.updateAnimationKey_ = requestAnimationFrame(this.updateAnimations_.bind(this)); this.updateAnimationKey_ = requestAnimationFrame(this.updateAnimations_);
}
if (!this.getAnimating()) {
setTimeout(this.resolveConstraints.bind(this), 0);
} }
} }
@@ -1090,7 +1085,7 @@ class View extends BaseObject {
const callback = options.callback ? options.callback : VOID; const callback = options.callback ? options.callback : VOID;
if (options.duration !== undefined) { if (options.duration !== undefined) {
this.animate_({ this.animate({
resolution: resolution, resolution: resolution,
center: this.getConstrainedCenter(center, resolution), center: this.getConstrainedCenter(center, resolution),
duration: options.duration, duration: options.duration,
@@ -1317,7 +1312,7 @@ class View extends BaseObject {
this.cancelAnimations(); this.cancelAnimations();
} }
this.animate_({ this.animate({
rotation: newRotation, rotation: newRotation,
center: newCenter, center: newCenter,
resolution: newResolution, resolution: newResolution,
@@ -1330,13 +1325,9 @@ class View extends BaseObject {
/** /**
* Notify the View that an interaction has started. * Notify the View that an interaction has started.
* The view state will be resolved to a stable one if needed
* (depending on its constraints).
* @api * @api
*/ */
beginInteraction() { beginInteraction() {
this.resolveConstraints(0);
this.setHint(ViewHint.INTERACTING, 1); this.setHint(ViewHint.INTERACTING, 1);
} }

View File

@@ -111,7 +111,7 @@ export function pan(view, delta, opt_duration) {
const currentCenter = view.getCenter(); const currentCenter = view.getCenter();
if (currentCenter) { if (currentCenter) {
const center = [currentCenter[0] + delta[0], currentCenter[1] + delta[1]]; const center = [currentCenter[0] + delta[0], currentCenter[1] + delta[1]];
view.animate_({ view.animate({
duration: opt_duration !== undefined ? opt_duration : 250, duration: opt_duration !== undefined ? opt_duration : 250,
easing: linear, easing: linear,
center: view.getConstrainedCenter(center) center: view.getConstrainedCenter(center)

View File

@@ -238,7 +238,7 @@ class Heatmap extends VectorLayer {
float sqRadius = texCoord.x * texCoord.x + texCoord.y * texCoord.y; float sqRadius = texCoord.x * texCoord.x + texCoord.y * texCoord.y;
float value = (1.0 - sqrt(sqRadius)) * u_blurSlope; float value = (1.0 - sqrt(sqRadius)) * u_blurSlope;
float alpha = smoothstep(0.0, 1.0, value) * v_opacity; float alpha = smoothstep(0.0, 1.0, value) * v_opacity;
gl_FragColor = vec4(alpha, alpha, alpha, alpha); gl_FragColor = vec4(1.0, 1.0, 1.0, alpha);
}`, }`,
uniforms: { uniforms: {
u_size: function() { u_size: function() {

View File

@@ -110,7 +110,8 @@ const FRAGMENT_SHADER = `
* *
* The following uniform is used for the main texture: `u_texture`. * The following uniform is used for the main texture: `u_texture`.
* *
* Please note that the main shader output should have premultiplied alpha, otherwise visual anomalies may occur. * Please note that the main shader output should have premultiplied alpha, otherwise the colors will be blended
* additively.
* *
* Points are rendered as quads with the following structure: * Points are rendered as quads with the following structure:
* *
@@ -322,9 +323,6 @@ class WebGLPointsLayerRenderer extends LayerRenderer {
baseIndex + 1, baseIndex + 2, baseIndex + 3 baseIndex + 1, baseIndex + 2, baseIndex + 3
); );
}); });
this.helper_.flushBufferData(ARRAY_BUFFER, this.verticesBuffer_);
this.helper_.flushBufferData(ELEMENT_ARRAY_BUFFER, this.indicesBuffer_);
} }
// write new data // write new data

View File

@@ -23,8 +23,8 @@ import {getContext} from '../webgl';
/** /**
* @typedef {Object} BufferCacheEntry * @typedef {Object} BufferCacheEntry
* @property {import("./Buffer.js").default} buffer * @property {import("./Buffer.js").default} buf
* @property {WebGLBuffer} webGlBuffer * @property {WebGLBuffer} buffer
*/ */
/** /**
@@ -153,24 +153,15 @@ export const DefaultAttrib = {
* ### Binding WebGL buffers and flushing data into them: * ### Binding WebGL buffers and flushing data into them:
* *
* Data that must be passed to the GPU has to be transferred using `WebGLArrayBuffer` objects. * Data that must be passed to the GPU has to be transferred using `WebGLArrayBuffer` objects.
* A buffer has to be created only once, but must be bound everytime the buffer content should be used for rendering. * A buffer has to be created only once, but must be bound everytime the data it holds is changed. Using `WebGLHelper.bindBuffer`
* This is done using `WebGLHelper.bindBuffer`. * will bind the buffer and flush the new data to the GPU.
* When the buffer's array content has changed, the new data has to be flushed to the GPU memory; this is done using
* `WebGLHelper.flushBufferData`. Note: this operation is expensive and should be done as infrequently as possible.
* *
* When binding a `WebGLArrayBuffer`, a `target` parameter must be given: it should be either {@link module:ol/webgl~ARRAY_BUFFER} * For now, the `WebGLHelper` class expects {@link module:ol/webgl/Buffer~WebGLArrayBuffer} objects.
* (if the buffer contains vertices data) or {@link module:ol/webgl~ELEMENT_ARRAY_BUFFER} (if the buffer contains indices data).
*
* Examples below:
* ```js * ```js
* // at initialization phase * // at initialization phase
* this.verticesBuffer = new WebGLArrayBuffer([], DYNAMIC_DRAW); * this.verticesBuffer = new WebGLArrayBuffer([], DYNAMIC_DRAW);
* this.indicesBuffer = new WebGLArrayBuffer([], DYNAMIC_DRAW); * this.indicesBuffer = new WebGLArrayBuffer([], DYNAMIC_DRAW);
* *
* // when array values have changed
* this.context.flushBufferData(ARRAY_BUFFER, this.verticesBuffer);
* this.context.flushBufferData(ELEMENT_ARRAY_BUFFER, this.indicesBuffer);
*
* // at rendering phase * // at rendering phase
* this.context.bindBuffer(ARRAY_BUFFER, this.verticesBuffer); * this.context.bindBuffer(ARRAY_BUFFER, this.verticesBuffer);
* this.context.bindBuffer(ELEMENT_ARRAY_BUFFER, this.indicesBuffer); * this.context.bindBuffer(ELEMENT_ARRAY_BUFFER, this.indicesBuffer);
@@ -351,35 +342,24 @@ class WebGLHelper extends Disposable {
* Just bind the buffer if it's in the cache. Otherwise create * Just bind the buffer if it's in the cache. Otherwise create
* the WebGL buffer, bind it, populate it, and add an entry to * the WebGL buffer, bind it, populate it, and add an entry to
* the cache. * the cache.
* @param {number} target Target, either ARRAY_BUFFER or ELEMENT_ARRAY_BUFFER. * TODO: improve this, the logic is unclear: we want A/ to bind a buffer and B/ to flush data in it
* @param {import("./Buffer").default} buffer Buffer. * @param {number} target Target.
* @param {import("./Buffer").default} buf Buffer.
* @api * @api
*/ */
bindBuffer(target, buffer) { bindBuffer(target, buf) {
const gl = this.getGL(); const gl = this.getGL();
const bufferKey = getUid(buffer); const arr = buf.getArray();
const bufferKey = getUid(buf);
let bufferCache = this.bufferCache_[bufferKey]; let bufferCache = this.bufferCache_[bufferKey];
if (!bufferCache) { if (!bufferCache) {
const webGlBuffer = gl.createBuffer(); const buffer = gl.createBuffer();
bufferCache = this.bufferCache_[bufferKey] = { bufferCache = this.bufferCache_[bufferKey] = {
buffer: buffer, buf: buf,
webGlBuffer: webGlBuffer buffer: buffer
}; };
} }
gl.bindBuffer(target, bufferCache.webGlBuffer); gl.bindBuffer(target, bufferCache.buffer);
}
/**
* Update the data contained in the buffer array; this is required for the
* new data to be rendered
* @param {number} target Target, either ARRAY_BUFFER or ELEMENT_ARRAY_BUFFER.
* @param {import("./Buffer").default} buffer Buffer.
* @api
*/
flushBufferData(target, buffer) {
const gl = this.getGL();
const arr = buffer.getArray();
this.bindBuffer(target, buffer);
let /** @type {ArrayBufferView} */ arrayBuffer; let /** @type {ArrayBufferView} */ arrayBuffer;
if (target == ARRAY_BUFFER) { if (target == ARRAY_BUFFER) {
arrayBuffer = new Float32Array(arr); arrayBuffer = new Float32Array(arr);
@@ -387,7 +367,7 @@ class WebGLHelper extends Disposable {
arrayBuffer = this.hasOESElementIndexUint ? arrayBuffer = this.hasOESElementIndexUint ?
new Uint32Array(arr) : new Uint16Array(arr); new Uint32Array(arr) : new Uint16Array(arr);
} }
gl.bufferData(target, arrayBuffer, buffer.getUsage()); gl.bufferData(target, arrayBuffer, buf.getUsage());
} }
/** /**

View File

@@ -28,6 +28,7 @@ const DEFAULT_FRAGMENT_SHADER = `
void main() { void main() {
gl_FragColor = texture2D(u_image, v_texCoord); gl_FragColor = texture2D(u_image, v_texCoord);
gl_FragColor.rgb *= gl_FragColor.a;
} }
`; `;
@@ -58,9 +59,6 @@ const DEFAULT_FRAGMENT_SHADER = `
* a pixel which is 100% red with an opacity of 50% must have a color of (r=0.5, g=0, b=0, a=0.5). * a pixel which is 100% red with an opacity of 50% must have a color of (r=0.5, g=0, b=0, a=0.5).
* Failing to provide pixel colors with premultiplied alpha will result in render anomalies. * Failing to provide pixel colors with premultiplied alpha will result in render anomalies.
* *
* The default post-processing pass does *not* multiply color values with alpha value, it expects color values to be
* premultiplied.
*
* Default shaders are shown hereafter: * Default shaders are shown hereafter:
* *
* * Vertex shader: * * Vertex shader:
@@ -93,6 +91,7 @@ const DEFAULT_FRAGMENT_SHADER = `
* *
* void main() { * void main() {
* gl_FragColor = texture2D(u_image, v_texCoord); * gl_FragColor = texture2D(u_image, v_texCoord);
* gl_FragColor.rgb *= gl_FragColor.a;
* } * }
* ``` * ```
* *

View File

@@ -24,7 +24,7 @@ describe('ol.interaction.KeyboardPan', function() {
describe('handleEvent()', function() { describe('handleEvent()', function() {
it('pans on arrow keys', function() { it('pans on arrow keys', function() {
const view = map.getView(); const view = map.getView();
const spy = sinon.spy(view, 'animate_'); const spy = sinon.spy(view, 'animate');
const event = new MapBrowserEvent('keydown', map, { const event = new MapBrowserEvent('keydown', map, {
type: 'keydown', type: 'keydown',
target: map.getTargetElement(), target: map.getTargetElement(),
@@ -51,7 +51,7 @@ describe('ol.interaction.KeyboardPan', function() {
expect(spy.getCall(3).args[0].center).to.eql([128, 0]); expect(spy.getCall(3).args[0].center).to.eql([128, 0]);
view.setCenter([0, 0]); view.setCenter([0, 0]);
view.animate_.restore(); view.animate.restore();
}); });
}); });

View File

@@ -41,18 +41,13 @@ describe('ol.renderer.canvas.TileLayer', function() {
document.body.removeChild(target); document.body.removeChild(target);
}); });
it('properly handles interim tiles', function(done) { it('properly handles interim tiles', function() {
const layer = map.getLayers().item(0); const layer = map.getLayers().item(0);
source.once('tileloadend', function(e) {
expect(e.tile.inTransition()).to.be(false);
done();
});
source.updateParams({TIME: '1'}); source.updateParams({TIME: '1'});
map.renderSync(); map.renderSync();
const tiles = map.getRenderer().getLayerRenderer(layer).renderedTiles; const tiles = map.getRenderer().getLayerRenderer(layer).renderedTiles;
expect(tiles.length).to.be(1); expect(tiles.length).to.be(1);
expect(tiles[0]).to.equal(tile); expect(tiles[0]).to.equal(tile);
expect(tile.inTransition()).to.be(true);
}); });
}); });

View File

@@ -1801,52 +1801,6 @@ describe('ol.View', function() {
}); });
}); });
describe('does not start unexpected animations during interaction', function() {
let map;
beforeEach(function() {
map = new Map({
target: createMapDiv(512, 256)
});
});
afterEach(function() {
disposeMap(map);
});
it('works when initialized with #setCenter() and #setZoom()', function(done) {
const view = map.getView();
let callCount = 0;
view.on('change:resolution', function() {
++callCount;
});
view.setCenter([0, 0]);
view.setZoom(0);
view.beginInteraction();
view.endInteraction();
setTimeout(function() {
expect(callCount).to.be(1);
done();
}, 500);
});
it('works when initialized with #animate()', function(done) {
const view = map.getView();
let callCount = 0;
view.on('change:resolution', function() {
++callCount;
});
view.animate({
center: [0, 0],
zoom: 0
});
view.beginInteraction();
view.endInteraction();
setTimeout(function() {
expect(callCount).to.be(1);
done();
}, 500);
});
});
describe('ol.View.isNoopAnimation()', function() { describe('ol.View.isNoopAnimation()', function() {
const cases = [{ const cases = [{