This means any specified loading strategy will be taken into account. Also added some tests to make sure the interaction with the source is correct.
392 lines
12 KiB
JavaScript
392 lines
12 KiB
JavaScript
import Feature from '../../../../../src/ol/Feature.js';
|
|
import Point from '../../../../../src/ol/geom/Point.js';
|
|
import VectorLayer from '../../../../../src/ol/layer/Vector.js';
|
|
import VectorSource from '../../../../../src/ol/source/Vector.js';
|
|
import WebGLPointsLayerRenderer from '../../../../../src/ol/renderer/webgl/PointsLayer.js';
|
|
import {get as getProjection} from '../../../../../src/ol/proj.js';
|
|
import ViewHint from '../../../../../src/ol/ViewHint.js';
|
|
import {WebGLWorkerMessageType} from '../../../../../src/ol/renderer/webgl/Layer.js';
|
|
import {compose as composeTransform, create as createTransform} from '../../../../../src/ol/transform.js';
|
|
|
|
const baseFrameState = {
|
|
viewHints: [],
|
|
viewState: {
|
|
projection: getProjection('EPSG:3857'),
|
|
resolution: 1,
|
|
rotation: 0,
|
|
center: [0, 0]
|
|
},
|
|
layerStatesArray: [{}],
|
|
layerIndex: 0,
|
|
pixelRatio: 1
|
|
};
|
|
|
|
const simpleVertexShader = `
|
|
precision mediump float;
|
|
uniform mat4 u_projectionMatrix;
|
|
uniform mat4 u_offsetScaleMatrix;
|
|
attribute vec2 a_position;
|
|
attribute float a_index;
|
|
|
|
void main(void) {
|
|
mat4 offsetMatrix = u_offsetScaleMatrix;
|
|
float offsetX = a_index == 0.0 || a_index == 3.0 ? -2.0 : 2.0;
|
|
float offsetY = a_index == 0.0 || a_index == 1.0 ? -2.0 : 2.0;
|
|
vec4 offsets = offsetMatrix * vec4(offsetX, offsetY, 0.0, 0.0);
|
|
gl_Position = u_projectionMatrix * vec4(a_position, 0.0, 1.0) + offsets;
|
|
}`;
|
|
const simpleFragmentShader = `
|
|
precision mediump float;
|
|
|
|
void main(void) {
|
|
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
|
|
}`;
|
|
|
|
// these shaders support hit detection
|
|
// they have a built-in size value of 4
|
|
const hitVertexShader = `
|
|
precision mediump float;
|
|
uniform mat4 u_projectionMatrix;
|
|
uniform mat4 u_offsetScaleMatrix;
|
|
attribute vec2 a_position;
|
|
attribute float a_index;
|
|
attribute vec4 a_hitColor;
|
|
varying vec4 v_hitColor;
|
|
|
|
void main(void) {
|
|
mat4 offsetMatrix = u_offsetScaleMatrix;
|
|
float offsetX = a_index == 0.0 || a_index == 3.0 ? -2.0 : 2.0;
|
|
float offsetY = a_index == 0.0 || a_index == 1.0 ? -2.0 : 2.0;
|
|
vec4 offsets = offsetMatrix * vec4(offsetX, offsetY, 0.0, 0.0);
|
|
gl_Position = u_projectionMatrix * vec4(a_position, 0.0, 1.0) + offsets;
|
|
v_hitColor = a_hitColor;
|
|
}`;
|
|
const hitFragmentShader = `
|
|
precision mediump float;
|
|
varying vec4 v_hitColor;
|
|
|
|
void main(void) {
|
|
gl_FragColor = v_hitColor;
|
|
}`;
|
|
|
|
describe('ol.renderer.webgl.PointsLayer', function() {
|
|
|
|
describe('constructor', function() {
|
|
|
|
let target;
|
|
|
|
beforeEach(function() {
|
|
target = document.createElement('div');
|
|
target.style.width = '256px';
|
|
target.style.height = '256px';
|
|
document.body.appendChild(target);
|
|
});
|
|
|
|
afterEach(function() {
|
|
document.body.removeChild(target);
|
|
});
|
|
|
|
it('creates a new instance', function() {
|
|
const layer = new VectorLayer({
|
|
source: new VectorSource()
|
|
});
|
|
const renderer = new WebGLPointsLayerRenderer(layer, {
|
|
vertexShader: simpleVertexShader,
|
|
fragmentShader: simpleFragmentShader
|
|
});
|
|
expect(renderer).to.be.a(WebGLPointsLayerRenderer);
|
|
});
|
|
|
|
});
|
|
|
|
describe('#prepareFrame', function() {
|
|
let layer, renderer, frameState;
|
|
|
|
beforeEach(function() {
|
|
layer = new VectorLayer({
|
|
source: new VectorSource(),
|
|
renderBuffer: 10
|
|
});
|
|
renderer = new WebGLPointsLayerRenderer(layer, {
|
|
vertexShader: simpleVertexShader,
|
|
fragmentShader: simpleFragmentShader,
|
|
hitVertexShader: hitVertexShader,
|
|
hitFragmentShader: hitFragmentShader
|
|
});
|
|
frameState = Object.assign({
|
|
size: [2, 2],
|
|
extent: [-100, -100, 100, 100]
|
|
}, baseFrameState);
|
|
});
|
|
|
|
it('calls WebGlHelper#prepareDraw', function() {
|
|
const spy = sinon.spy(renderer.helper, 'prepareDraw');
|
|
renderer.prepareFrame(frameState);
|
|
expect(spy.called).to.be(true);
|
|
});
|
|
|
|
it('fills up a buffer with 2 triangles per point', function(done) {
|
|
layer.getSource().addFeature(new Feature({
|
|
geometry: new Point([10, 20])
|
|
}));
|
|
layer.getSource().addFeature(new Feature({
|
|
geometry: new Point([30, 40])
|
|
}));
|
|
renderer.prepareFrame(frameState);
|
|
|
|
const attributePerVertex = 3;
|
|
|
|
renderer.worker_.addEventListener('message', function(event) {
|
|
if (event.data.type !== WebGLWorkerMessageType.GENERATE_BUFFERS) {
|
|
return;
|
|
}
|
|
expect(renderer.verticesBuffer_.getArray().length).to.eql(2 * 4 * attributePerVertex);
|
|
expect(renderer.indicesBuffer_.getArray().length).to.eql(2 * 6);
|
|
|
|
expect(renderer.verticesBuffer_.getArray()[0]).to.eql(10);
|
|
expect(renderer.verticesBuffer_.getArray()[1]).to.eql(20);
|
|
expect(renderer.verticesBuffer_.getArray()[4 * attributePerVertex + 0]).to.eql(30);
|
|
expect(renderer.verticesBuffer_.getArray()[4 * attributePerVertex + 1]).to.eql(40);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('fills up the hit render buffer with 2 triangles per point', function(done) {
|
|
layer.getSource().addFeature(new Feature({
|
|
geometry: new Point([10, 20])
|
|
}));
|
|
layer.getSource().addFeature(new Feature({
|
|
geometry: new Point([30, 40])
|
|
}));
|
|
renderer.prepareFrame(frameState);
|
|
|
|
const attributePerVertex = 8;
|
|
|
|
renderer.worker_.addEventListener('message', function(event) {
|
|
if (event.data.type !== WebGLWorkerMessageType.GENERATE_BUFFERS) {
|
|
return;
|
|
}
|
|
if (!renderer.hitVerticesBuffer_.getArray()) {
|
|
return;
|
|
}
|
|
expect(renderer.hitVerticesBuffer_.getArray().length).to.eql(2 * 4 * attributePerVertex);
|
|
expect(renderer.indicesBuffer_.getArray().length).to.eql(2 * 6);
|
|
|
|
expect(renderer.hitVerticesBuffer_.getArray()[0]).to.eql(10);
|
|
expect(renderer.hitVerticesBuffer_.getArray()[1]).to.eql(20);
|
|
expect(renderer.hitVerticesBuffer_.getArray()[4 * attributePerVertex + 0]).to.eql(30);
|
|
expect(renderer.hitVerticesBuffer_.getArray()[4 * attributePerVertex + 1]).to.eql(40);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('clears the buffers when the features are gone', function(done) {
|
|
const source = layer.getSource();
|
|
source.addFeature(new Feature({
|
|
geometry: new Point([10, 20])
|
|
}));
|
|
source.removeFeature(source.getFeatures()[0]);
|
|
source.addFeature(new Feature({
|
|
geometry: new Point([10, 20])
|
|
}));
|
|
renderer.prepareFrame(frameState);
|
|
|
|
renderer.worker_.addEventListener('message', function(event) {
|
|
if (event.data.type !== WebGLWorkerMessageType.GENERATE_BUFFERS) {
|
|
return;
|
|
}
|
|
const attributePerVertex = 3;
|
|
expect(renderer.verticesBuffer_.getArray().length).to.eql(4 * attributePerVertex);
|
|
expect(renderer.indicesBuffer_.getArray().length).to.eql(6);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('rebuilds the buffers only when not interacting or animating', function() {
|
|
const spy = sinon.spy(renderer, 'rebuildBuffers_');
|
|
|
|
frameState.viewHints[ViewHint.INTERACTING] = 1;
|
|
frameState.viewHints[ViewHint.ANIMATING] = 0;
|
|
renderer.prepareFrame(frameState);
|
|
expect(spy.called).to.be(false);
|
|
|
|
frameState.viewHints[ViewHint.INTERACTING] = 0;
|
|
frameState.viewHints[ViewHint.ANIMATING] = 1;
|
|
renderer.prepareFrame(frameState);
|
|
expect(spy.called).to.be(false);
|
|
|
|
frameState.viewHints[ViewHint.INTERACTING] = 0;
|
|
frameState.viewHints[ViewHint.ANIMATING] = 0;
|
|
renderer.prepareFrame(frameState);
|
|
expect(spy.called).to.be(true);
|
|
});
|
|
|
|
it('rebuilds the buffers only when the frame extent changed', function() {
|
|
const spy = sinon.spy(renderer, 'rebuildBuffers_');
|
|
|
|
renderer.prepareFrame(frameState);
|
|
expect(spy.callCount).to.be(1);
|
|
|
|
renderer.prepareFrame(frameState);
|
|
expect(spy.callCount).to.be(1);
|
|
|
|
frameState.extent = [10, 20, 30, 40];
|
|
renderer.prepareFrame(frameState);
|
|
expect(spy.callCount).to.be(2);
|
|
});
|
|
|
|
it('triggers source loading when the extent changes', function() {
|
|
const spy = sinon.spy(layer.getSource(), 'loadFeatures');
|
|
|
|
renderer.prepareFrame(frameState);
|
|
expect(spy.callCount).to.be(1);
|
|
|
|
renderer.prepareFrame(frameState);
|
|
expect(spy.callCount).to.be(1);
|
|
|
|
frameState.extent = [10, 20, 30, 40];
|
|
renderer.prepareFrame(frameState);
|
|
expect(spy.callCount).to.be(2);
|
|
expect(spy.getCall(1).args[0]).to.eql([0, 10, 40, 50]); // renderBuffer is 10
|
|
});
|
|
|
|
it('triggers source loading when the source revision changes', function() {
|
|
const spy = sinon.spy(layer.getSource(), 'loadFeatures');
|
|
|
|
renderer.prepareFrame(frameState);
|
|
expect(spy.callCount).to.be(1);
|
|
|
|
renderer.prepareFrame(frameState);
|
|
expect(spy.callCount).to.be(1);
|
|
|
|
layer.getSource().changed();
|
|
renderer.prepareFrame(frameState);
|
|
expect(spy.callCount).to.be(2);
|
|
});
|
|
});
|
|
|
|
describe('#forEachFeatureAtCoordinate', function() {
|
|
let layer, renderer, feature, feature2;
|
|
|
|
beforeEach(function() {
|
|
feature = new Feature({geometry: new Point([0, 0]), id: 1});
|
|
feature2 = new Feature({geometry: new Point([14, 14]), id: 2});
|
|
layer = new VectorLayer({
|
|
source: new VectorSource({
|
|
features: [feature, feature2]
|
|
})
|
|
});
|
|
renderer = new WebGLPointsLayerRenderer(layer, {
|
|
vertexShader: simpleVertexShader,
|
|
fragmentShader: simpleFragmentShader,
|
|
hitVertexShader: hitVertexShader,
|
|
hitFragmentShader: hitFragmentShader
|
|
});
|
|
});
|
|
|
|
it('correctly hit detects a feature', function(done) {
|
|
const transform = composeTransform(createTransform(), 20, 20, 1, -1, 0, 0, 0);
|
|
const frameState = Object.assign({
|
|
extent: [-20, -20, 20, 20],
|
|
size: [40, 40],
|
|
coordinateToPixelTransform: transform
|
|
}, baseFrameState);
|
|
let found;
|
|
const cb = function(feature) {
|
|
found = feature;
|
|
};
|
|
|
|
renderer.prepareFrame(frameState);
|
|
renderer.worker_.addEventListener('message', function() {
|
|
if (!renderer.hitRenderInstructions_) {
|
|
return;
|
|
}
|
|
renderer.prepareFrame(frameState);
|
|
renderer.renderFrame(frameState);
|
|
|
|
function checkHit(x, y, expected) {
|
|
found = null;
|
|
renderer.forEachFeatureAtCoordinate([x, y], frameState, 0, cb, null);
|
|
expect(found).to.be(expected);
|
|
}
|
|
|
|
checkHit(0, 0, feature);
|
|
checkHit(1, -1, feature);
|
|
checkHit(-2, 2, feature);
|
|
checkHit(2, 0, null);
|
|
checkHit(1, -3, null);
|
|
|
|
checkHit(14, 14, feature2);
|
|
checkHit(15, 13, feature2);
|
|
checkHit(12, 16, feature2);
|
|
checkHit(16, 14, null);
|
|
checkHit(13, 11, null);
|
|
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('correctly hit detects with pixelratio != 1', function(done) {
|
|
const transform = composeTransform(createTransform(), 20, 20, 1, -1, 0, 0, 0);
|
|
const frameState = Object.assign({
|
|
pixelRatio: 3,
|
|
extent: [-20, -20, 20, 20],
|
|
size: [40, 40],
|
|
coordinateToPixelTransform: transform
|
|
}, baseFrameState);
|
|
let found;
|
|
const cb = function(feature) {
|
|
found = feature;
|
|
};
|
|
|
|
renderer.prepareFrame(frameState);
|
|
renderer.worker_.addEventListener('message', function() {
|
|
if (!renderer.hitRenderInstructions_) {
|
|
return;
|
|
}
|
|
renderer.prepareFrame(frameState);
|
|
renderer.renderFrame(frameState);
|
|
|
|
function checkHit(x, y, expected) {
|
|
found = null;
|
|
renderer.forEachFeatureAtCoordinate([x, y], frameState, 0, cb, null);
|
|
expect(found).to.be(expected);
|
|
}
|
|
|
|
checkHit(0, 0, feature);
|
|
checkHit(1, -1, feature);
|
|
checkHit(-2, 2, feature);
|
|
checkHit(2, 0, null);
|
|
checkHit(1, -3, null);
|
|
|
|
checkHit(14, 14, feature2);
|
|
checkHit(15, 13, feature2);
|
|
checkHit(12, 16, feature2);
|
|
checkHit(16, 14, null);
|
|
checkHit(13, 11, null);
|
|
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('#disposeInternal', function() {
|
|
it('terminates the worker and calls dispose on the helper', function() {
|
|
const layer = new VectorLayer({
|
|
source: new VectorSource()
|
|
});
|
|
const renderer = new WebGLPointsLayerRenderer(layer, {
|
|
vertexShader: simpleVertexShader,
|
|
fragmentShader: simpleFragmentShader
|
|
});
|
|
|
|
const spyHelper = sinon.spy(renderer.helper, 'disposeInternal');
|
|
const spyWorker = sinon.spy(renderer.worker_, 'terminate');
|
|
renderer.disposeInternal();
|
|
expect(spyHelper.called).to.be(true);
|
|
expect(spyWorker.called).to.be(true);
|
|
});
|
|
});
|
|
|
|
});
|