/** * @module ol/render/webgl/MixedGeometryBatch */ import {getUid} from '../../util.js'; import {linearRingIsClockwise} from '../../geom/flat/orient.js'; /** * @typedef {import("../../render/Feature").default} RenderFeature */ /** * @typedef {import("../../Feature").default} Feature */ /** * @typedef {import("../../geom/Geometry.js").Type} GeometryType */ /** * @typedef {Object} GeometryBatchItem Object that holds a reference to a feature as well as the raw coordinates of its various geometries * @property {Feature|RenderFeature} feature Feature * @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 {PointGeometryBatch|LineStringGeometryBatch|PolygonGeometryBatch} GeometryBatch */ /** * @typedef {Object} PolygonGeometryBatch A geometry batch specific to polygons * @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 {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 * @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 {number} verticesCount Amount of vertices from geometries in the batch. */ /** * @typedef {Object} PointGeometryBatch A geometry batch specific to points * @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. */ /** * @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 linear 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, }; /** * @type {PointGeometryBatch} */ this.pointBatch = { entries: {}, geometriesCount: 0, }; /** * @type {LineStringGeometryBatch} */ this.lineStringBatch = { entries: {}, geometriesCount: 0, verticesCount: 0, }; } /** * @param {Array} features Array of features to add to the batch */ addFeatures(features) { for (let i = 0; i < features.length; i++) { this.addFeature(features[i]); } } /** * @param {Feature|RenderFeature} feature Feature to add to the batch */ addFeature(feature) { const geometry = feature.getGeometry(); if (!geometry) { return; } this.addGeometry_(geometry, feature); } /** * @param {Feature|RenderFeature} feature 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 {Feature|RenderFeature} feature 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 {Feature|RenderFeature} feature 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|RenderFeature} geometry Geometry * @param {Feature|RenderFeature} feature Feature * @private */ addGeometry_(geometry, feature) { const type = geometry.getType(); switch (type) { case 'GeometryCollection': const geometries = /** @type {import("../../geom").GeometryCollection} */ ( geometry ).getGeometriesArray(); for (const geometry of geometries) { this.addGeometry_(geometry, feature); } break; case 'MultiPolygon': const multiPolygonGeom = /** @type {import("../../geom").MultiPolygon|RenderFeature} */ ( geometry ); this.addCoordinates_( type, multiPolygonGeom.getFlatCoordinates(), multiPolygonGeom.getEndss(), feature, getUid(feature) ); break; case 'MultiLineString': const multiLineGeom = /** @type {import("../../geom").MultiLineString|RenderFeature} */ ( geometry ); this.addCoordinates_( type, multiLineGeom.getFlatCoordinates(), multiLineGeom.getEnds(), feature, getUid(feature) ); break; case 'MultiPoint': const multiPointGeom = /** @type {import("../../geom").MultiPoint|RenderFeature} */ ( geometry ); this.addCoordinates_( type, multiPointGeom.getFlatCoordinates(), null, feature, getUid(feature) ); break; case 'Polygon': const polygonGeom = /** @type {import("../../geom").Polygon|RenderFeature} */ (geometry); this.addCoordinates_( type, polygonGeom.getFlatCoordinates(), polygonGeom.getEnds(), feature, getUid(feature) ); break; case 'Point': const pointGeom = /** @type {import("../../geom").Point} */ (geometry); this.addCoordinates_( type, pointGeom.getFlatCoordinates(), null, feature, getUid(feature) ); break; case 'LineString': case 'LinearRing': const lineGeom = /** @type {import("../../geom").LineString} */ ( geometry ); this.addCoordinates_( type, lineGeom.getFlatCoordinates(), null, feature, getUid(feature) ); break; default: // pass } } /** * @param {GeometryType} type Geometry type * @param {Array} flatCoords Flat coordinates * @param {Array | Array> | null} ends Coordinate ends * @param {Feature|RenderFeature} feature Feature * @param {string} featureUid Feature uid * @private */ addCoordinates_(type, flatCoords, ends, feature, featureUid) { /** @type {number} */ let verticesCount; switch (type) { case 'MultiPolygon': const multiPolygonEndss = /** @type {Array>} */ (ends); for (let i = 0, ii = multiPolygonEndss.length; i < ii; i++) { let polygonEnds = multiPolygonEndss[i]; const prevPolygonEnds = i > 0 ? multiPolygonEndss[i - 1] : null; const startIndex = prevPolygonEnds ? prevPolygonEnds[prevPolygonEnds.length - 1] : 0; const endIndex = polygonEnds[polygonEnds.length - 1]; const polygonCoords = flatCoords.slice(startIndex, endIndex); polygonEnds = startIndex > 0 ? polygonEnds.map((end) => end - startIndex) : polygonEnds; this.addCoordinates_( 'Polygon', polygonCoords, polygonEnds, feature, featureUid ); } break; case 'MultiLineString': const multiLineEnds = /** @type {Array} */ (ends); for (let i = 0, ii = multiLineEnds.length; i < ii; i++) { const startIndex = i > 0 ? multiLineEnds[i - 1] : 0; const ringCoords = flatCoords.slice(startIndex, multiLineEnds[i]); this.addCoordinates_( 'LinearRing', ringCoords, null, feature, featureUid ); } break; case 'MultiPoint': for (let i = 0, ii = flatCoords.length; i < ii; i += 2) { this.addCoordinates_( 'Point', flatCoords.slice(i, i + 2), null, feature, featureUid ); } break; case 'Polygon': const polygonEnds = /** @type {Array} */ (ends); // first look for a CW ring; if so, handle it and following rings as another polygon for (let i = 1, ii = polygonEnds.length; i < ii; i++) { const ringStartIndex = polygonEnds[i - 1]; if ( i > 0 && linearRingIsClockwise(flatCoords, ringStartIndex, polygonEnds[i], 2) ) { this.addCoordinates_( 'Polygon', flatCoords.slice(0, ringStartIndex), polygonEnds.slice(0, i), feature, featureUid ); this.addCoordinates_( 'Polygon', flatCoords.slice(ringStartIndex), polygonEnds.slice(i).map((end) => end - polygonEnds[i - 1]), feature, featureUid ); return; } } if (!this.polygonBatch.entries[featureUid]) { this.polygonBatch.entries[featureUid] = { feature: feature, flatCoordss: [], verticesCount: 0, ringsCount: 0, ringsVerticesCounts: [], }; } verticesCount = flatCoords.length / 2; const ringsCount = ends.length; const ringsVerticesCount = ends.map((end, ind, arr) => ind > 0 ? (end - arr[ind - 1]) / 2 : end / 2 ); this.polygonBatch.verticesCount += verticesCount; this.polygonBatch.ringsCount += ringsCount; this.polygonBatch.geometriesCount++; this.polygonBatch.entries[featureUid].flatCoordss.push(flatCoords); this.polygonBatch.entries[featureUid].ringsVerticesCounts.push( ringsVerticesCount ); this.polygonBatch.entries[featureUid].verticesCount += verticesCount; this.polygonBatch.entries[featureUid].ringsCount += ringsCount; for (let i = 0, ii = polygonEnds.length; i < ii; i++) { const startIndex = i > 0 ? polygonEnds[i - 1] : 0; const ringCoords = flatCoords.slice(startIndex, polygonEnds[i]); this.addCoordinates_( 'LinearRing', ringCoords, null, feature, featureUid ); } break; case 'Point': if (!this.pointBatch.entries[featureUid]) { this.pointBatch.entries[featureUid] = { feature: feature, flatCoordss: [], }; } this.pointBatch.geometriesCount++; this.pointBatch.entries[featureUid].flatCoordss.push(flatCoords); break; case 'LineString': case 'LinearRing': if (!this.lineStringBatch.entries[featureUid]) { this.lineStringBatch.entries[featureUid] = { feature: feature, flatCoordss: [], verticesCount: 0, }; } verticesCount = flatCoords.length / 2; this.lineStringBatch.verticesCount += verticesCount; this.lineStringBatch.geometriesCount++; this.lineStringBatch.entries[featureUid].flatCoordss.push(flatCoords); this.lineStringBatch.entries[featureUid].verticesCount += verticesCount; break; default: // pass } } /** * @param {Feature|RenderFeature} feature Feature */ changeFeature(feature) { this.clearFeatureEntryInPointBatch_(feature); this.clearFeatureEntryInPolygonBatch_(feature); this.clearFeatureEntryInLineStringBatch_(feature); const geometry = feature.getGeometry(); if (!geometry) { return; } this.addGeometry_(geometry, feature); } /** * @param {Feature|RenderFeature} feature 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;