Merge pull request #9655 from jahow/webgl-interaction

Add hit detection on the WebGL points renderer
This commit is contained in:
Olivier Guyot
2019-06-28 11:22:03 +02:00
committed by GitHub
14 changed files with 829 additions and 149 deletions

View File

@@ -18,3 +18,4 @@ cloak:
value: Your Mapbox access token from https://mapbox.com/ here
---
<div id="map" class="map"></div>
<div>Current sighting: <span id="info"></span></div>

View File

@@ -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.';
});
});

View File

@@ -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.

View File

@@ -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<number>} [opt_array] Reusable array
* @return {Array<number>} 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<number>} 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;

View File

@@ -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;

View File

@@ -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<string, import("../Feature.js").default<Geometry>>}
*/
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<Geometry>} 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));
}

View File

@@ -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<number>} 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;
}
}

View File

@@ -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<number>} [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<number>}
* @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<number>} 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<number>} 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;

View File

@@ -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;

View File

@@ -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);
});
});
});

View File

@@ -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();
});
});
});
});

View File

@@ -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() {

View File

@@ -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);
});
});
});
});

View File

@@ -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);
});
});
});