Merge pull request #9655 from jahow/webgl-interaction
Add hit detection on the WebGL points renderer
This commit is contained in:
@@ -18,3 +18,4 @@ cloak:
|
|||||||
value: Your Mapbox access token from https://mapbox.com/ here
|
value: Your Mapbox access token from https://mapbox.com/ here
|
||||||
---
|
---
|
||||||
<div id="map" class="map"></div>
|
<div id="map" class="map"></div>
|
||||||
|
<div>Current sighting: <span id="info"></span></div>
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ function loadData() {
|
|||||||
|
|
||||||
loadData();
|
loadData();
|
||||||
|
|
||||||
new Map({
|
const map = new Map({
|
||||||
layers: [
|
layers: [
|
||||||
new TileLayer({
|
new TileLayer({
|
||||||
source: new TileJSON({
|
source: new TileJSON({
|
||||||
@@ -121,3 +121,18 @@ new Map({
|
|||||||
zoom: 2
|
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.';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -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
|
* Load the image if not already loaded, and register the image change
|
||||||
* listener if needed.
|
* listener if needed.
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ class WebGLLayerRenderer extends LayerRenderer {
|
|||||||
getShaderCompileErrors() {
|
getShaderCompileErrors() {
|
||||||
return this.helper.getShaderCompileErrors();
|
return this.helper.getShaderCompileErrors();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -231,11 +232,47 @@ export function writePointFeatureToBuffers(instructions, elementIndex, vertexBuf
|
|||||||
* @private
|
* @private
|
||||||
* @return {ImageData} Image data.
|
* @return {ImageData} Image data.
|
||||||
*/
|
*/
|
||||||
export function getBlankTexture() {
|
export function getBlankImageData() {
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
const image = canvas.getContext('2d').createImageData(1, 1);
|
const image = canvas.getContext('2d').createImageData(1, 1);
|
||||||
image.data[0] = image.data[1] = image.data[2] = image.data[3] = 255;
|
image.data[0] = image.data[1] = image.data[2] = image.data[3] = 255;
|
||||||
return image;
|
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;
|
export default WebGLLayerRenderer;
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import {DYNAMIC_DRAW, ARRAY_BUFFER, ELEMENT_ARRAY_BUFFER, FLOAT} from '../../web
|
|||||||
import {DefaultAttrib, DefaultUniform} from '../../webgl/Helper.js';
|
import {DefaultAttrib, DefaultUniform} from '../../webgl/Helper.js';
|
||||||
import GeometryType from '../../geom/GeometryType.js';
|
import GeometryType from '../../geom/GeometryType.js';
|
||||||
import WebGLLayerRenderer, {
|
import WebGLLayerRenderer, {
|
||||||
getBlankTexture,
|
colorDecodeId,
|
||||||
|
colorEncodeId,
|
||||||
|
getBlankImageData,
|
||||||
POINT_INSTRUCTIONS_COUNT, POINT_VERTEX_STRIDE, WebGLWorkerMessageType,
|
POINT_INSTRUCTIONS_COUNT, POINT_VERTEX_STRIDE, WebGLWorkerMessageType,
|
||||||
writePointFeatureInstructions
|
writePointFeatureInstructions
|
||||||
} from './Layer.js';
|
} from './Layer.js';
|
||||||
@@ -19,6 +21,8 @@ import {
|
|||||||
apply as applyTransform
|
apply as applyTransform
|
||||||
} from '../../transform.js';
|
} from '../../transform.js';
|
||||||
import {create as createWebGLWorker} from '../../worker/webgl.js';
|
import {create as createWebGLWorker} from '../../worker/webgl.js';
|
||||||
|
import {getUid} from '../../util.js';
|
||||||
|
import WebGLRenderTarget from '../../webgl/RenderTarget.js';
|
||||||
|
|
||||||
const VERTEX_SHADER = `
|
const VERTEX_SHADER = `
|
||||||
precision mediump float;
|
precision mediump float;
|
||||||
@@ -68,6 +72,26 @@ const FRAGMENT_SHADER = `
|
|||||||
gl_FragColor.rgb *= gl_FragColor.a;
|
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
|
* @typedef {Object} Options
|
||||||
* @property {function(import("../../Feature").default):number} [sizeCallback] Will be called on every feature in the
|
* @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 options = opt_options || {};
|
||||||
|
|
||||||
const uniforms = options.uniforms || {};
|
const uniforms = options.uniforms || {};
|
||||||
uniforms.u_texture = options.texture || getBlankTexture();
|
uniforms.u_texture = options.texture || getBlankImageData();
|
||||||
const projectionMatrixTransform = createTransform();
|
const projectionMatrixTransform = createTransform();
|
||||||
uniforms[DefaultUniform.PROJECTION_MATRIX] = projectionMatrixTransform;
|
uniforms[DefaultUniform.PROJECTION_MATRIX] = projectionMatrixTransform;
|
||||||
|
|
||||||
@@ -212,14 +236,17 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
|
|||||||
this.sourceRevision_ = -1;
|
this.sourceRevision_ = -1;
|
||||||
|
|
||||||
this.verticesBuffer_ = new WebGLArrayBuffer(ARRAY_BUFFER, DYNAMIC_DRAW);
|
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.indicesBuffer_ = new WebGLArrayBuffer(ELEMENT_ARRAY_BUFFER, DYNAMIC_DRAW);
|
||||||
|
|
||||||
this.program_ = this.helper.getProgram(
|
this.program_ = this.helper.getProgram(
|
||||||
options.fragmentShader || FRAGMENT_SHADER,
|
options.fragmentShader || FRAGMENT_SHADER,
|
||||||
options.vertexShader || VERTEX_SHADER
|
options.vertexShader || VERTEX_SHADER
|
||||||
);
|
);
|
||||||
|
this.hitProgram_ = this.helper.getProgram(
|
||||||
this.helper.useProgram(this.program_);
|
HIT_FRAGMENT_SHADER,
|
||||||
|
options.vertexShader || VERTEX_SHADER
|
||||||
|
);
|
||||||
|
|
||||||
this.sizeCallback_ = options.sizeCallback || function() {
|
this.sizeCallback_ = options.sizeCallback || function() {
|
||||||
return 1;
|
return 1;
|
||||||
@@ -274,21 +301,55 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
|
|||||||
*/
|
*/
|
||||||
this.renderInstructions_ = new Float32Array(0);
|
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_ = createWebGLWorker();
|
||||||
this.worker_.addEventListener('message', function(event) {
|
this.worker_.addEventListener('message', function(event) {
|
||||||
const received = event.data;
|
const received = event.data;
|
||||||
if (received.type === WebGLWorkerMessageType.GENERATE_BUFFERS) {
|
if (received.type === WebGLWorkerMessageType.GENERATE_BUFFERS) {
|
||||||
const projectionTransform = received.projectionTransform;
|
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.indicesBuffer_.fromArrayBuffer(received.indexBuffer);
|
||||||
this.helper.flushBufferData(this.verticesBuffer_);
|
|
||||||
this.helper.flushBufferData(this.indicesBuffer_);
|
this.helper.flushBufferData(this.indicesBuffer_);
|
||||||
|
|
||||||
// saves the projection transform for the current frame state
|
if (received.hitDetection) {
|
||||||
this.renderTransform_ = projectionTransform;
|
this.hitRenderTransform_ = projectionTransform;
|
||||||
makeInverseTransform(this.invertRenderTransform_, this.renderTransform_);
|
makeInverseTransform(this.invertHitRenderTransform_, this.hitRenderTransform_);
|
||||||
|
this.hitRenderInstructions_ = new Float32Array(event.data.renderInstructions);
|
||||||
this.renderInstructions_ = new Float32Array(event.data.renderInstructions);
|
} else {
|
||||||
|
this.renderTransform_ = projectionTransform;
|
||||||
|
makeInverseTransform(this.invertRenderTransform_, this.renderTransform_);
|
||||||
|
this.renderInstructions_ = new Float32Array(event.data.renderInstructions);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
}
|
}
|
||||||
@@ -315,6 +376,9 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
|
|||||||
canvas.style.opacity = opacity;
|
canvas.style.opacity = opacity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.renderHitDetection(frameState);
|
||||||
|
this.hitRenderTarget_.clearCachedData();
|
||||||
|
|
||||||
return canvas;
|
return canvas;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,6 +413,7 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
|
|||||||
this.helper.makeProjectionTransform(frameState, this.currentTransform_);
|
this.helper.makeProjectionTransform(frameState, this.currentTransform_);
|
||||||
multiplyTransform(this.currentTransform_, this.invertRenderTransform_);
|
multiplyTransform(this.currentTransform_, this.invertRenderTransform_);
|
||||||
|
|
||||||
|
this.helper.useProgram(this.program_);
|
||||||
this.helper.prepareDraw(frameState);
|
this.helper.prepareDraw(frameState);
|
||||||
|
|
||||||
// write new data
|
// write new data
|
||||||
@@ -384,11 +449,16 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
|
|||||||
if (!this.renderInstructions_ || this.renderInstructions_.length !== totalInstructionsCount) {
|
if (!this.renderInstructions_ || this.renderInstructions_.length !== totalInstructionsCount) {
|
||||||
this.renderInstructions_ = new Float32Array(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
|
// loop on features to fill the buffer
|
||||||
let feature;
|
let feature;
|
||||||
const tmpCoords = [];
|
const tmpCoords = [];
|
||||||
|
const tmpColor = [];
|
||||||
let elementIndex = 0;
|
let elementIndex = 0;
|
||||||
|
let u0, v0, u1, v1, size, opacity, rotateWithView, color;
|
||||||
for (let i = 0; i < features.length; i++) {
|
for (let i = 0; i < features.length; i++) {
|
||||||
feature = features[i];
|
feature = features[i];
|
||||||
if (!feature.getGeometry() || feature.getGeometry().getType() !== GeometryType.POINT) {
|
if (!feature.getGeometry() || feature.getGeometry().getType() !== GeometryType.POINT) {
|
||||||
@@ -399,19 +469,45 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
|
|||||||
tmpCoords[1] = this.coordCallback_(feature, 1);
|
tmpCoords[1] = this.coordCallback_(feature, 1);
|
||||||
applyTransform(projectionTransform, tmpCoords);
|
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_,
|
this.renderInstructions_,
|
||||||
elementIndex,
|
elementIndex,
|
||||||
tmpCoords[0],
|
tmpCoords[0],
|
||||||
tmpCoords[1],
|
tmpCoords[1],
|
||||||
this.texCoordCallback_(feature, 0),
|
u0,
|
||||||
this.texCoordCallback_(feature, 1),
|
v0,
|
||||||
this.texCoordCallback_(feature, 2),
|
u1,
|
||||||
this.texCoordCallback_(feature, 3),
|
v1,
|
||||||
this.sizeCallback_(feature),
|
size,
|
||||||
this.opacityCallback_(feature),
|
opacity,
|
||||||
this.rotateWithViewCallback_(feature),
|
rotateWithView,
|
||||||
this.colorCallback_(feature, this.colorArray_)
|
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
|
// additional properties will be sent back as-is by the worker
|
||||||
message['projectionTransform'] = projectionTransform;
|
message['projectionTransform'] = projectionTransform;
|
||||||
|
|
||||||
this.worker_.postMessage(message, [this.renderInstructions_.buffer]);
|
this.worker_.postMessage(message, [this.renderInstructions_.buffer]);
|
||||||
this.renderInstructions_ = null;
|
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;
|
export default WebGLPointsLayerRenderer;
|
||||||
|
|||||||
+25
-20
@@ -241,11 +241,11 @@ class VectorSource extends Source {
|
|||||||
this.idIndex_ = {};
|
this.idIndex_ = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A lookup of features without id (keyed by getUid(feature)).
|
* A lookup of features by uid (using getUid(feature)).
|
||||||
* @private
|
* @private
|
||||||
* @type {!Object<string, import("../Feature.js").default<Geometry>>}
|
* @type {!Object<string, import("../Feature.js").default<Geometry>>}
|
||||||
*/
|
*/
|
||||||
this.undefIdIndex_ = {};
|
this.uidIndex_ = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
@@ -359,10 +359,11 @@ class VectorSource extends Source {
|
|||||||
} else {
|
} else {
|
||||||
valid = false;
|
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
|
30); // The passed `feature` was already added to the source
|
||||||
this.undefIdIndex_[featureKey] = feature;
|
this.uidIndex_[featureKey] = feature;
|
||||||
}
|
}
|
||||||
return valid;
|
return valid;
|
||||||
}
|
}
|
||||||
@@ -489,7 +490,7 @@ class VectorSource extends Source {
|
|||||||
if (!this.featuresCollection_) {
|
if (!this.featuresCollection_) {
|
||||||
this.featureChangeKeys_ = {};
|
this.featureChangeKeys_ = {};
|
||||||
this.idIndex_ = {};
|
this.idIndex_ = {};
|
||||||
this.undefIdIndex_ = {};
|
this.uidIndex_ = {};
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (this.featuresRtree_) {
|
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.
|
* Get the format associated with this source.
|
||||||
*
|
*
|
||||||
@@ -831,20 +844,13 @@ class VectorSource extends Source {
|
|||||||
const id = feature.getId();
|
const id = feature.getId();
|
||||||
if (id !== undefined) {
|
if (id !== undefined) {
|
||||||
const sid = id.toString();
|
const sid = id.toString();
|
||||||
if (featureKey in this.undefIdIndex_) {
|
if (this.idIndex_[sid] !== feature) {
|
||||||
delete this.undefIdIndex_[featureKey];
|
this.removeFromIdIndex_(feature);
|
||||||
this.idIndex_[sid] = feature;
|
this.idIndex_[sid] = feature;
|
||||||
} else {
|
|
||||||
if (this.idIndex_[sid] !== feature) {
|
|
||||||
this.removeFromIdIndex_(feature);
|
|
||||||
this.idIndex_[sid] = feature;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!(featureKey in this.undefIdIndex_)) {
|
this.removeFromIdIndex_(feature);
|
||||||
this.removeFromIdIndex_(feature);
|
this.uidIndex_[featureKey] = feature;
|
||||||
this.undefIdIndex_[featureKey] = feature;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.changed();
|
this.changed();
|
||||||
this.dispatchEvent(new VectorSourceEvent(
|
this.dispatchEvent(new VectorSourceEvent(
|
||||||
@@ -862,7 +868,7 @@ class VectorSource extends Source {
|
|||||||
if (id !== undefined) {
|
if (id !== undefined) {
|
||||||
return id in this.idIndex_;
|
return id in this.idIndex_;
|
||||||
} else {
|
} 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();
|
const id = feature.getId();
|
||||||
if (id !== undefined) {
|
if (id !== undefined) {
|
||||||
delete this.idIndex_[id.toString()];
|
delete this.idIndex_[id.toString()];
|
||||||
} else {
|
|
||||||
delete this.undefIdIndex_[featureKey];
|
|
||||||
}
|
}
|
||||||
|
delete this.uidIndex_[featureKey];
|
||||||
this.dispatchEvent(new VectorSourceEvent(
|
this.dispatchEvent(new VectorSourceEvent(
|
||||||
VectorEventType.REMOVEFEATURE, feature));
|
VectorEventType.REMOVEFEATURE, feature));
|
||||||
}
|
}
|
||||||
|
|||||||
+49
-46
@@ -432,6 +432,30 @@ class WebGLHelper extends Disposable {
|
|||||||
gl.enable(gl.BLEND);
|
gl.enable(gl.BLEND);
|
||||||
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
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.applyFrameState(frameState);
|
||||||
this.applyUniforms(frameState);
|
this.applyUniforms(frameState);
|
||||||
}
|
}
|
||||||
@@ -742,57 +766,36 @@ class WebGLHelper extends Disposable {
|
|||||||
// TODO: shutdown program
|
// TODO: shutdown program
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: these are not used and should be reworked
|
* Will create or reuse a given webgl texture and apply the given size. If no image data
|
||||||
* @param {number=} opt_wrapS wrapS.
|
* specified, the texture will be empty, otherwise image data will be used and the `size`
|
||||||
* @param {number=} opt_wrapT wrapT.
|
* parameter will be ignored.
|
||||||
* @return {WebGLTexture} The texture.
|
* 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 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.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_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;
|
return texture;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import WebGLLayerRenderer, {
|
import WebGLLayerRenderer, {
|
||||||
getBlankTexture, POINT_INSTRUCTIONS_COUNT, POINT_VERTEX_STRIDE,
|
colorDecodeId,
|
||||||
|
colorEncodeId,
|
||||||
|
getBlankImageData, POINT_INSTRUCTIONS_COUNT, POINT_VERTEX_STRIDE,
|
||||||
writePointFeatureInstructions, writePointFeatureToBuffers
|
writePointFeatureInstructions, writePointFeatureToBuffers
|
||||||
} from '../../../../../src/ol/renderer/webgl/Layer.js';
|
} from '../../../../../src/ol/renderer/webgl/Layer.js';
|
||||||
import Layer from '../../../../../src/ol/layer/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() {
|
it('creates a 1x1 white texture', function() {
|
||||||
const texture = getBlankTexture();
|
const texture = getBlankImageData();
|
||||||
expect(texture.height).to.eql(1);
|
expect(texture.height).to.eql(1);
|
||||||
expect(texture.width).to.eql(1);
|
expect(texture.width).to.eql(1);
|
||||||
expect(texture.data[0]).to.eql(255);
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import WebGLPointsLayerRenderer from '../../../../../src/ol/renderer/webgl/Point
|
|||||||
import {get as getProjection} from '../../../../../src/ol/proj.js';
|
import {get as getProjection} from '../../../../../src/ol/proj.js';
|
||||||
import ViewHint from '../../../../../src/ol/ViewHint.js';
|
import ViewHint from '../../../../../src/ol/ViewHint.js';
|
||||||
import {POINT_VERTEX_STRIDE, WebGLWorkerMessageType} from '../../../../../src/ol/renderer/webgl/Layer.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() {
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 {get as getProjection, transformExtent, fromLonLat} from '../../../../src/ol/proj.js';
|
||||||
import VectorSource from '../../../../src/ol/source/Vector.js';
|
import VectorSource from '../../../../src/ol/source/Vector.js';
|
||||||
import GeoJSON from '../../../../src/ol/format/GeoJSON.js';
|
import GeoJSON from '../../../../src/ol/format/GeoJSON.js';
|
||||||
|
import {getUid} from '../../../../src/ol/util.js';
|
||||||
|
|
||||||
|
|
||||||
describe('ol.source.Vector', function() {
|
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('#loadFeatures', function() {
|
||||||
|
|
||||||
describe('with the "bbox" strategy', function() {
|
describe('with the "bbox" strategy', function() {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user