Webgl points renderer / add a cache for features in the source
This allows quicker access to features as well as their geometries and properties, reducing the time taken by a rebuildBuffers call.
This commit is contained in:
@@ -19,13 +19,23 @@ import {getUid} from '../../util.js';
|
|||||||
import WebGLRenderTarget from '../../webgl/RenderTarget.js';
|
import WebGLRenderTarget from '../../webgl/RenderTarget.js';
|
||||||
import {assert} from '../../asserts.js';
|
import {assert} from '../../asserts.js';
|
||||||
import BaseVector from '../../layer/BaseVector.js';
|
import BaseVector from '../../layer/BaseVector.js';
|
||||||
|
import {listen} from '../../events.js';
|
||||||
|
import VectorEventType from '../../source/VectorEventType.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} CustomAttribute A description of a custom attribute to be passed on to the GPU, with a value different
|
* @typedef {Object} CustomAttribute A description of a custom attribute to be passed on to the GPU, with a value different
|
||||||
* for each feature.
|
* for each feature.
|
||||||
* @property {string} name Attribute name.
|
* @property {string} name Attribute name.
|
||||||
* @property {function(import("../../Feature").default):number} callback This callback computes the numerical value of the
|
* @property {function(import("../../Feature").default, Object<string, *>):number} callback This callback computes the numerical value of the
|
||||||
* attribute for a given feature.
|
* attribute for a given feature (properties are available as 2nd arg for quicker access).
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} FeatureCacheItem Object that holds a reference to a feature, its geometry and properties. Used to optimize
|
||||||
|
* rebuildBuffers by accessing these objects quicker.
|
||||||
|
* @property {import("../../Feature").default} feature Feature
|
||||||
|
* @property {Object<string, *>} properties Feature properties
|
||||||
|
* @property {import("../../geom").Geometry} geometry Feature geometry
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -269,6 +279,50 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
|
|||||||
this.getLayer().changed();
|
this.getLayer().changed();
|
||||||
}
|
}
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This object will be updated when the source changes. Key is uid.
|
||||||
|
* @type {Object<string, FeatureCacheItem>}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this.featureCache_ = {};
|
||||||
|
|
||||||
|
const source = this.getLayer().getSource();
|
||||||
|
listen(source, VectorEventType.ADDFEATURE, this.handleSourceFeatureChanged_, this);
|
||||||
|
listen(source, VectorEventType.CHANGEFEATURE, this.handleSourceFeatureChanged_, this);
|
||||||
|
listen(source, VectorEventType.REMOVEFEATURE, this.handleSourceFeatureDelete_, this);
|
||||||
|
source.getFeatures().forEach(function(feature) {
|
||||||
|
const uid = getUid(feature);
|
||||||
|
this.featureCache_[uid] = {
|
||||||
|
feature: feature,
|
||||||
|
properties: feature.getProperties(),
|
||||||
|
geometry: feature.getGeometry()
|
||||||
|
};
|
||||||
|
}.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import("../../source/Vector.js").VectorSourceEvent} event Event.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
handleSourceFeatureChanged_(event) {
|
||||||
|
const feature = event.feature;
|
||||||
|
const uid = getUid(feature);
|
||||||
|
this.featureCache_[uid] = {
|
||||||
|
feature: feature,
|
||||||
|
properties: feature.getProperties(),
|
||||||
|
geometry: feature.getGeometry()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import("../../source/Vector.js").VectorSourceEvent} event Event.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
handleSourceFeatureDelete_(event) {
|
||||||
|
const feature = event.feature;
|
||||||
|
const uid = getUid(feature);
|
||||||
|
delete this.featureCache_[uid];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -343,45 +397,44 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
rebuildBuffers_(frameState) {
|
rebuildBuffers_(frameState) {
|
||||||
const layer = this.getLayer();
|
|
||||||
const vectorSource = layer.getSource();
|
|
||||||
|
|
||||||
// saves the projection transform for the current frame state
|
// saves the projection transform for the current frame state
|
||||||
const projectionTransform = createTransform();
|
const projectionTransform = createTransform();
|
||||||
this.helper.makeProjectionTransform(frameState, projectionTransform);
|
this.helper.makeProjectionTransform(frameState, projectionTransform);
|
||||||
|
|
||||||
const features = vectorSource.getFeatures();
|
const featureUids = Object.keys(this.featureCache_);
|
||||||
|
|
||||||
// here we anticipate the amount of render instructions that we well generate
|
// here we anticipate the amount of render instructions that we well generate
|
||||||
// this can be done since we know that for normal render we only have x, y as base instructions,
|
// this can be done since we know that for normal render we only have x, y as base instructions,
|
||||||
// and x, y, r, g, b, a and featureUid for hit render instructions
|
// and x, y, r, g, b, a and featureUid for hit render instructions
|
||||||
// and we also know the amount of custom attributes to append to these
|
// and we also know the amount of custom attributes to append to these
|
||||||
const totalInstructionsCount = (2 + this.customAttributes.length) * features.length;
|
const totalInstructionsCount = (2 + this.customAttributes.length) * featureUids.length;
|
||||||
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.hitDetectionEnabled_) {
|
if (this.hitDetectionEnabled_) {
|
||||||
const totalHitInstructionsCount = (7 + this.customAttributes.length) * features.length;
|
const totalHitInstructionsCount = (7 + this.customAttributes.length) * featureUids.length;
|
||||||
if (!this.hitRenderInstructions_ || this.hitRenderInstructions_.length !== totalHitInstructionsCount) {
|
if (!this.hitRenderInstructions_ || this.hitRenderInstructions_.length !== totalHitInstructionsCount) {
|
||||||
this.hitRenderInstructions_ = new Float32Array(totalHitInstructionsCount);
|
this.hitRenderInstructions_ = new Float32Array(totalHitInstructionsCount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// loop on features to fill the buffer
|
// loop on features to fill the buffer
|
||||||
let feature;
|
let featureUid, featureCache, geometry;
|
||||||
const tmpCoords = [];
|
const tmpCoords = [];
|
||||||
const tmpColor = [];
|
const tmpColor = [];
|
||||||
let renderIndex = 0;
|
let renderIndex = 0;
|
||||||
let hitIndex = 0;
|
let hitIndex = 0;
|
||||||
let hitColor;
|
let hitColor;
|
||||||
for (let i = 0; i < features.length; i++) {
|
for (let i = 0; i < featureUids.length; i++) {
|
||||||
feature = features[i];
|
featureUid = featureUids[i];
|
||||||
if (!feature.getGeometry() || feature.getGeometry().getType() !== GeometryType.POINT) {
|
featureCache = this.featureCache_[featureUid];
|
||||||
|
geometry = /** @type {import("../../geom").Point} */(featureCache.geometry);
|
||||||
|
if (!geometry || geometry.getType() !== GeometryType.POINT) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpCoords[0] = feature.getGeometry().getFlatCoordinates()[0];
|
tmpCoords[0] = geometry.getFlatCoordinates()[0];
|
||||||
tmpCoords[1] = feature.getGeometry().getFlatCoordinates()[1];
|
tmpCoords[1] = geometry.getFlatCoordinates()[1];
|
||||||
applyTransform(projectionTransform, tmpCoords);
|
applyTransform(projectionTransform, tmpCoords);
|
||||||
|
|
||||||
hitColor = colorEncodeId(hitIndex + 6, tmpColor);
|
hitColor = colorEncodeId(hitIndex + 6, tmpColor);
|
||||||
@@ -398,13 +451,13 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
|
|||||||
this.hitRenderInstructions_[hitIndex++] = hitColor[1];
|
this.hitRenderInstructions_[hitIndex++] = hitColor[1];
|
||||||
this.hitRenderInstructions_[hitIndex++] = hitColor[2];
|
this.hitRenderInstructions_[hitIndex++] = hitColor[2];
|
||||||
this.hitRenderInstructions_[hitIndex++] = hitColor[3];
|
this.hitRenderInstructions_[hitIndex++] = hitColor[3];
|
||||||
this.hitRenderInstructions_[hitIndex++] = Number(getUid(feature));
|
this.hitRenderInstructions_[hitIndex++] = Number(featureUid);
|
||||||
}
|
}
|
||||||
|
|
||||||
// pushing custom attributes
|
// pushing custom attributes
|
||||||
let value;
|
let value;
|
||||||
for (let j = 0; j < this.customAttributes.length; j++) {
|
for (let j = 0; j < this.customAttributes.length; j++) {
|
||||||
value = this.customAttributes[j].callback(feature);
|
value = this.customAttributes[j].callback(featureCache.feature, featureCache.properties);
|
||||||
this.renderInstructions_[renderIndex++] = value;
|
this.renderInstructions_[renderIndex++] = value;
|
||||||
if (this.hitDetectionEnabled_) {
|
if (this.hitDetectionEnabled_) {
|
||||||
this.hitRenderInstructions_[hitIndex++] = value;
|
this.hitRenderInstructions_[hitIndex++] = value;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {get as getProjection} from '../../../../../src/ol/proj.js';
|
|||||||
import ViewHint from '../../../../../src/ol/ViewHint.js';
|
import ViewHint from '../../../../../src/ol/ViewHint.js';
|
||||||
import {WebGLWorkerMessageType} from '../../../../../src/ol/renderer/webgl/Layer.js';
|
import {WebGLWorkerMessageType} from '../../../../../src/ol/renderer/webgl/Layer.js';
|
||||||
import {compose as composeTransform, create as createTransform} from '../../../../../src/ol/transform.js';
|
import {compose as composeTransform, create as createTransform} from '../../../../../src/ol/transform.js';
|
||||||
|
import {getUid} from '../../../../../src/ol/util.js';
|
||||||
|
|
||||||
const baseFrameState = {
|
const baseFrameState = {
|
||||||
viewHints: [],
|
viewHints: [],
|
||||||
@@ -388,4 +389,120 @@ describe('ol.renderer.webgl.PointsLayer', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('featureCache_', function() {
|
||||||
|
let source, layer, features;
|
||||||
|
|
||||||
|
function getCache(feature, renderer) {
|
||||||
|
return renderer.featureCache_[getUid(feature)];
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
source = new VectorSource();
|
||||||
|
layer = new VectorLayer({
|
||||||
|
source
|
||||||
|
});
|
||||||
|
features = [
|
||||||
|
new Feature({
|
||||||
|
id: 'A',
|
||||||
|
test: 'abcd',
|
||||||
|
geometry: new Point([0, 1])
|
||||||
|
}),
|
||||||
|
new Feature({
|
||||||
|
id: 'D',
|
||||||
|
test: 'efgh',
|
||||||
|
geometry: new Point([2, 3])
|
||||||
|
}),
|
||||||
|
new Feature({
|
||||||
|
id: 'C',
|
||||||
|
test: 'ijkl',
|
||||||
|
geometry: new Point([4, 5])
|
||||||
|
})
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
it('contains no features initially', function() {
|
||||||
|
const renderer = new WebGLPointsLayerRenderer(layer, {
|
||||||
|
vertexShader: simpleVertexShader,
|
||||||
|
fragmentShader: simpleFragmentShader
|
||||||
|
});
|
||||||
|
expect(Object.keys(renderer.featureCache_).length).to.be(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('contains the features initially present in the source', function() {
|
||||||
|
source.addFeatures(features);
|
||||||
|
const renderer = new WebGLPointsLayerRenderer(layer, {
|
||||||
|
vertexShader: simpleVertexShader,
|
||||||
|
fragmentShader: simpleFragmentShader
|
||||||
|
});
|
||||||
|
expect(Object.keys(renderer.featureCache_).length).to.be(3);
|
||||||
|
expect(getCache(features[0], renderer).feature).to.be(features[0]);
|
||||||
|
expect(getCache(features[0], renderer).geometry).to.be(features[0].getGeometry());
|
||||||
|
expect(getCache(features[0], renderer).properties['test']).to.be(features[0].get('test'));
|
||||||
|
expect(getCache(features[1], renderer).feature).to.be(features[1]);
|
||||||
|
expect(getCache(features[1], renderer).geometry).to.be(features[1].getGeometry());
|
||||||
|
expect(getCache(features[1], renderer).properties['test']).to.be(features[1].get('test'));
|
||||||
|
expect(getCache(features[2], renderer).feature).to.be(features[2]);
|
||||||
|
expect(getCache(features[2], renderer).geometry).to.be(features[2].getGeometry());
|
||||||
|
expect(getCache(features[2], renderer).properties['test']).to.be(features[2].get('test'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('contains the features added to the source', function() {
|
||||||
|
const renderer = new WebGLPointsLayerRenderer(layer, {
|
||||||
|
vertexShader: simpleVertexShader,
|
||||||
|
fragmentShader: simpleFragmentShader
|
||||||
|
});
|
||||||
|
|
||||||
|
source.addFeature(features[0]);
|
||||||
|
expect(Object.keys(renderer.featureCache_).length).to.be(1);
|
||||||
|
|
||||||
|
source.addFeature(features[1]);
|
||||||
|
expect(Object.keys(renderer.featureCache_).length).to.be(2);
|
||||||
|
|
||||||
|
expect(getCache(features[0], renderer).feature).to.be(features[0]);
|
||||||
|
expect(getCache(features[0], renderer).geometry).to.be(features[0].getGeometry());
|
||||||
|
expect(getCache(features[0], renderer).properties['test']).to.be(features[0].get('test'));
|
||||||
|
expect(getCache(features[1], renderer).feature).to.be(features[1]);
|
||||||
|
expect(getCache(features[1], renderer).geometry).to.be(features[1].getGeometry());
|
||||||
|
expect(getCache(features[1], renderer).properties['test']).to.be(features[1].get('test'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not contain the features removed to the source', function() {
|
||||||
|
const renderer = new WebGLPointsLayerRenderer(layer, {
|
||||||
|
vertexShader: simpleVertexShader,
|
||||||
|
fragmentShader: simpleFragmentShader
|
||||||
|
});
|
||||||
|
|
||||||
|
source.addFeatures(features);
|
||||||
|
expect(Object.keys(renderer.featureCache_).length).to.be(3);
|
||||||
|
|
||||||
|
source.removeFeature(features[1]);
|
||||||
|
expect(Object.keys(renderer.featureCache_).length).to.be(2);
|
||||||
|
|
||||||
|
expect(getCache(features[0], renderer).feature).to.be(features[0]);
|
||||||
|
expect(getCache(features[0], renderer).geometry).to.be(features[0].getGeometry());
|
||||||
|
expect(getCache(features[0], renderer).properties['test']).to.be(features[0].get('test'));
|
||||||
|
expect(getCache(features[2], renderer).feature).to.be(features[2]);
|
||||||
|
expect(getCache(features[2], renderer).geometry).to.be(features[2].getGeometry());
|
||||||
|
expect(getCache(features[2], renderer).properties['test']).to.be(features[2].get('test'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('contains up to date properties and geometry', function() {
|
||||||
|
const renderer = new WebGLPointsLayerRenderer(layer, {
|
||||||
|
vertexShader: simpleVertexShader,
|
||||||
|
fragmentShader: simpleFragmentShader
|
||||||
|
});
|
||||||
|
|
||||||
|
source.addFeatures(features);
|
||||||
|
features[0].set('test', 'updated');
|
||||||
|
features[0].set('added', true);
|
||||||
|
features[0].getGeometry().setCoordinates([10, 20]);
|
||||||
|
expect(Object.keys(renderer.featureCache_).length).to.be(3);
|
||||||
|
|
||||||
|
expect(getCache(features[0], renderer).feature).to.be(features[0]);
|
||||||
|
expect(getCache(features[0], renderer).geometry.getCoordinates()).to.eql([10, 20]);
|
||||||
|
expect(getCache(features[0], renderer).properties['test']).to.be(features[0].get('test'));
|
||||||
|
expect(getCache(features[0], renderer).properties['added']).to.be(features[0].get('added'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user