diff --git a/src/ol/render/webgl/MixedGeometryBatch.js b/src/ol/render/webgl/MixedGeometryBatch.js new file mode 100644 index 0000000000..c0270657c1 --- /dev/null +++ b/src/ol/render/webgl/MixedGeometryBatch.js @@ -0,0 +1,336 @@ +/** + * @module ol/render/webgl/MixedGeometryBatch + */ +import {getUid} from '../../util.js'; +import GeometryType from '../../geom/GeometryType.js'; +import WebGLArrayBuffer from '../../webgl/Buffer.js'; +import {ARRAY_BUFFER, DYNAMIC_DRAW, ELEMENT_ARRAY_BUFFER} from '../../webgl.js'; +import {create as createTransform} from '../../transform.js'; + +/** + * @typedef {Object} GeometryBatchItem Object that holds a reference to a feature as well as the raw coordinates of its various geometries + * @property {import("../../Feature").default} feature Feature + * @property {Object} properties Feature properties + * @property {Array>} flatCoordss Array of flat coordinates arrays, one for each geometry related to the feature + * @property {number} [verticesCount] Only defined for linestring and polygon batches + * @property {number} [ringsCount] Only defined for polygon batches + * @property {Array>} [ringsVerticesCounts] Array of vertices counts in each ring for each geometry; only defined for polygons batches + */ + +/** + * @typedef {Object} AbstractGeometryBatch + * @abstract + * @property {Object} entries Dictionary of all entries in the batch with associated computed values. + * One entry corresponds to one feature. Key is feature uid. + * @property {number} geometriesCount Amount of geometries in the batch. + * @property {Float32Array} renderInstructions Render instructions for polygons are structured like so: + * [ numberOfRings, numberOfVerticesInRing0, ..., numberOfVerticesInRingN, x0, y0, customAttr0, ..., xN, yN, customAttrN, numberOfRings,... ] + * @property {WebGLArrayBuffer} verticesBuffer + * @property {WebGLArrayBuffer} indicesBuffer + * @property {import("../../transform.js").Transform} renderInstructionsTransform Converts world space coordinates to screen space; applies to the rendering instructions + * @property {import("../../transform.js").Transform} verticesBufferTransform Converts world space coordinates to screen space; applies to the webgl vertices buffer + * @property {import("../../transform.js").Transform} invertVerticesBufferTransform Screen space to world space; applies to the webgl vertices buffer + */ + +/** + * @typedef {Object} PolygonGeometryBatch A geometry batch specific to polygons + * @extends {AbstractGeometryBatch} + * @property {number} verticesCount Amount of vertices from geometries in the batch. + * @property {number} ringsCount How many outer and inner rings in this batch. + */ + +/** + * @typedef {Object} LineStringGeometryBatch A geometry batch specific to lines + * @extends {AbstractGeometryBatch} + * @property {number} verticesCount Amount of vertices from geometries in the batch. + */ + +/** + * @typedef {Object} PointGeometryBatch A geometry batch specific to points + * @extends {AbstractGeometryBatch} + */ + +/** + * @classdesc This class is used to group several geometries of various types together for faster rendering. + * Three inner batches are maintained for polygons, lines and points. Each time a feature is added, changed or removed + * from the batch, these inner batches are modified accordingly in order to keep them up-to-date. + * + * A feature can be present in several inner batches, for example a polygon geometry will be present in the polygon batch + * and its linar rings will be present in the line batch. Multi geometries are also broken down into individual geometries + * and added to the corresponding batches in a recursive manner. + * + * Corresponding {@link module:ol/render/webgl/BatchRenderer} instances are then used to generate the render instructions + * and WebGL buffers (vertices and indices) for each inner batches; render instructions are stored on the inner batches, + * alongside the transform used to convert world coords to screen coords at the time these instructions were generated. + * The resulting WebGL buffers are stored on the batches as well. + * + * An important aspect of geometry batches is that there is no guarantee that render instructions and WebGL buffers + * are synchronized, i.e. render instructions can describe a new state while WebGL buffers might not have been written yet. + * This is why two world-to-screen transforms are stored on each batch: one for the render instructions and one for + * the WebGL buffers. + */ +class MixedGeometryBatch { + constructor() { + /** + * @type {PolygonGeometryBatch} + */ + this.polygonBatch = { + entries: {}, + geometriesCount: 0, + verticesCount: 0, + ringsCount: 0, + renderInstructions: new Float32Array(0), + verticesBuffer: new WebGLArrayBuffer(ARRAY_BUFFER, DYNAMIC_DRAW), + indicesBuffer: new WebGLArrayBuffer(ELEMENT_ARRAY_BUFFER, DYNAMIC_DRAW), + renderInstructionsTransform: createTransform(), + verticesBufferTransform: createTransform(), + invertVerticesBufferTransform: createTransform(), + }; + + /** + * @type {PointGeometryBatch} + */ + this.pointBatch = { + entries: {}, + geometriesCount: 0, + renderInstructions: new Float32Array(0), + verticesBuffer: new WebGLArrayBuffer(ARRAY_BUFFER, DYNAMIC_DRAW), + indicesBuffer: new WebGLArrayBuffer(ELEMENT_ARRAY_BUFFER, DYNAMIC_DRAW), + renderInstructionsTransform: createTransform(), + verticesBufferTransform: createTransform(), + invertVerticesBufferTransform: createTransform(), + }; + + /** + * @type {LineStringGeometryBatch} + */ + this.lineStringBatch = { + entries: {}, + geometriesCount: 0, + verticesCount: 0, + renderInstructions: new Float32Array(0), + verticesBuffer: new WebGLArrayBuffer(ARRAY_BUFFER, DYNAMIC_DRAW), + indicesBuffer: new WebGLArrayBuffer(ELEMENT_ARRAY_BUFFER, DYNAMIC_DRAW), + renderInstructionsTransform: createTransform(), + verticesBufferTransform: createTransform(), + invertVerticesBufferTransform: createTransform(), + }; + } + + /** + * @param {import("../../Feature").default[]} features + */ + addFeatures(features) { + for (let i = 0; i < features.length; i++) { + this.addFeature(features[i]); + } + } + + /** + * @param {import("../../Feature").default} feature + */ + addFeature(feature) { + const geometry = feature.getGeometry(); + if (!geometry) { + return; + } + this.addGeometry_(geometry, feature); + } + + /** + * @param {import("../../Feature").default} feature + * @return {GeometryBatchItem} + * @private + */ + addFeatureEntryInPointBatch_(feature) { + const uid = getUid(feature); + if (!(uid in this.pointBatch.entries)) { + this.pointBatch.entries[uid] = { + feature: feature, + properties: feature.getProperties(), + flatCoordss: [], + }; + } + return this.pointBatch.entries[uid]; + } + + /** + * @param {import("../../Feature").default} feature + * @return {GeometryBatchItem} + * @private + */ + addFeatureEntryInLineStringBatch_(feature) { + const uid = getUid(feature); + if (!(uid in this.lineStringBatch.entries)) { + this.lineStringBatch.entries[uid] = { + feature: feature, + properties: feature.getProperties(), + flatCoordss: [], + verticesCount: 0, + }; + } + return this.lineStringBatch.entries[uid]; + } + + /** + * @param {import("../../Feature").default} feature + * @return {GeometryBatchItem} + * @private + */ + addFeatureEntryInPolygonBatch_(feature) { + const uid = getUid(feature); + if (!(uid in this.polygonBatch.entries)) { + this.polygonBatch.entries[uid] = { + feature: feature, + properties: feature.getProperties(), + flatCoordss: [], + verticesCount: 0, + ringsCount: 0, + ringsVerticesCounts: [], + }; + } + return this.polygonBatch.entries[uid]; + } + + /** + * @param {import("../../Feature").default} feature + * @private + */ + clearFeatureEntryInPointBatch_(feature) { + const entry = this.pointBatch.entries[getUid(feature)]; + if (!entry) return; + this.pointBatch.geometriesCount -= entry.flatCoordss.length; + delete this.pointBatch.entries[getUid(feature)]; + } + + /** + * @param {import("../../Feature").default} feature + * @private + */ + clearFeatureEntryInLineStringBatch_(feature) { + const entry = this.lineStringBatch.entries[getUid(feature)]; + if (!entry) return; + this.lineStringBatch.verticesCount -= entry.verticesCount; + this.lineStringBatch.geometriesCount -= entry.flatCoordss.length; + delete this.lineStringBatch.entries[getUid(feature)]; + } + + /** + * @param {import("../../Feature").default} feature + * @private + */ + clearFeatureEntryInPolygonBatch_(feature) { + const entry = this.polygonBatch.entries[getUid(feature)]; + if (!entry) return; + this.polygonBatch.verticesCount -= entry.verticesCount; + this.polygonBatch.ringsCount -= entry.ringsCount; + this.polygonBatch.geometriesCount -= entry.flatCoordss.length; + delete this.polygonBatch.entries[getUid(feature)]; + } + + /** + * @param {import("../../geom").Geometry} geometry + * @param {import("../../Feature").default} feature + * @private + */ + addGeometry_(geometry, feature) { + const type = geometry.getType(); + let flatCoords; + let verticesCount; + let batchEntry; + switch (type) { + case GeometryType.GEOMETRY_COLLECTION: + geometry + .getGeometries() + .map((geom) => this.addGeometry_(geom, feature)); + break; + case GeometryType.MULTI_POLYGON: + geometry + .getPolygons() + .map((polygon) => this.addGeometry_(polygon, feature)); + break; + case GeometryType.MULTI_LINE_STRING: + geometry + .getLineStrings() + .map((line) => this.addGeometry_(line, feature)); + break; + case GeometryType.MULTI_POINT: + geometry.getPoints().map((point) => this.addGeometry_(point, feature)); + break; + case GeometryType.POLYGON: + batchEntry = this.addFeatureEntryInPolygonBatch_(feature); + flatCoords = geometry.getFlatCoordinates(); + verticesCount = flatCoords.length / 2; + const ringsCount = geometry.getLinearRingCount(); + const ringsVerticesCount = geometry + .getEnds() + .map((end, ind, arr) => + ind > 0 ? (end - arr[ind - 1]) / 2 : end / 2 + ); + this.polygonBatch.verticesCount += verticesCount; + this.polygonBatch.ringsCount += ringsCount; + this.polygonBatch.geometriesCount++; + batchEntry.flatCoordss.push(flatCoords); + batchEntry.ringsVerticesCounts.push(ringsVerticesCount); + batchEntry.verticesCount += verticesCount; + batchEntry.ringsCount += ringsCount; + geometry + .getLinearRings() + .map((ring) => this.addGeometry_(ring, feature)); + break; + case GeometryType.POINT: + batchEntry = this.addFeatureEntryInPointBatch_(feature); + flatCoords = geometry.getFlatCoordinates(); + this.pointBatch.geometriesCount++; + batchEntry.flatCoordss.push(flatCoords); + break; + case GeometryType.LINE_STRING: + case GeometryType.LINEAR_RING: + batchEntry = this.addFeatureEntryInLineStringBatch_(feature); + flatCoords = geometry.getFlatCoordinates(); + verticesCount = flatCoords.length / 2; + this.lineStringBatch.verticesCount += verticesCount; + this.lineStringBatch.geometriesCount++; + batchEntry.flatCoordss.push(flatCoords); + batchEntry.verticesCount += verticesCount; + break; + } + } + + /** + * @param {import("../../Feature").default} feature + */ + changeFeature(feature) { + this.clearFeatureEntryInPointBatch_(feature); + this.clearFeatureEntryInPolygonBatch_(feature); + this.clearFeatureEntryInLineStringBatch_(feature); + const geometry = feature.getGeometry(); + if (!geometry) { + return; + } + this.addGeometry_(geometry, feature); + } + + /** + * @param {import("../../Feature").default} feature + */ + removeFeature(feature) { + this.clearFeatureEntryInPointBatch_(feature); + this.clearFeatureEntryInPolygonBatch_(feature); + this.clearFeatureEntryInLineStringBatch_(feature); + } + + clear() { + this.polygonBatch.entries = {}; + this.polygonBatch.geometriesCount = 0; + this.polygonBatch.verticesCount = 0; + this.polygonBatch.ringsCount = 0; + this.lineStringBatch.entries = {}; + this.lineStringBatch.geometriesCount = 0; + this.lineStringBatch.verticesCount = 0; + this.pointBatch.entries = {}; + this.pointBatch.geometriesCount = 0; + } +} + +export default MixedGeometryBatch; diff --git a/test/browser/spec/ol/render/webgl/mixedgeometrybatch.test.js b/test/browser/spec/ol/render/webgl/mixedgeometrybatch.test.js new file mode 100644 index 0000000000..cd28c50e3d --- /dev/null +++ b/test/browser/spec/ol/render/webgl/mixedgeometrybatch.test.js @@ -0,0 +1,778 @@ +import Feature from '../../../../../../src/ol/Feature.js'; +import Point from '../../../../../../src/ol/geom/Point.js'; +import MixedGeometryBatch from '../../../../../../src/ol/render/webgl/MixedGeometryBatch.js'; +import LineString from '../../../../../../src/ol/geom/LineString.js'; +import {getUid} from '../../../../../../src/ol/index.js'; +import Polygon from '../../../../../../src/ol/geom/Polygon.js'; +import LinearRing from '../../../../../../src/ol/geom/LinearRing.js'; +import MultiPolygon from '../../../../../../src/ol/geom/MultiPolygon.js'; +import GeometryCollection from '../../../../../../src/ol/geom/GeometryCollection.js'; +import MultiLineString from '../../../../../../src/ol/geom/MultiLineString.js'; +import MultiPoint from '../../../../../../src/ol/geom/MultiPoint.js'; + +describe('MixedGeometryBatch', function () { + let mixedBatch; + + beforeEach(() => { + mixedBatch = new MixedGeometryBatch(); + }); + + describe('#addFeatures', () => { + let features, spy; + beforeEach(() => { + features = [new Feature(), new Feature(), new Feature()]; + spy = sinon.spy(mixedBatch, 'addFeature'); + mixedBatch.addFeatures(features); + }); + it('calls addFeature for each feature', () => { + expect(spy.callCount).to.be(3); + expect(spy.args[0][0]).to.be(features[0]); + expect(spy.args[1][0]).to.be(features[1]); + expect(spy.args[2][0]).to.be(features[2]); + }); + }); + + describe('features with Point geometries', () => { + let geometry1, feature1, geometry2, feature2; + + beforeEach(() => { + geometry1 = new Point([0, 1]); + feature1 = new Feature({ + geometry: geometry1, + prop1: 'abcd', + prop2: 'efgh', + }); + geometry2 = new Point([2, 3]); + feature2 = new Feature({ + geometry: geometry2, + prop3: '1234', + prop4: '5678', + }); + }); + + describe('#addFeature', () => { + beforeEach(() => { + mixedBatch.addFeature(feature1); + mixedBatch.addFeature(feature2); + }); + it('puts the geometries in the point batch', () => { + const keys = Object.keys(mixedBatch.pointBatch.entries); + const uid1 = getUid(feature1); + const uid2 = getUid(feature2); + expect(keys).to.eql([uid1, uid2]); + expect(mixedBatch.pointBatch.entries[uid1]).to.eql({ + feature: feature1, + properties: feature1.getProperties(), + flatCoordss: [[0, 1]], + }); + expect(mixedBatch.pointBatch.entries[uid2]).to.eql({ + feature: feature2, + properties: feature2.getProperties(), + flatCoordss: [[2, 3]], + }); + }); + it('computes the geometries count', () => { + expect(mixedBatch.pointBatch.geometriesCount).to.be(2); + }); + it('leaves other batches untouched', () => { + expect(Object.keys(mixedBatch.polygonBatch.entries)).to.have.length(0); + expect(Object.keys(mixedBatch.lineStringBatch.entries)).to.have.length( + 0 + ); + }); + }); + + describe('#changeFeature', () => { + beforeEach(() => { + mixedBatch.addFeature(feature1); + mixedBatch.addFeature(feature2); + }); + describe('modifying geometry and props', () => { + beforeEach(() => { + feature1.set('prop1', 'changed'); + geometry1.setCoordinates([100, 101]); + mixedBatch.changeFeature(feature1); + }); + it('updates the modified properties and geometry in the point batch', () => { + const entry = mixedBatch.pointBatch.entries[getUid(feature1)]; + expect(entry.properties.prop1).to.eql('changed'); + }); + it('keeps geometry count the same', () => { + expect(mixedBatch.pointBatch.geometriesCount).to.be(2); + }); + }); + describe('changing the geometry', () => { + let newGeom; + beforeEach(() => { + newGeom = new Point([40, 41]); + feature1.setGeometry(newGeom); + mixedBatch.changeFeature(feature1); + }); + it('updates the geometry in the point batch', () => { + const entry = mixedBatch.pointBatch.entries[getUid(feature1)]; + expect(entry.flatCoordss).to.eql([[40, 41]]); + }); + it('keeps geometry count the same', () => { + expect(mixedBatch.pointBatch.geometriesCount).to.be(2); + }); + }); + }); + + describe('#removeFeature', () => { + beforeEach(() => { + mixedBatch.addFeature(feature1); + mixedBatch.addFeature(feature2); + mixedBatch.removeFeature(feature1); + }); + it('clears the entry related to this feature', () => { + const keys = Object.keys(mixedBatch.pointBatch.entries); + expect(keys).to.not.contain(getUid(feature1)); + }); + it('recompute geometry count', () => { + expect(mixedBatch.pointBatch.geometriesCount).to.be(1); + }); + }); + }); + + describe('features with LineString geometries', () => { + let geometry1, feature1, geometry2, feature2; + + beforeEach(() => { + geometry1 = new LineString([ + [0, 1], + [2, 3], + [4, 5], + [6, 7], + ]); + feature1 = new Feature({ + geometry: geometry1, + prop1: 'abcd', + prop2: 'efgh', + }); + geometry2 = new LineString([ + [8, 9], + [10, 11], + [12, 13], + ]); + feature2 = new Feature({ + geometry: geometry2, + prop3: '1234', + prop4: '5678', + }); + }); + + describe('#addFeature', () => { + beforeEach(() => { + mixedBatch.addFeature(feature1); + mixedBatch.addFeature(feature2); + }); + it('puts the geometries in the linestring batch', () => { + const keys = Object.keys(mixedBatch.lineStringBatch.entries); + const uid1 = getUid(feature1); + const uid2 = getUid(feature2); + expect(keys).to.eql([uid1, uid2]); + expect(mixedBatch.lineStringBatch.entries[uid1]).to.eql({ + feature: feature1, + properties: feature1.getProperties(), + flatCoordss: [[0, 1, 2, 3, 4, 5, 6, 7]], + verticesCount: 4, + }); + expect(mixedBatch.lineStringBatch.entries[uid2]).to.eql({ + feature: feature2, + properties: feature2.getProperties(), + flatCoordss: [[8, 9, 10, 11, 12, 13]], + verticesCount: 3, + }); + }); + it('computes the aggregated metrics on all geoms', () => { + expect(mixedBatch.lineStringBatch.verticesCount).to.be(7); + expect(mixedBatch.lineStringBatch.geometriesCount).to.be(2); + }); + it('leaves other batches untouched', () => { + expect(Object.keys(mixedBatch.polygonBatch.entries)).to.have.length(0); + expect(Object.keys(mixedBatch.pointBatch.entries)).to.have.length(0); + }); + }); + + describe('#changeFeature', () => { + beforeEach(() => { + mixedBatch.addFeature(feature1); + mixedBatch.addFeature(feature2); + }); + describe('modifying geometry and props', () => { + beforeEach(() => { + feature1.set('prop1', 'changed'); + geometry1.appendCoordinate([100, 101]); + geometry1.appendCoordinate([102, 103]); + mixedBatch.changeFeature(feature1); + }); + it('updates the modified properties and geometry in the linestring batch', () => { + const entry = mixedBatch.lineStringBatch.entries[getUid(feature1)]; + expect(entry.properties.prop1).to.eql('changed'); + expect(entry.verticesCount).to.eql(6); + expect(entry.flatCoordss).to.eql([ + [0, 1, 2, 3, 4, 5, 6, 7, 100, 101, 102, 103], + ]); + }); + it('updates the aggregated metrics on all geoms', () => { + expect(mixedBatch.lineStringBatch.verticesCount).to.be(9); + expect(mixedBatch.lineStringBatch.geometriesCount).to.be(2); + }); + }); + describe('changing the geometry', () => { + let newGeom; + beforeEach(() => { + newGeom = new LineString([ + [40, 41], + [42, 43], + ]); + feature1.setGeometry(newGeom); + mixedBatch.changeFeature(feature1); + }); + it('updates the geometry in the linestring batch', () => { + const entry = mixedBatch.lineStringBatch.entries[getUid(feature1)]; + expect(entry.flatCoordss).to.eql([[40, 41, 42, 43]]); + }); + it('updates the aggregated metrics on all geoms', () => { + expect(mixedBatch.lineStringBatch.verticesCount).to.be(5); + expect(mixedBatch.lineStringBatch.geometriesCount).to.be(2); + }); + }); + }); + + describe('#removeFeature', () => { + beforeEach(() => { + mixedBatch.addFeature(feature1); + mixedBatch.addFeature(feature2); + mixedBatch.removeFeature(feature1); + }); + it('clears the entry related to this feature', () => { + const keys = Object.keys(mixedBatch.lineStringBatch.entries); + expect(keys).to.not.contain(getUid(feature1)); + }); + it('updates the aggregated metrics on all geoms', () => { + expect(mixedBatch.lineStringBatch.verticesCount).to.be(3); + expect(mixedBatch.lineStringBatch.geometriesCount).to.be(1); + }); + }); + }); + + describe('features with Polygon geometries', () => { + let geometry1, feature1, geometry2, feature2; + + beforeEach(() => { + geometry1 = new Polygon([ + [ + [0, 1], + [2, 3], + [4, 5], + [6, 7], + ], + [ + [20, 21], + [22, 23], + [24, 25], + ], + ]); + feature1 = new Feature({ + geometry: geometry1, + prop1: 'abcd', + prop2: 'efgh', + }); + geometry2 = new Polygon([ + [ + [8, 9], + [10, 11], + [12, 13], + ], + [ + [30, 31], + [32, 33], + [34, 35], + ], + [ + [40, 41], + [42, 43], + [44, 45], + [46, 47], + ], + ]); + feature2 = new Feature({ + geometry: geometry2, + prop3: '1234', + prop4: '5678', + }); + }); + + describe('#addFeature', () => { + beforeEach(() => { + mixedBatch.addFeature(feature1); + mixedBatch.addFeature(feature2); + }); + it('puts the polygons in the polygon batch', () => { + const keys = Object.keys(mixedBatch.polygonBatch.entries); + const uid1 = getUid(feature1); + const uid2 = getUid(feature2); + expect(keys).to.eql([uid1, uid2]); + expect(mixedBatch.polygonBatch.entries[uid1]).to.eql({ + feature: feature1, + properties: feature1.getProperties(), + flatCoordss: [[0, 1, 2, 3, 4, 5, 6, 7, 20, 21, 22, 23, 24, 25]], + verticesCount: 7, + ringsCount: 2, + ringsVerticesCounts: [[4, 3]], + }); + expect(mixedBatch.polygonBatch.entries[uid2]).to.eql({ + feature: feature2, + properties: feature2.getProperties(), + flatCoordss: [ + [ + 8, 9, 10, 11, 12, 13, 30, 31, 32, 33, 34, 35, 40, 41, 42, 43, 44, + 45, 46, 47, + ], + ], + verticesCount: 10, + ringsCount: 3, + ringsVerticesCounts: [[3, 3, 4]], + }); + }); + it('computes the aggregated metrics on all polygons', () => { + expect(mixedBatch.polygonBatch.verticesCount).to.be(17); + expect(mixedBatch.polygonBatch.geometriesCount).to.be(2); + expect(mixedBatch.polygonBatch.ringsCount).to.be(5); + }); + it('puts the linear rings in the linestring batch', () => { + const keys = Object.keys(mixedBatch.lineStringBatch.entries); + expect(keys).to.eql([getUid(feature1), getUid(feature2)]); + expect(mixedBatch.lineStringBatch.entries[getUid(feature1)]).to.eql({ + feature: feature1, + properties: feature1.getProperties(), + flatCoordss: [ + [0, 1, 2, 3, 4, 5, 6, 7], + [20, 21, 22, 23, 24, 25], + ], + verticesCount: 7, + }); + expect(mixedBatch.lineStringBatch.entries[getUid(feature2)]).to.eql({ + feature: feature2, + properties: feature2.getProperties(), + flatCoordss: [ + [8, 9, 10, 11, 12, 13], + [30, 31, 32, 33, 34, 35], + [40, 41, 42, 43, 44, 45, 46, 47], + ], + verticesCount: 10, + }); + }); + it('computes the aggregated metrics on all linestrings', () => { + expect(mixedBatch.lineStringBatch.verticesCount).to.be(17); + expect(mixedBatch.lineStringBatch.geometriesCount).to.be(5); + }); + it('leaves point batch untouched', () => { + expect(Object.keys(mixedBatch.pointBatch.entries)).to.have.length(0); + }); + }); + + describe('#changeFeature', () => { + beforeEach(() => { + mixedBatch.addFeature(feature1); + mixedBatch.addFeature(feature2); + }); + describe('modifying geometry and props', () => { + beforeEach(() => { + feature1.set('prop1', 'changed'); + geometry1.appendLinearRing( + new LinearRing([ + [201, 202], + [203, 204], + [205, 206], + [207, 208], + ]) + ); + mixedBatch.changeFeature(feature1); + }); + it('updates the modified properties and geometry in the polygon batch', () => { + const entry = mixedBatch.polygonBatch.entries[getUid(feature1)]; + expect(entry.properties.prop1).to.eql('changed'); + expect(entry.verticesCount).to.eql(11); + expect(entry.ringsCount).to.eql(3); + expect(entry.ringsVerticesCounts).to.eql([[4, 3, 4]]); + }); + it('updates the aggregated metrics on all geoms', () => { + expect(mixedBatch.polygonBatch.verticesCount).to.be(21); + expect(mixedBatch.polygonBatch.geometriesCount).to.be(2); + expect(mixedBatch.polygonBatch.ringsCount).to.be(6); + }); + }); + describe('changing the geometry', () => { + let newGeom; + beforeEach(() => { + newGeom = new Polygon([ + [ + [201, 202], + [203, 204], + [205, 206], + [207, 208], + ], + ]); + feature1.setGeometry(newGeom); + mixedBatch.changeFeature(feature1); + }); + it('updates the geometry in the polygon batch', () => { + const entry = mixedBatch.polygonBatch.entries[getUid(feature1)]; + expect(entry.feature).to.be(feature1); + expect(entry.verticesCount).to.eql(4); + expect(entry.ringsCount).to.eql(1); + expect(entry.ringsVerticesCounts).to.eql([[4]]); + expect(entry.flatCoordss).to.eql([ + [201, 202, 203, 204, 205, 206, 207, 208], + ]); + }); + it('updates the aggregated metrics on all geoms', () => { + expect(mixedBatch.polygonBatch.verticesCount).to.be(14); + expect(mixedBatch.polygonBatch.geometriesCount).to.be(2); + expect(mixedBatch.polygonBatch.ringsCount).to.be(4); + }); + }); + }); + + describe('#removeFeature', () => { + beforeEach(() => { + mixedBatch.addFeature(feature1); + mixedBatch.addFeature(feature2); + mixedBatch.removeFeature(feature1); + }); + it('clears the entry related to this feature', () => { + const keys = Object.keys(mixedBatch.polygonBatch.entries); + expect(keys).to.not.contain(getUid(feature1)); + }); + it('updates the aggregated metrics on all geoms', () => { + expect(mixedBatch.polygonBatch.verticesCount).to.be(10); + expect(mixedBatch.polygonBatch.geometriesCount).to.be(1); + expect(mixedBatch.polygonBatch.ringsCount).to.be(3); + }); + }); + }); + + describe('feature with nested geometries (collection, multi)', () => { + let feature, geomCollection, multiPolygon, multiPoint, multiLine; + + beforeEach(() => { + multiPoint = new MultiPoint([ + [101, 102], + [201, 202], + [301, 302], + ]); + multiLine = new MultiLineString([ + [ + [0, 1], + [2, 3], + [4, 5], + [6, 7], + ], + [ + [8, 9], + [10, 11], + [12, 13], + ], + ]); + multiPolygon = new MultiPolygon([ + [ + [ + [0, 1], + [2, 3], + [4, 5], + [6, 7], + ], + [ + [20, 21], + [22, 23], + [24, 25], + ], + ], + [ + [ + [8, 9], + [10, 11], + [12, 13], + ], + [ + [30, 31], + [32, 33], + [34, 35], + ], + [ + [40, 41], + [42, 43], + [44, 45], + [46, 47], + ], + ], + ]); + geomCollection = new GeometryCollection([ + multiPolygon, + multiLine, + multiPoint, + ]); + feature = new Feature({ + geometry: geomCollection, + prop3: '1234', + prop4: '5678', + }); + }); + + describe('#addFeature', () => { + beforeEach(() => { + mixedBatch.addFeature(feature); + }); + it('puts the polygons in the polygon batch', () => { + const uid = getUid(feature); + expect(mixedBatch.polygonBatch.entries[uid]).to.eql({ + feature: feature, + properties: feature.getProperties(), + flatCoordss: [ + [0, 1, 2, 3, 4, 5, 6, 7, 20, 21, 22, 23, 24, 25], + [ + 8, 9, 10, 11, 12, 13, 30, 31, 32, 33, 34, 35, 40, 41, 42, 43, 44, + 45, 46, 47, + ], + ], + verticesCount: 17, + ringsCount: 5, + ringsVerticesCounts: [ + [4, 3], + [3, 3, 4], + ], + }); + }); + it('puts the polygon rings and linestrings in the linestring batch', () => { + const uid = getUid(feature); + expect(mixedBatch.lineStringBatch.entries[uid]).to.eql({ + feature: feature, + properties: feature.getProperties(), + flatCoordss: [ + [0, 1, 2, 3, 4, 5, 6, 7], + [20, 21, 22, 23, 24, 25], + [8, 9, 10, 11, 12, 13], + [30, 31, 32, 33, 34, 35], + [40, 41, 42, 43, 44, 45, 46, 47], + [0, 1, 2, 3, 4, 5, 6, 7], + [8, 9, 10, 11, 12, 13], + ], + verticesCount: 24, + }); + }); + it('puts the points in the linestring batch', () => { + const uid = getUid(feature); + expect(mixedBatch.pointBatch.entries[uid]).to.eql({ + feature: feature, + properties: feature.getProperties(), + flatCoordss: [ + [101, 102], + [201, 202], + [301, 302], + ], + }); + }); + it('computes the aggregated metrics on all polygons', () => { + expect(mixedBatch.polygonBatch.verticesCount).to.be(17); + expect(mixedBatch.polygonBatch.geometriesCount).to.be(2); + expect(mixedBatch.polygonBatch.ringsCount).to.be(5); + }); + it('computes the aggregated metrics on all linestring', () => { + expect(mixedBatch.lineStringBatch.verticesCount).to.be(24); + expect(mixedBatch.lineStringBatch.geometriesCount).to.be(7); + }); + it('computes the aggregated metrics on all points', () => { + expect(mixedBatch.pointBatch.geometriesCount).to.be(3); + }); + }); + + describe('#changeFeature', () => { + beforeEach(() => { + mixedBatch.addFeature(feature); + }); + describe('modifying geometry', () => { + beforeEach(() => { + multiLine.appendLineString( + new LineString([ + [500, 501], + [502, 503], + [504, 505], + [506, 507], + ]) + ); + multiPolygon.appendPolygon( + new Polygon([ + [ + [201, 202], + [203, 204], + [205, 206], + [207, 208], + [209, 210], + ], + ]) + ); + mixedBatch.changeFeature(feature); + }); + it('updates the geometries in the polygon batch', () => { + const entry = mixedBatch.polygonBatch.entries[getUid(feature)]; + expect(entry).to.eql({ + feature: feature, + properties: feature.getProperties(), + flatCoordss: [ + [0, 1, 2, 3, 4, 5, 6, 7, 20, 21, 22, 23, 24, 25], + [ + 8, 9, 10, 11, 12, 13, 30, 31, 32, 33, 34, 35, 40, 41, 42, 43, + 44, 45, 46, 47, + ], + [201, 202, 203, 204, 205, 206, 207, 208, 209, 210], + ], + verticesCount: 22, + ringsCount: 6, + ringsVerticesCounts: [[4, 3], [3, 3, 4], [5]], + }); + }); + it('updates the geometries in the linestring batch', () => { + const entry = mixedBatch.lineStringBatch.entries[getUid(feature)]; + expect(entry).to.eql({ + feature: feature, + properties: feature.getProperties(), + flatCoordss: [ + [0, 1, 2, 3, 4, 5, 6, 7], + [20, 21, 22, 23, 24, 25], + [8, 9, 10, 11, 12, 13], + [30, 31, 32, 33, 34, 35], + [40, 41, 42, 43, 44, 45, 46, 47], + [201, 202, 203, 204, 205, 206, 207, 208, 209, 210], + [0, 1, 2, 3, 4, 5, 6, 7], + [8, 9, 10, 11, 12, 13], + [500, 501, 502, 503, 504, 505, 506, 507], + ], + verticesCount: 33, + }); + }); + it('updates the aggregated metrics on the polygon batch', () => { + expect(mixedBatch.polygonBatch.verticesCount).to.be(22); + expect(mixedBatch.polygonBatch.geometriesCount).to.be(3); + expect(mixedBatch.polygonBatch.ringsCount).to.be(6); + }); + it('updates the aggregated metrics on the linestring batch', () => { + expect(mixedBatch.lineStringBatch.verticesCount).to.be(33); + expect(mixedBatch.lineStringBatch.geometriesCount).to.be(9); + }); + }); + describe('changing the geometry', () => { + beforeEach(() => { + feature.setGeometry( + new Polygon([ + [ + [201, 202], + [203, 204], + [205, 206], + [207, 208], + ], + ]) + ); + mixedBatch.changeFeature(feature); + }); + it('updates the geometries in the polygon batch', () => { + const entry = mixedBatch.polygonBatch.entries[getUid(feature)]; + expect(entry).to.eql({ + feature: feature, + properties: feature.getProperties(), + flatCoordss: [[201, 202, 203, 204, 205, 206, 207, 208]], + verticesCount: 4, + ringsCount: 1, + ringsVerticesCounts: [[4]], + }); + }); + it('updates the geometries in the linestring batch', () => { + const entry = mixedBatch.lineStringBatch.entries[getUid(feature)]; + expect(entry).to.eql({ + feature: feature, + properties: feature.getProperties(), + flatCoordss: [[201, 202, 203, 204, 205, 206, 207, 208]], + verticesCount: 4, + }); + }); + it('updates the aggregated metrics on the polygon batch', () => { + expect(mixedBatch.polygonBatch.verticesCount).to.be(4); + expect(mixedBatch.polygonBatch.geometriesCount).to.be(1); + expect(mixedBatch.polygonBatch.ringsCount).to.be(1); + }); + it('updates the aggregated metrics on the linestring batch', () => { + expect(mixedBatch.lineStringBatch.verticesCount).to.be(4); + expect(mixedBatch.lineStringBatch.geometriesCount).to.be(1); + }); + it('updates the aggregated metrics on the point batch', () => { + const keys = Object.keys(mixedBatch.pointBatch.entries); + expect(keys).to.not.contain(getUid(feature)); + expect(mixedBatch.pointBatch.geometriesCount).to.be(0); + }); + }); + }); + + describe('#removeFeature', () => { + beforeEach(() => { + mixedBatch.addFeature(feature); + mixedBatch.removeFeature(feature); + }); + it('clears all entries in the polygon batch', () => { + const keys = Object.keys(mixedBatch.polygonBatch.entries); + expect(keys).to.have.length(0); + expect(mixedBatch.polygonBatch.verticesCount).to.be(0); + expect(mixedBatch.polygonBatch.geometriesCount).to.be(0); + expect(mixedBatch.polygonBatch.ringsCount).to.be(0); + }); + it('clears all entries in the linestring batch', () => { + const keys = Object.keys(mixedBatch.lineStringBatch.entries); + expect(keys).to.have.length(0); + expect(mixedBatch.lineStringBatch.verticesCount).to.be(0); + expect(mixedBatch.lineStringBatch.geometriesCount).to.be(0); + }); + it('clears all entries in the point batch', () => { + const keys = Object.keys(mixedBatch.pointBatch.entries); + expect(keys).to.have.length(0); + expect(mixedBatch.pointBatch.geometriesCount).to.be(0); + }); + }); + }); + + describe('#clear', () => { + beforeEach(() => { + const feature1 = new Feature( + new Polygon([ + [ + [201, 202], + [203, 204], + [205, 206], + [207, 208], + ], + ]) + ); + const feature2 = new Feature(new Point([201, 202])); + mixedBatch.addFeature(feature1); + mixedBatch.addFeature(feature2); + mixedBatch.clear(); + }); + + it('clears polygon batch', () => { + expect(Object.keys(mixedBatch.polygonBatch.entries)).to.have.length(0); + expect(mixedBatch.polygonBatch.geometriesCount).to.be(0); + expect(mixedBatch.polygonBatch.verticesCount).to.be(0); + expect(mixedBatch.polygonBatch.ringsCount).to.be(0); + }); + + it('clears linestring batch', () => { + expect(Object.keys(mixedBatch.lineStringBatch.entries)).to.have.length(0); + expect(mixedBatch.lineStringBatch.geometriesCount).to.be(0); + expect(mixedBatch.lineStringBatch.verticesCount).to.be(0); + }); + + it('clears point batch', () => { + expect(Object.keys(mixedBatch.pointBatch.entries)).to.have.length(0); + expect(mixedBatch.pointBatch.geometriesCount).to.be(0); + }); + }); +});