diff --git a/examples/icon-sprite-webgl.html b/examples/icon-sprite-webgl.html
index f465510d4c..c6dc366cd0 100644
--- a/examples/icon-sprite-webgl.html
+++ b/examples/icon-sprite-webgl.html
@@ -18,3 +18,4 @@ cloak:
value: Your Mapbox access token from https://mapbox.com/ here
---
+Current sighting:
diff --git a/examples/icon-sprite-webgl.js b/examples/icon-sprite-webgl.js
index 7c0e1da6bd..08f875e634 100644
--- a/examples/icon-sprite-webgl.js
+++ b/examples/icon-sprite-webgl.js
@@ -103,7 +103,7 @@ function loadData() {
loadData();
-new Map({
+const map = new Map({
layers: [
new TileLayer({
source: new TileJSON({
@@ -121,3 +121,18 @@ new Map({
zoom: 2
})
});
+
+const info = document.getElementById('info');
+map.on('pointermove', function(evt) {
+ if (map.getView().getInteracting()) {
+ 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.';
+ });
+});
diff --git a/src/ol/renderer/Layer.js b/src/ol/renderer/Layer.js
index cde8519918..9c0187c1e7 100644
--- a/src/ol/renderer/Layer.js
+++ b/src/ol/renderer/Layer.js
@@ -132,15 +132,6 @@ class LayerRenderer extends Observable {
}
}
- /**
- * @param {import("../coordinate.js").Coordinate} coordinate Coordinate.
- * @param {import("../PluggableMap.js").FrameState} frameState Frame state.
- * @return {boolean} Is there a feature at the given coordinate?
- */
- hasFeatureAtCoordinate(coordinate, frameState) {
- return false;
- }
-
/**
* Load the image if not already loaded, and register the image change
* listener if needed.
diff --git a/src/ol/renderer/webgl/Layer.js b/src/ol/renderer/webgl/Layer.js
index b50b54d1b2..c8e6cbbc20 100644
--- a/src/ol/renderer/webgl/Layer.js
+++ b/src/ol/renderer/webgl/Layer.js
@@ -81,6 +81,7 @@ class WebGLLayerRenderer extends LayerRenderer {
getShaderCompileErrors() {
return this.helper.getShaderCompileErrors();
}
+
}
@@ -231,11 +232,47 @@ export function writePointFeatureToBuffers(instructions, elementIndex, vertexBuf
* @private
* @return {ImageData} Image data.
*/
-export function getBlankTexture() {
+export function getBlankImageData() {
const canvas = document.createElement('canvas');
const image = canvas.getContext('2d').createImageData(1, 1);
image.data[0] = image.data[1] = image.data[2] = image.data[3] = 255;
return image;
}
+/**
+ * Generates a color array based on a numerical id
+ * Note: the range for each component is 0 to 1 with 256 steps
+ * @param {number} id Id
+ * @param {Array} [opt_array] Reusable array
+ * @return {Array} Color array containing the encoded id
+ */
+export function colorEncodeId(id, opt_array) {
+ const array = opt_array || [];
+ const radix = 256;
+ const divide = radix - 1;
+ array[0] = Math.floor(id / radix / radix / radix) / divide;
+ array[1] = (Math.floor(id / radix / radix) % radix) / divide;
+ array[2] = (Math.floor(id / radix) % radix) / divide;
+ array[3] = (id % radix) / divide;
+ return array;
+}
+
+
+/**
+ * Reads an id from a color-encoded array
+ * Note: the expected range for each component is 0 to 1 with 256 steps.
+ * @param {Array} color Color array containing the encoded id
+ * @return {number} Decoded id
+ */
+export function colorDecodeId(color) {
+ let id = 0;
+ const radix = 256;
+ const mult = radix - 1;
+ id += Math.round(color[0] * radix * radix * radix * mult);
+ id += Math.round(color[1] * radix * radix * mult);
+ id += Math.round(color[2] * radix * mult);
+ id += Math.round(color[3] * mult);
+ return id;
+}
+
export default WebGLLayerRenderer;
diff --git a/src/ol/renderer/webgl/PointsLayer.js b/src/ol/renderer/webgl/PointsLayer.js
index 05dab3ef5f..6f430792c5 100644
--- a/src/ol/renderer/webgl/PointsLayer.js
+++ b/src/ol/renderer/webgl/PointsLayer.js
@@ -6,7 +6,9 @@ import {DYNAMIC_DRAW, ARRAY_BUFFER, ELEMENT_ARRAY_BUFFER, FLOAT} from '../../web
import {DefaultAttrib, DefaultUniform} from '../../webgl/Helper.js';
import GeometryType from '../../geom/GeometryType.js';
import WebGLLayerRenderer, {
- getBlankTexture,
+ colorDecodeId,
+ colorEncodeId,
+ getBlankImageData,
POINT_INSTRUCTIONS_COUNT, POINT_VERTEX_STRIDE, WebGLWorkerMessageType,
writePointFeatureInstructions
} from './Layer.js';
@@ -19,6 +21,8 @@ import {
apply as applyTransform
} from '../../transform.js';
import {create as createWebGLWorker} from '../../worker/webgl.js';
+import {getUid} from '../../util.js';
+import WebGLRenderTarget from '../../webgl/RenderTarget.js';
const VERTEX_SHADER = `
precision mediump float;
@@ -68,6 +72,26 @@ const FRAGMENT_SHADER = `
gl_FragColor.rgb *= gl_FragColor.a;
}`;
+const HIT_FRAGMENT_SHADER = `
+ precision mediump float;
+
+ uniform sampler2D u_texture;
+
+ varying vec2 v_texCoord;
+ varying float v_opacity;
+ varying vec4 v_color;
+
+ void main(void) {
+ if (v_opacity == 0.0) {
+ discard;
+ }
+ vec4 textureColor = texture2D(u_texture, v_texCoord);
+ if (textureColor.a < 0.1) {
+ discard;
+ }
+ gl_FragColor = v_color;
+ }`;
+
/**
* @typedef {Object} Options
* @property {function(import("../../Feature").default):number} [sizeCallback] Will be called on every feature in the
@@ -200,7 +224,7 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
const options = opt_options || {};
const uniforms = options.uniforms || {};
- uniforms.u_texture = options.texture || getBlankTexture();
+ uniforms.u_texture = options.texture || getBlankImageData();
const projectionMatrixTransform = createTransform();
uniforms[DefaultUniform.PROJECTION_MATRIX] = projectionMatrixTransform;
@@ -212,14 +236,17 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
this.sourceRevision_ = -1;
this.verticesBuffer_ = new WebGLArrayBuffer(ARRAY_BUFFER, DYNAMIC_DRAW);
+ this.hitVerticesBuffer_ = new WebGLArrayBuffer(ARRAY_BUFFER, DYNAMIC_DRAW);
this.indicesBuffer_ = new WebGLArrayBuffer(ELEMENT_ARRAY_BUFFER, DYNAMIC_DRAW);
this.program_ = this.helper.getProgram(
options.fragmentShader || FRAGMENT_SHADER,
options.vertexShader || VERTEX_SHADER
);
-
- this.helper.useProgram(this.program_);
+ this.hitProgram_ = this.helper.getProgram(
+ HIT_FRAGMENT_SHADER,
+ options.vertexShader || VERTEX_SHADER
+ );
this.sizeCallback_ = options.sizeCallback || function() {
return 1;
@@ -274,21 +301,55 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
*/
this.renderInstructions_ = new Float32Array(0);
+ /**
+ * @type {import("../../transform.js").Transform}
+ * @private
+ */
+ this.hitRenderTransform_ = createTransform();
+
+ /**
+ * @type {import("../../transform.js").Transform}
+ * @private
+ */
+ this.invertHitRenderTransform_ = createTransform();
+
+ /**
+ * These instructions are used for hit detection
+ * @type {Float32Array}
+ * @private
+ */
+ this.hitRenderInstructions_ = new Float32Array(0);
+
+ /**
+ * @type {WebGLRenderTarget}
+ * @private
+ */
+ this.hitRenderTarget_ = new WebGLRenderTarget(this.helper);
+
this.worker_ = createWebGLWorker();
this.worker_.addEventListener('message', function(event) {
const received = event.data;
if (received.type === WebGLWorkerMessageType.GENERATE_BUFFERS) {
const projectionTransform = received.projectionTransform;
- this.verticesBuffer_.fromArrayBuffer(received.vertexBuffer);
+ if (received.hitDetection) {
+ this.hitVerticesBuffer_.fromArrayBuffer(received.vertexBuffer);
+ this.helper.flushBufferData(this.hitVerticesBuffer_);
+ } else {
+ this.verticesBuffer_.fromArrayBuffer(received.vertexBuffer);
+ this.helper.flushBufferData(this.verticesBuffer_);
+ }
this.indicesBuffer_.fromArrayBuffer(received.indexBuffer);
- this.helper.flushBufferData(this.verticesBuffer_);
this.helper.flushBufferData(this.indicesBuffer_);
- // saves the projection transform for the current frame state
- this.renderTransform_ = projectionTransform;
- makeInverseTransform(this.invertRenderTransform_, this.renderTransform_);
-
- this.renderInstructions_ = new Float32Array(event.data.renderInstructions);
+ if (received.hitDetection) {
+ this.hitRenderTransform_ = projectionTransform;
+ makeInverseTransform(this.invertHitRenderTransform_, this.hitRenderTransform_);
+ this.hitRenderInstructions_ = new Float32Array(event.data.renderInstructions);
+ } else {
+ this.renderTransform_ = projectionTransform;
+ makeInverseTransform(this.invertRenderTransform_, this.renderTransform_);
+ this.renderInstructions_ = new Float32Array(event.data.renderInstructions);
+ }
}
}.bind(this));
}
@@ -315,6 +376,9 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
canvas.style.opacity = opacity;
}
+ this.renderHitDetection(frameState);
+ this.hitRenderTarget_.clearCachedData();
+
return canvas;
}
@@ -349,6 +413,7 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
this.helper.makeProjectionTransform(frameState, this.currentTransform_);
multiplyTransform(this.currentTransform_, this.invertRenderTransform_);
+ this.helper.useProgram(this.program_);
this.helper.prepareDraw(frameState);
// write new data
@@ -384,11 +449,16 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
if (!this.renderInstructions_ || this.renderInstructions_.length !== totalInstructionsCount) {
this.renderInstructions_ = new Float32Array(totalInstructionsCount);
}
+ if (!this.hitRenderInstructions_ || this.hitRenderInstructions_.length !== totalInstructionsCount) {
+ this.hitRenderInstructions_ = new Float32Array(totalInstructionsCount);
+ }
// loop on features to fill the buffer
let feature;
const tmpCoords = [];
+ const tmpColor = [];
let elementIndex = 0;
+ let u0, v0, u1, v1, size, opacity, rotateWithView, color;
for (let i = 0; i < features.length; i++) {
feature = features[i];
if (!feature.getGeometry() || feature.getGeometry().getType() !== GeometryType.POINT) {
@@ -399,19 +469,45 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
tmpCoords[1] = this.coordCallback_(feature, 1);
applyTransform(projectionTransform, tmpCoords);
- elementIndex = writePointFeatureInstructions(
+ u0 = this.texCoordCallback_(feature, 0);
+ v0 = this.texCoordCallback_(feature, 1);
+ u1 = this.texCoordCallback_(feature, 2);
+ v1 = this.texCoordCallback_(feature, 3);
+ size = this.sizeCallback_(feature);
+ opacity = this.opacityCallback_(feature);
+ rotateWithView = this.rotateWithViewCallback_(feature);
+ color = this.colorCallback_(feature, this.colorArray_);
+
+ writePointFeatureInstructions(
this.renderInstructions_,
elementIndex,
tmpCoords[0],
tmpCoords[1],
- this.texCoordCallback_(feature, 0),
- this.texCoordCallback_(feature, 1),
- this.texCoordCallback_(feature, 2),
- this.texCoordCallback_(feature, 3),
- this.sizeCallback_(feature),
- this.opacityCallback_(feature),
- this.rotateWithViewCallback_(feature),
- this.colorCallback_(feature, this.colorArray_)
+ u0,
+ v0,
+ u1,
+ v1,
+ size,
+ opacity,
+ rotateWithView,
+ color
+ );
+
+ // for hit detection, the feature uid is saved in the opacity value
+ // and the index of the opacity value is encoded in the color values
+ elementIndex = writePointFeatureInstructions(
+ this.hitRenderInstructions_,
+ elementIndex,
+ tmpCoords[0],
+ tmpCoords[1],
+ u0,
+ v0,
+ u1,
+ v1,
+ size,
+ opacity > 0 ? Number(getUid(feature)) : 0,
+ rotateWithView,
+ colorEncodeId(elementIndex + 7, tmpColor)
);
}
@@ -422,12 +518,80 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
};
// additional properties will be sent back as-is by the worker
message['projectionTransform'] = projectionTransform;
-
this.worker_.postMessage(message, [this.renderInstructions_.buffer]);
this.renderInstructions_ = null;
+
+ /** @type import('./Layer').WebGLWorkerGenerateBuffersMessage */
+ const hitMessage = {
+ type: WebGLWorkerMessageType.GENERATE_BUFFERS,
+ renderInstructions: this.hitRenderInstructions_.buffer
+ };
+ hitMessage['projectionTransform'] = projectionTransform;
+ hitMessage['hitDetection'] = true;
+ this.worker_.postMessage(hitMessage, [this.hitRenderInstructions_.buffer]);
+ this.hitRenderInstructions_ = null;
}
+ /**
+ * @inheritDoc
+ */
+ forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback, declutteredFeatures) {
+ if (!this.hitRenderInstructions_) {
+ return;
+ }
+ const pixel = applyTransform(frameState.coordinateToPixelTransform, coordinate.slice());
+
+ const data = this.hitRenderTarget_.readPixel(pixel[0], pixel[1]);
+ const color = [
+ data[0] / 255,
+ data[1] / 255,
+ data[2] / 255,
+ data[3] / 255
+ ];
+ const index = colorDecodeId(color);
+ const opacity = this.hitRenderInstructions_[index];
+ const uid = Math.floor(opacity).toString();
+
+ const source = this.getLayer().getSource();
+ const feature = source.getFeatureByUid(uid);
+ if (feature) {
+ return callback(feature, this.getLayer());
+ }
+ }
+
+ /**
+ * Render the hit detection data to the corresponding render target
+ * @param {import("../../PluggableMap.js").FrameState} frameState current frame state
+ */
+ renderHitDetection(frameState) {
+ const width = frameState.size[0];
+ const height = frameState.size[1];
+ const size = [width, height];
+
+ this.hitRenderTarget_.setSize(size);
+
+ this.helper.useProgram(this.hitProgram_);
+ this.helper.prepareDrawToRenderTarget(frameState, this.hitRenderTarget_, true);
+
+ this.helper.makeProjectionTransform(frameState, this.currentTransform_);
+ multiplyTransform(this.currentTransform_, this.invertHitRenderTransform_);
+
+ this.helper.bindBuffer(this.hitVerticesBuffer_);
+ this.helper.bindBuffer(this.indicesBuffer_);
+
+ const stride = POINT_VERTEX_STRIDE;
+ const bytesPerFloat = Float32Array.BYTES_PER_ELEMENT;
+ this.helper.enableAttributeArray(DefaultAttrib.POSITION, 2, FLOAT, bytesPerFloat * stride, 0);
+ this.helper.enableAttributeArray(DefaultAttrib.OFFSETS, 2, FLOAT, bytesPerFloat * stride, bytesPerFloat * 2);
+ this.helper.enableAttributeArray(DefaultAttrib.TEX_COORD, 2, FLOAT, bytesPerFloat * stride, bytesPerFloat * 4);
+ this.helper.enableAttributeArray(DefaultAttrib.OPACITY, 1, FLOAT, bytesPerFloat * stride, bytesPerFloat * 6);
+ this.helper.enableAttributeArray(DefaultAttrib.ROTATE_WITH_VIEW, 1, FLOAT, bytesPerFloat * stride, bytesPerFloat * 7);
+ this.helper.enableAttributeArray(DefaultAttrib.COLOR, 4, FLOAT, bytesPerFloat * stride, bytesPerFloat * 8);
+
+ const renderCount = this.indicesBuffer_.getArray() ? this.indicesBuffer_.getArray().length : 0;
+ this.helper.drawElements(0, renderCount);
+ }
}
export default WebGLPointsLayerRenderer;
diff --git a/src/ol/source/Vector.js b/src/ol/source/Vector.js
index 23aa25a30c..9f4a0f39f5 100644
--- a/src/ol/source/Vector.js
+++ b/src/ol/source/Vector.js
@@ -241,11 +241,11 @@ class VectorSource extends Source {
this.idIndex_ = {};
/**
- * A lookup of features without id (keyed by getUid(feature)).
+ * A lookup of features by uid (using getUid(feature)).
* @private
* @type {!Object>}
*/
- this.undefIdIndex_ = {};
+ this.uidIndex_ = {};
/**
* @private
@@ -359,10 +359,11 @@ class VectorSource extends Source {
} else {
valid = false;
}
- } else {
- assert(!(featureKey in this.undefIdIndex_),
+ }
+ if (valid) {
+ assert(!(featureKey in this.uidIndex_),
30); // The passed `feature` was already added to the source
- this.undefIdIndex_[featureKey] = feature;
+ this.uidIndex_[featureKey] = feature;
}
return valid;
}
@@ -489,7 +490,7 @@ class VectorSource extends Source {
if (!this.featuresCollection_) {
this.featureChangeKeys_ = {};
this.idIndex_ = {};
- this.undefIdIndex_ = {};
+ this.uidIndex_ = {};
}
} else {
if (this.featuresRtree_) {
@@ -770,6 +771,18 @@ class VectorSource extends Source {
}
+ /**
+ * Get a feature by its internal unique identifier (using `getUid`).
+ *
+ * @param {string} uid Feature identifier.
+ * @return {import("../Feature.js").default} The feature (or `null` if not found).
+ */
+ getFeatureByUid(uid) {
+ const feature = this.uidIndex_[uid];
+ return feature !== undefined ? feature : null;
+ }
+
+
/**
* Get the format associated with this source.
*
@@ -831,20 +844,13 @@ class VectorSource extends Source {
const id = feature.getId();
if (id !== undefined) {
const sid = id.toString();
- if (featureKey in this.undefIdIndex_) {
- delete this.undefIdIndex_[featureKey];
+ if (this.idIndex_[sid] !== feature) {
+ this.removeFromIdIndex_(feature);
this.idIndex_[sid] = feature;
- } else {
- if (this.idIndex_[sid] !== feature) {
- this.removeFromIdIndex_(feature);
- this.idIndex_[sid] = feature;
- }
}
} else {
- if (!(featureKey in this.undefIdIndex_)) {
- this.removeFromIdIndex_(feature);
- this.undefIdIndex_[featureKey] = feature;
- }
+ this.removeFromIdIndex_(feature);
+ this.uidIndex_[featureKey] = feature;
}
this.changed();
this.dispatchEvent(new VectorSourceEvent(
@@ -862,7 +868,7 @@ class VectorSource extends Source {
if (id !== undefined) {
return id in this.idIndex_;
} else {
- return getUid(feature) in this.undefIdIndex_;
+ return getUid(feature) in this.uidIndex_;
}
}
@@ -964,9 +970,8 @@ class VectorSource extends Source {
const id = feature.getId();
if (id !== undefined) {
delete this.idIndex_[id.toString()];
- } else {
- delete this.undefIdIndex_[featureKey];
}
+ delete this.uidIndex_[featureKey];
this.dispatchEvent(new VectorSourceEvent(
VectorEventType.REMOVEFEATURE, feature));
}
diff --git a/src/ol/webgl/Helper.js b/src/ol/webgl/Helper.js
index 1bc6e2d0db..e844e255ae 100644
--- a/src/ol/webgl/Helper.js
+++ b/src/ol/webgl/Helper.js
@@ -432,6 +432,30 @@ class WebGLHelper extends Disposable {
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+ gl.useProgram(this.currentProgram_);
+ this.applyFrameState(frameState);
+ this.applyUniforms(frameState);
+ }
+
+ /**
+ * Clear the render target & bind it for future draw operations.
+ * This is similar to `prepareDraw`, only post processes will not be applied.
+ * Note: the whole viewport will be drawn to the render target, regardless of its size.
+ * @param {import("../PluggableMap.js").FrameState} frameState current frame state
+ * @param {import("./RenderTarget.js").default} renderTarget Render target to draw to
+ * @param {boolean} [opt_disableAlphaBlend] If true, no alpha blending will happen.
+ */
+ prepareDrawToRenderTarget(frameState, renderTarget, opt_disableAlphaBlend) {
+ const gl = this.getGL();
+
+ gl.bindFramebuffer(gl.FRAMEBUFFER, renderTarget.getFramebuffer());
+ gl.bindTexture(gl.TEXTURE_2D, renderTarget.getTexture());
+ gl.clearColor(0.0, 0.0, 0.0, 0.0);
+ gl.clear(gl.COLOR_BUFFER_BIT);
+ gl.enable(gl.BLEND);
+ gl.blendFunc(gl.ONE, opt_disableAlphaBlend ? gl.ZERO : gl.ONE_MINUS_SRC_ALPHA);
+
+ gl.useProgram(this.currentProgram_);
this.applyFrameState(frameState);
this.applyUniforms(frameState);
}
@@ -742,57 +766,36 @@ class WebGLHelper extends Disposable {
// TODO: shutdown program
/**
- * TODO: these are not used and should be reworked
- * @param {number=} opt_wrapS wrapS.
- * @param {number=} opt_wrapT wrapT.
- * @return {WebGLTexture} The texture.
+ * Will create or reuse a given webgl texture and apply the given size. If no image data
+ * specified, the texture will be empty, otherwise image data will be used and the `size`
+ * parameter will be ignored.
+ * Note: wrap parameters are set to clamp to edge, min filter is set to linear.
+ * @param {Array} size Expected size of the texture
+ * @param {ImageData|HTMLImageElement|HTMLCanvasElement} [opt_data] Image data/object to bind to the texture
+ * @param {WebGLTexture} [opt_texture] Existing texture to reuse
+ * @return {WebGLTexture} The generated texture
+ * @api
*/
- createTextureInternal(opt_wrapS, opt_wrapT) {
+ createTexture(size, opt_data, opt_texture) {
const gl = this.getGL();
- const texture = gl.createTexture();
+ const texture = opt_texture || gl.createTexture();
+
+ // set params & size
+ const level = 0;
+ const internalFormat = gl.RGBA;
+ const border = 0;
+ const format = gl.RGBA;
+ const type = gl.UNSIGNED_BYTE;
gl.bindTexture(gl.TEXTURE_2D, texture);
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
+ if (opt_data) {
+ gl.texImage2D(gl.TEXTURE_2D, level, internalFormat, format, type, opt_data);
+ } else {
+ gl.texImage2D(gl.TEXTURE_2D, level, internalFormat, size[0], size[1], border, format, type, null);
+ }
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
- if (opt_wrapS !== undefined) {
- gl.texParameteri(
- gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, opt_wrapS);
- }
- if (opt_wrapT !== undefined) {
- gl.texParameteri(
- gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, opt_wrapT);
- }
-
- return texture;
- }
-
- /**
- * TODO: these are not used and should be reworked
- * @param {number} width Width.
- * @param {number} height Height.
- * @param {number=} opt_wrapS wrapS.
- * @param {number=} opt_wrapT wrapT.
- * @return {WebGLTexture} The texture.
- */
- createEmptyTexture(width, height, opt_wrapS, opt_wrapT) {
- const gl = this.getGL();
- const texture = this.createTextureInternal(opt_wrapS, opt_wrapT);
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
- return texture;
- }
-
-
- /**
- * TODO: these are not used and should be reworked
- * @param {HTMLCanvasElement|HTMLImageElement|HTMLVideoElement} image Image.
- * @param {number=} opt_wrapS wrapS.
- * @param {number=} opt_wrapT wrapT.
- * @return {WebGLTexture} The texture.
- */
- createTexture(image, opt_wrapS, opt_wrapT) {
- const gl = this.getGL();
- const texture = this.createTextureInternal(opt_wrapS, opt_wrapT);
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
return texture;
}
}
diff --git a/src/ol/webgl/RenderTarget.js b/src/ol/webgl/RenderTarget.js
new file mode 100644
index 0000000000..4500495dc1
--- /dev/null
+++ b/src/ol/webgl/RenderTarget.js
@@ -0,0 +1,164 @@
+/**
+ * A wrapper class to simplify rendering to a texture instead of the final canvas
+ * @module ol/webgl/RenderTarget
+ */
+import {equals} from '../array.js';
+
+// for pixel color reading
+const tmpArray4 = new Uint8Array(4);
+
+/**
+ * @classdesc
+ * This class is a wrapper around the association of both a `WebGLTexture` and a `WebGLFramebuffer` instances,
+ * simplifying initialization and binding for rendering.
+ * @api
+ */
+class WebGLRenderTarget {
+
+ /**
+ * @param {import("./Helper.js").default} helper WebGL helper; mandatory.
+ * @param {Array} [opt_size] Expected size of the render target texture; note: this can be changed later on.
+ */
+ constructor(helper, opt_size) {
+ /**
+ * @private
+ * @type {import("./Helper.js").default}
+ */
+ this.helper_ = helper;
+ const gl = helper.getGL();
+
+ /**
+ * @private
+ * @type {WebGLTexture}
+ */
+ this.texture_ = gl.createTexture();
+
+ /**
+ * @private
+ * @type {WebGLFramebuffer}
+ */
+ this.framebuffer_ = gl.createFramebuffer();
+
+ /**
+ * @type {Array}
+ * @private
+ */
+ this.size_ = opt_size || [1, 1];
+
+ /**
+ * @type {Uint8Array}
+ * @private
+ */
+ this.data_ = new Uint8Array(0);
+
+ /**
+ * @type {boolean}
+ * @private
+ */
+ this.dataCacheDirty_ = true;
+
+ this.updateSize_();
+ }
+
+ /**
+ * Changes the size of the render target texture. Note: will do nothing if the size
+ * is already the same.
+ * @param {Array} size Expected size of the render target texture
+ * @api
+ */
+ setSize(size) {
+ if (equals(size, this.size_)) {
+ return;
+ }
+ this.size_[0] = size[0];
+ this.size_[1] = size[1];
+ this.updateSize_();
+ }
+
+ /**
+ * Returns the size of the render target texture
+ * @return {Array} Size of the render target texture
+ * @api
+ */
+ getSize() {
+ return this.size_;
+ }
+
+ /**
+ * This will cause following calls to `#readAll` or `#readPixel` to download the content of the
+ * render target into memory, which is an expensive operation.
+ * This content will be kept in cache but should be cleared after each new render.
+ * @api
+ */
+ clearCachedData() {
+ this.dataCacheDirty_ = true;
+ }
+
+ /**
+ * Returns the full content of the frame buffer as a series of r, g, b, a components
+ * in the 0-255 range (unsigned byte).
+ * @return {Uint8Array} Integer array of color values
+ * @api
+ */
+ readAll() {
+ if (this.dataCacheDirty_) {
+ const size = this.size_;
+ const gl = this.helper_.getGL();
+
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer_);
+ gl.readPixels(0, 0, size[0], size[1], gl.RGBA, gl.UNSIGNED_BYTE, this.data_);
+ this.dataCacheDirty_ = false;
+ }
+ return this.data_;
+ }
+
+ /**
+ * Reads one pixel of the frame buffer as an array of r, g, b, a components
+ * in the 0-255 range (unsigned byte).
+ * @param {number} x Pixel coordinate
+ * @param {number} y Pixel coordinate
+ * @returns {Uint8Array} Integer array with one color value (4 components)
+ * @api
+ */
+ readPixel(x, y) {
+ this.readAll();
+ const index = Math.floor(x) + (this.size_[1] - Math.floor(y) - 1) * this.size_[0];
+ tmpArray4[0] = this.data_[index * 4];
+ tmpArray4[1] = this.data_[index * 4 + 1];
+ tmpArray4[2] = this.data_[index * 4 + 2];
+ tmpArray4[3] = this.data_[index * 4 + 3];
+ return tmpArray4;
+ }
+
+ /**
+ * @return {WebGLTexture} Texture to render to
+ */
+ getTexture() {
+ return this.texture_;
+ }
+
+ /**
+ * @return {WebGLFramebuffer} Frame buffer of the render target
+ */
+ getFramebuffer() {
+ return this.framebuffer_;
+ }
+
+ /**
+ * @private
+ */
+ updateSize_() {
+ const size = this.size_;
+ const gl = this.helper_.getGL();
+
+ this.texture_ = this.helper_.createTexture(size, null, this.texture_);
+
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer_);
+ gl.viewport(0, 0, size[0], size[1]);
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.texture_, 0);
+
+ this.data_ = new Uint8Array(size[0] * size[1] * 4);
+ }
+}
+
+export default WebGLRenderTarget;
diff --git a/src/ol/webgl/Shader.js b/src/ol/webgl/Shader.js
deleted file mode 100644
index ed9e0f0566..0000000000
--- a/src/ol/webgl/Shader.js
+++ /dev/null
@@ -1,48 +0,0 @@
-/**
- * @module ol/webgl/Shader
- */
-import {abstract} from '../util.js';
-
-/**
- * @abstract
- */
-class WebGLShader {
-
- /**
- * @param {string} source Source.
- */
- constructor(source) {
-
- /**
- * @private
- * @type {string}
- */
- this.source_ = source;
-
- }
-
- /**
- * @return {boolean} Is animated?
- */
- isAnimated() {
- return false;
- }
-
- /**
- * @abstract
- * @return {number} Type.
- */
- getType() {
- return abstract();
- }
-
- /**
- * @return {string} Source.
- */
- getSource() {
- return this.source_;
- }
-}
-
-
-export default WebGLShader;
diff --git a/test/spec/ol/renderer/webgl/layer.test.js b/test/spec/ol/renderer/webgl/layer.test.js
index cb9f30f3c1..8bbd676850 100644
--- a/test/spec/ol/renderer/webgl/layer.test.js
+++ b/test/spec/ol/renderer/webgl/layer.test.js
@@ -1,5 +1,7 @@
import WebGLLayerRenderer, {
- getBlankTexture, POINT_INSTRUCTIONS_COUNT, POINT_VERTEX_STRIDE,
+ colorDecodeId,
+ colorEncodeId,
+ getBlankImageData, POINT_INSTRUCTIONS_COUNT, POINT_VERTEX_STRIDE,
writePointFeatureInstructions, writePointFeatureToBuffers
} from '../../../../../src/ol/renderer/webgl/Layer.js';
import Layer from '../../../../../src/ol/layer/Layer.js';
@@ -237,9 +239,9 @@ describe('ol.renderer.webgl.Layer', function() {
});
- describe('getBlankTexture', function() {
+ describe('getBlankImageData', function() {
it('creates a 1x1 white texture', function() {
- const texture = getBlankTexture();
+ const texture = getBlankImageData();
expect(texture.height).to.eql(1);
expect(texture.width).to.eql(1);
expect(texture.data[0]).to.eql(255);
@@ -249,4 +251,35 @@ describe('ol.renderer.webgl.Layer', function() {
});
});
+ describe('colorEncodeId and colorDecodeId', function() {
+ it('correctly encodes and decodes ids', function() {
+ expect(colorDecodeId(colorEncodeId(0))).to.eql(0);
+ expect(colorDecodeId(colorEncodeId(1))).to.eql(1);
+ expect(colorDecodeId(colorEncodeId(123))).to.eql(123);
+ expect(colorDecodeId(colorEncodeId(12345))).to.eql(12345);
+ expect(colorDecodeId(colorEncodeId(123456))).to.eql(123456);
+ expect(colorDecodeId(colorEncodeId(91612))).to.eql(91612);
+ expect(colorDecodeId(colorEncodeId(1234567890))).to.eql(1234567890);
+ });
+
+ it('correctly reuses array', function() {
+ const arr = [];
+ expect(colorEncodeId(123, arr)).to.be(arr);
+ });
+
+ it('is compatible with Uint8Array storage', function() {
+ const encoded = colorEncodeId(91612);
+ const typed = Uint8Array.of(encoded[0] * 255, encoded[1] * 255,
+ encoded[2] * 255, encoded[3] * 255);
+ const arr = [
+ typed[0] / 255,
+ typed[1] / 255,
+ typed[2] / 255,
+ typed[3] / 255
+ ];
+ const decoded = colorDecodeId(arr);
+ expect(decoded).to.eql(91612);
+ });
+ });
+
});
diff --git a/test/spec/ol/renderer/webgl/pointslayer.test.js b/test/spec/ol/renderer/webgl/pointslayer.test.js
index e08f32ed8f..7d67294960 100644
--- a/test/spec/ol/renderer/webgl/pointslayer.test.js
+++ b/test/spec/ol/renderer/webgl/pointslayer.test.js
@@ -6,6 +6,7 @@ import WebGLPointsLayerRenderer from '../../../../../src/ol/renderer/webgl/Point
import {get as getProjection} from '../../../../../src/ol/proj.js';
import ViewHint from '../../../../../src/ol/ViewHint.js';
import {POINT_VERTEX_STRIDE, WebGLWorkerMessageType} from '../../../../../src/ol/renderer/webgl/Layer.js';
+import {create as createTransform, translate as translateTransform} from '../../../../../src/ol/transform.js';
describe('ol.renderer.webgl.PointsLayer', function() {
@@ -146,4 +147,67 @@ describe('ol.renderer.webgl.PointsLayer', function() {
});
});
+ describe('#forEachFeatureAtCoordinate', function() {
+ let layer, renderer, feature;
+
+ beforeEach(function() {
+ feature = new Feature(new Point([0, 0]));
+ layer = new VectorLayer({
+ source: new VectorSource({
+ features: [feature]
+ })
+ });
+ renderer = new WebGLPointsLayerRenderer(layer, {
+ sizeCallback: function() {
+ return 4;
+ }
+ });
+ });
+
+ it('correctly hit detects a feature', function(done) {
+ const transform = translateTransform(createTransform(), 20, 20);
+ const projection = getProjection('EPSG:3857');
+ const frameState = {
+ viewState: {
+ projection: projection,
+ resolution: 1,
+ rotation: 0,
+ center: [0, 0]
+ },
+ layerStatesArray: [{}],
+ layerIndex: 0,
+ extent: [-20, -20, 20, 20],
+ size: [40, 40],
+ viewHints: [],
+ coordinateToPixelTransform: transform
+ };
+ let found;
+ const cb = function(feature) {
+ found = feature;
+ };
+
+ renderer.prepareFrame(frameState);
+ renderer.worker_.addEventListener('message', function() {
+ if (!renderer.hitRenderInstructions_) {
+ return;
+ }
+ renderer.renderFrame(frameState);
+
+ function checkHit(x, y, expected) {
+ found = null;
+ renderer.forEachFeatureAtCoordinate([x, y], frameState, 0, cb, null);
+ expect(found).to.be(expected ? feature : null);
+ }
+
+ checkHit(0, 0, true);
+ checkHit(1, -2, true);
+ checkHit(-2, 1, true);
+ checkHit(2, 0, false);
+ checkHit(1, -3, false);
+
+ done();
+ });
+ });
+ });
+
});
diff --git a/test/spec/ol/source/vector.test.js b/test/spec/ol/source/vector.test.js
index d48d2962a3..9fdb56a983 100644
--- a/test/spec/ol/source/vector.test.js
+++ b/test/spec/ol/source/vector.test.js
@@ -10,6 +10,7 @@ import {bbox as bboxStrategy} from '../../../../src/ol/loadingstrategy.js';
import {get as getProjection, transformExtent, fromLonLat} from '../../../../src/ol/proj.js';
import VectorSource from '../../../../src/ol/source/Vector.js';
import GeoJSON from '../../../../src/ol/format/GeoJSON.js';
+import {getUid} from '../../../../src/ol/util.js';
describe('ol.source.Vector', function() {
@@ -519,6 +520,59 @@ describe('ol.source.Vector', function() {
});
+ describe('#getFeatureByUid()', function() {
+ let source;
+ beforeEach(function() {
+ source = new VectorSource();
+ });
+
+ it('returns a feature with an id', function() {
+ const feature = new Feature();
+ feature.setId('abcd');
+ source.addFeature(feature);
+ expect(source.getFeatureByUid(getUid(feature))).to.be(feature);
+ });
+
+ it('returns a feature without id', function() {
+ const feature = new Feature();
+ source.addFeature(feature);
+ expect(source.getFeatureByUid(getUid(feature))).to.be(feature);
+ });
+
+ it('returns null when no feature is found', function() {
+ const feature = new Feature();
+ feature.setId('abcd');
+ source.addFeature(feature);
+ const wrongId = 'abcd';
+ expect(source.getFeatureByUid(wrongId)).to.be(null);
+ });
+
+ it('returns null after removing feature', function() {
+ const feature = new Feature();
+ feature.setId('abcd');
+ source.addFeature(feature);
+ const uid = getUid(feature);
+ expect(source.getFeatureByUid(uid)).to.be(feature);
+ source.removeFeature(feature);
+ expect(source.getFeatureByUid(uid)).to.be(null);
+ });
+
+ it('returns null after clear', function() {
+ const feature = new Feature();
+ feature.setId('abcd');
+ source.addFeature(feature);
+ const uid = getUid(feature);
+ expect(source.getFeatureByUid(uid)).to.be(feature);
+ source.clear();
+ expect(source.getFeatureByUid(uid)).to.be(null);
+ });
+
+ it('returns null when no features are present', function() {
+ expect(source.getFeatureByUid('abcd')).to.be(null);
+ });
+
+ });
+
describe('#loadFeatures', function() {
describe('with the "bbox" strategy', function() {
diff --git a/test/spec/ol/webgl/helper.test.js b/test/spec/ol/webgl/helper.test.js
index c474941c60..e53f4c2d0d 100644
--- a/test/spec/ol/webgl/helper.test.js
+++ b/test/spec/ol/webgl/helper.test.js
@@ -220,5 +220,74 @@ describe('ol.webgl.WebGLHelper', function() {
});
});
+ describe('#createTexture', function() {
+ let h;
+ beforeEach(function() {
+ h = new WebGLHelper();
+ });
+
+ it('creates an empty texture from scratch', function() {
+ const width = 4;
+ const height = 4;
+ const t = h.createTexture([width, height]);
+ const gl = h.getGL();
+
+ const fb = gl.createFramebuffer();
+ gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, t, 0);
+ const data = new Uint8Array(width * height * 4);
+ gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, data);
+ gl.deleteFramebuffer(fb);
+
+ expect(data[0]).to.eql(0);
+ expect(data[1]).to.eql(0);
+ expect(data[2]).to.eql(0);
+ expect(data[3]).to.eql(0);
+ expect(data[4]).to.eql(0);
+ expect(data[5]).to.eql(0);
+ expect(data[6]).to.eql(0);
+ expect(data[7]).to.eql(0);
+ });
+
+ it('creates a texture from image data', function() {
+ const width = 4;
+ const height = 4;
+ const canvas = document.createElement('canvas');
+ const image = canvas.getContext('2d').createImageData(width, height);
+ for (let i = 0; i < image.data.length; i += 4) {
+ image.data[i] = 100;
+ image.data[i + 1] = 150;
+ image.data[i + 2] = 200;
+ image.data[i + 3] = 250;
+ }
+ const t = h.createTexture([width, height], image);
+ const gl = h.getGL();
+
+ const fb = gl.createFramebuffer();
+ gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, t, 0);
+ const data = new Uint8Array(width * height * 4);
+ gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, data);
+ gl.deleteFramebuffer(fb);
+
+ expect(data[0]).to.eql(100);
+ expect(data[1]).to.eql(150);
+ expect(data[2]).to.eql(200);
+ expect(data[3]).to.eql(250);
+ expect(data[4]).to.eql(100);
+ expect(data[5]).to.eql(150);
+ expect(data[6]).to.eql(200);
+ expect(data[7]).to.eql(250);
+ });
+
+ it('reuses a given texture', function() {
+ const width = 4;
+ const height = 4;
+ const gl = h.getGL();
+ const t1 = gl.createTexture();
+ const t2 = h.createTexture([width, height], undefined, t1);
+ expect(t1).to.be(t2);
+ });
+ });
});
});
diff --git a/test/spec/ol/webgl/rendertarget.test.js b/test/spec/ol/webgl/rendertarget.test.js
new file mode 100644
index 0000000000..3468b16c26
--- /dev/null
+++ b/test/spec/ol/webgl/rendertarget.test.js
@@ -0,0 +1,128 @@
+import WebGLRenderTarget from '../../../../src/ol/webgl/RenderTarget.js';
+import WebGLHelper from '../../../../src/ol/webgl/Helper.js';
+
+
+describe('ol.webgl.RenderTarget', function() {
+ let helper, testImage_4x4;
+
+ beforeEach(function() {
+ helper = new WebGLHelper();
+
+ const canvas = document.createElement('canvas');
+ testImage_4x4 = canvas.getContext('2d').createImageData(4, 4);
+ for (let i = 0; i < testImage_4x4.data.length; i += 4) {
+ testImage_4x4.data[i] = 100 + i / 4;
+ testImage_4x4.data[i + 1] = 100 + i / 4;
+ testImage_4x4.data[i + 2] = 200 + i / 4;
+ testImage_4x4.data[i + 3] = 200 + i / 4;
+ }
+ });
+
+ describe('constructor', function() {
+
+ it('creates a target of size 1x1', function() {
+ const rt = new WebGLRenderTarget(helper);
+ expect(rt.getSize()).to.eql([1, 1]);
+ });
+
+ it('creates a target of specified size', function() {
+ const rt = new WebGLRenderTarget(helper, [12, 34]);
+ expect(rt.getSize()).to.eql([12, 34]);
+ });
+
+ });
+
+ describe('#setSize', function() {
+
+ it('updates the target size', function() {
+ const rt = new WebGLRenderTarget(helper, [12, 34]);
+ expect(rt.getSize()).to.eql([12, 34]);
+ rt.setSize([45, 67]);
+ expect(rt.getSize()).to.eql([45, 67]);
+ });
+
+ it('does nothing if the size has not changed', function() {
+ const rt = new WebGLRenderTarget(helper, [12, 34]);
+ const spy = sinon.spy(rt, 'updateSize_');
+ rt.setSize([12, 34]);
+ expect(spy.called).to.be(false);
+ rt.setSize([12, 345]);
+ expect(spy.called).to.be(true);
+ });
+
+ });
+
+ describe('#readAll', function() {
+
+ it('returns 1-pixel data with the default options', function() {
+ const rt = new WebGLRenderTarget(helper);
+ expect(rt.readAll().length).to.eql(4);
+ });
+
+ it('returns the content of the texture', function() {
+ const rt = new WebGLRenderTarget(helper, [4, 4]);
+ helper.createTexture([4, 4], testImage_4x4, rt.getTexture());
+ const data = rt.readAll();
+
+ expect(data[0]).to.eql(100);
+ expect(data[1]).to.eql(100);
+ expect(data[2]).to.eql(200);
+ expect(data[3]).to.eql(200);
+ expect(data[4]).to.eql(101);
+ expect(data[5]).to.eql(101);
+ expect(data[6]).to.eql(201);
+ expect(data[7]).to.eql(201);
+ expect(data.length).to.eql(4 * 4 * 4);
+ });
+
+ it('does not call gl.readPixels again when #clearCachedData is not called', function() {
+ const rt = new WebGLRenderTarget(helper, [4, 4]);
+ helper.createTexture([4, 4], testImage_4x4, rt.getTexture());
+ const spy = sinon.spy(rt.helper_.getGL(), 'readPixels');
+ rt.readAll();
+ expect(spy.callCount).to.eql(1);
+ rt.readAll();
+ expect(spy.callCount).to.eql(1);
+ rt.clearCachedData();
+ rt.readAll();
+ expect(spy.callCount).to.eql(2);
+ });
+
+ });
+
+ describe('#readPixel', function() {
+
+ it('returns the content of one pixel', function() {
+ const rt = new WebGLRenderTarget(helper, [4, 4]);
+ helper.createTexture([4, 4], testImage_4x4, rt.getTexture());
+
+ let data = rt.readPixel(0, 0);
+ expect(data[0]).to.eql(112);
+ expect(data[1]).to.eql(112);
+ expect(data[2]).to.eql(212);
+ expect(data[3]).to.eql(212);
+
+ data = rt.readPixel(3, 3);
+ expect(data[0]).to.eql(103);
+ expect(data[1]).to.eql(103);
+ expect(data[2]).to.eql(203);
+ expect(data[3]).to.eql(203);
+ expect(data.length).to.eql(4);
+ });
+
+ it('does not call gl.readPixels again when #clearCachedData is not called', function() {
+ const rt = new WebGLRenderTarget(helper, [4, 4]);
+ helper.createTexture([4, 4], testImage_4x4, rt.getTexture());
+ const spy = sinon.spy(rt.helper_.getGL(), 'readPixels');
+ rt.readPixel(0, 0);
+ expect(spy.callCount).to.eql(1);
+ rt.readPixel(1, 1);
+ expect(spy.callCount).to.eql(1);
+ rt.clearCachedData();
+ rt.readPixel(2, 2);
+ expect(spy.callCount).to.eql(2);
+ });
+
+ });
+
+});