From 8e862766fcb49553b644f7f82a01e54b4bcaa68e Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Sun, 20 Sep 2020 23:36:27 +0200 Subject: [PATCH] New decluttering implementation --- examples/offscreen-canvas.worker.js | 1 + src/ol/PluggableMap.js | 2 + src/ol/VectorRenderTile.js | 6 + src/ol/layer/BaseVector.js | 12 + src/ol/layer/Heatmap.js | 2 + src/ol/render/VectorContext.js | 6 +- src/ol/render/canvas.js | 17 +- src/ol/render/canvas/Builder.js | 12 +- src/ol/render/canvas/Executor.js | 428 ++++++++++++++------- src/ol/render/canvas/ExecutorGroup.js | 30 +- src/ol/render/canvas/ImageBuilder.js | 17 +- src/ol/render/canvas/LineStringBuilder.js | 2 +- src/ol/render/canvas/PolygonBuilder.js | 2 +- src/ol/render/canvas/TextBuilder.js | 15 +- src/ol/renderer/Composite.js | 11 +- src/ol/renderer/Layer.js | 5 + src/ol/renderer/Map.js | 5 +- src/ol/renderer/canvas/VectorImageLayer.js | 7 + src/ol/renderer/canvas/VectorLayer.js | 198 +++++++--- src/ol/renderer/canvas/VectorTileLayer.js | 206 ++++++---- src/ol/renderer/vector.js | 129 +++++-- src/ol/source/Raster.js | 1 + 22 files changed, 784 insertions(+), 330 deletions(-) diff --git a/examples/offscreen-canvas.worker.js b/examples/offscreen-canvas.worker.js index f37f334743..5adec394e5 100644 --- a/examples/offscreen-canvas.worker.js +++ b/examples/offscreen-canvas.worker.js @@ -144,6 +144,7 @@ worker.addEventListener('message', (event) => { renderer.renderFrame(frameState, canvas); } }); + layers.forEach((layer) => layer.renderDeclutter(frameState)); if (tileQueue.getTilesLoading() < maxTotalLoading) { tileQueue.reprioritize(); tileQueue.loadMoreTiles(maxTotalLoading, maxNewLoads); diff --git a/src/ol/PluggableMap.js b/src/ol/PluggableMap.js index 3bf3509ea7..e90cc38a2e 100644 --- a/src/ol/PluggableMap.js +++ b/src/ol/PluggableMap.js @@ -50,6 +50,7 @@ import {removeNode} from './dom.js'; * @property {import("./View.js").State} viewState The state of the current view. * @property {boolean} animate * @property {import("./transform.js").Transform} coordinateToPixelTransform + * @property {import("rbush").default} declutterTree * @property {null|import("./extent.js").Extent} extent * @property {number} index * @property {Array} layerStatesArray @@ -1372,6 +1373,7 @@ class PluggableMap extends BaseObject { frameState = { animate: false, coordinateToPixelTransform: this.coordinateToPixelTransform_, + declutterTree: null, extent: getForViewAndSize( viewState.center, viewState.resolution, diff --git a/src/ol/VectorRenderTile.js b/src/ol/VectorRenderTile.js index 782cd7860c..5fb388f092 100644 --- a/src/ol/VectorRenderTile.js +++ b/src/ol/VectorRenderTile.js @@ -45,6 +45,12 @@ class VectorRenderTile extends Tile { */ this.executorGroups = {}; + /** + * Executor groups for decluttering, by layer uid. Entries are read/written by the renderer. + * @type {Object>} + */ + this.declutterExecutorGroups = {}; + /** * Number of loading source tiles. Read/written by the source. * @type {number} diff --git a/src/ol/layer/BaseVector.js b/src/ol/layer/BaseVector.js index c0562bed93..20189f2483 100644 --- a/src/ol/layer/BaseVector.js +++ b/src/ol/layer/BaseVector.js @@ -2,6 +2,7 @@ * @module ol/layer/BaseVector */ import Layer from './Layer.js'; +import RBush from 'rbush'; import {assign} from '../obj.js'; import { createDefaultStyle, @@ -214,6 +215,17 @@ class BaseVectorLayer extends Layer { return this.updateWhileInteracting_; } + /** + * Render declutter items for this layer + * @param {import("../PluggableMap.js").FrameState} frameState Frame state. + */ + renderDeclutter(frameState) { + if (!frameState.declutterTree) { + frameState.declutterTree = new RBush(9); + } + /** @type {*} */ (this.getRenderer()).renderDeclutter(frameState); + } + /** * @param {import("../render.js").OrderFunction|null|undefined} renderOrder * Render order. diff --git a/src/ol/layer/Heatmap.js b/src/ol/layer/Heatmap.js index fca3dae345..258e322296 100644 --- a/src/ol/layer/Heatmap.js +++ b/src/ol/layer/Heatmap.js @@ -309,6 +309,8 @@ class Heatmap extends VectorLayer { ], }); } + + renderDeclutter() {} } /** diff --git a/src/ol/render/VectorContext.js b/src/ol/render/VectorContext.js index 4fc3a1f39e..bad1facc35 100644 --- a/src/ol/render/VectorContext.js +++ b/src/ol/render/VectorContext.js @@ -100,13 +100,15 @@ class VectorContext { /** * @param {import("../style/Image.js").default} imageStyle Image style. + * @param {Object=} opt_sharedData Shared data for combined decluttering with a text style. */ - setImageStyle(imageStyle) {} + setImageStyle(imageStyle, opt_sharedData) {} /** * @param {import("../style/Text.js").default} textStyle Text style. + * @param {Object=} opt_sharedData Shared data for combined decluttering with an image style. */ - setTextStyle(textStyle) {} + setTextStyle(textStyle, opt_sharedData) {} } export default VectorContext; diff --git a/src/ol/render/canvas.js b/src/ol/render/canvas.js index 7c4b00db70..27435dcd4c 100644 --- a/src/ol/render/canvas.js +++ b/src/ol/render/canvas.js @@ -67,6 +67,16 @@ import {toString} from '../transform.js'; * @property {Array} [padding] */ +/** + * @typedef {Object} SerializableInstructions + * @property {Array<*>} instructions The rendering instructions. + * @property {Array<*>} hitDetectionInstructions The rendering hit detection instructions. + * @property {Array} coordinates The array of all coordinates. + * @property {!Object} [textStates] The text states (decluttering). + * @property {!Object} [fillStates] The fill states (decluttering). + * @property {!Object} [strokeStates] The stroke states (decluttering). + */ + /** * @const * @type {string} @@ -276,9 +286,8 @@ export const measureTextHeight = (function () { * @type {HTMLDivElement} */ let div; - const heights = textHeights; return function (fontSpec) { - let height = heights[fontSpec]; + let height = textHeights[fontSpec]; if (height == undefined) { if (WORKER_OFFSCREEN_CANVAS) { const font = getFontParameters(fontSpec); @@ -286,7 +295,7 @@ export const measureTextHeight = (function () { const lineHeight = isNaN(Number(font.lineHeight)) ? 1.2 : Number(font.lineHeight); - textHeights[fontSpec] = + height = lineHeight * (metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent); } else { @@ -301,9 +310,9 @@ export const measureTextHeight = (function () { div.style.font = fontSpec; document.body.appendChild(div); height = div.offsetHeight; - heights[fontSpec] = height; document.body.removeChild(div); } + textHeights[fontSpec] = height; } return height; }; diff --git a/src/ol/render/canvas/Builder.js b/src/ol/render/canvas/Builder.js index 9d40e8e2b6..fd7e0e478c 100644 --- a/src/ol/render/canvas/Builder.js +++ b/src/ol/render/canvas/Builder.js @@ -29,16 +29,6 @@ import { inflateMultiCoordinatesArray, } from '../../geom/flat/inflate.js'; -/** - * @typedef {Object} SerializableInstructions - * @property {Array<*>} instructions The rendering instructions. - * @property {Array<*>} hitDetectionInstructions The rendering hit detection instructions. - * @property {Array} coordinates The array of all coordinates. - * @property {!Object} [textStates] The text states (decluttering). - * @property {!Object} [fillStates] The fill states (decluttering). - * @property {!Object} [strokeStates] The stroke states (decluttering). - */ - class CanvasBuilder extends VectorContext { /** * @param {number} tolerance Tolerance. @@ -383,7 +373,7 @@ class CanvasBuilder extends VectorContext { } /** - * @return {SerializableInstructions} the serializable instructions. + * @return {import("../canvas.js").SerializableInstructions} the serializable instructions. */ finish() { return { diff --git a/src/ol/render/canvas/Executor.js b/src/ol/render/canvas/Executor.js index b37ca24863..8bef5052dd 100644 --- a/src/ol/render/canvas/Executor.js +++ b/src/ol/render/canvas/Executor.js @@ -28,13 +28,25 @@ import {lineStringLength} from '../../geom/flat/length.js'; import {transform2D} from '../../geom/flat/transform.js'; /** - * @typedef {Object} SerializableInstructions - * @property {Array<*>} instructions The rendering instructions. - * @property {Array<*>} hitDetectionInstructions The rendering hit detection instructions. - * @property {Array} coordinates The array of all coordinates. - * @property {!Object} textStates The text states (decluttering). - * @property {!Object} fillStates The fill states (decluttering). - * @property {!Object} strokeStates The stroke states (decluttering). + * @typedef {Object} BBox + * @property {number} minX + * @property {number} minY + * @property {number} maxX + * @property {number} maxY + * @property {*} value + */ + +/** + * @typedef {Object} ImageOrLabelDimensions + * @property {number} drawImageX + * @property {number} drawImageY + * @property {number} drawImageW + * @property {number} drawImageH + * @property {number} originX + * @property {number} originY + * @property {Array} scale + * @property {BBox} declutterBox + * @property {import("../../transform.js").Transform} canvasTransform */ /** @@ -42,11 +54,6 @@ import {transform2D} from '../../geom/flat/transform.js'; */ const tmpExtent = createEmpty(); -/** - * @type {!import("../../transform.js").Transform} - */ -const tmpTransform = createTransform(); - /** @type {import("../../coordinate.js").Coordinate} */ const p1 = []; /** @type {import("../../coordinate.js").Coordinate} */ @@ -56,12 +63,21 @@ const p3 = []; /** @type {import("../../coordinate.js").Coordinate} */ const p4 = []; +/** + * @param {Array<*>} replayImageOrLabelArgs Arguments to replayImageOrLabel + * @return {BBox} Declutter bbox. + */ +function getDeclutterBox(replayImageOrLabelArgs) { + return /** @type {ImageOrLabelDimensions} */ (replayImageOrLabelArgs[3]) + .declutterBox; +} + class Executor { /** * @param {number} resolution Resolution. * @param {number} pixelRatio Pixel ratio. * @param {boolean} overlaps The replay can have overlapping geometries. - * @param {SerializableInstructions} instructions The serializable instructions + * @param {import("../canvas.js").SerializableInstructions} instructions The serializable instructions * @param {import("../../size.js").Size} renderBuffer Render buffer (width/height) in pixels. */ constructor(resolution, pixelRatio, overlaps, instructions, renderBuffer) { @@ -293,60 +309,49 @@ class Executor { /** * @private - * @param {CanvasRenderingContext2D} context Context. - * @param {number} contextScale Scale of the context. - * @param {number} x X. - * @param {number} y Y. - * @param {import("../canvas.js").Label|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} imageOrLabel Image. + * @param {number} sheetWidth Width of the sprite sheet. + * @param {number} sheetHeight Height of the sprite sheet. + * @param {number} centerX X. + * @param {number} centerY Y. + * @param {number} width Width. + * @param {number} height Height. * @param {number} anchorX Anchor X. * @param {number} anchorY Anchor Y. - * @param {number} height Height. - * @param {number} opacity Opacity. * @param {number} originX Origin X. * @param {number} originY Origin Y. * @param {number} rotation Rotation. * @param {import("../../size.js").Size} scale Scale. * @param {boolean} snapToPixel Snap to pixel. - * @param {number} width Width. * @param {Array} padding Padding. - * @param {Array<*>} fillInstruction Fill instruction. - * @param {Array<*>} strokeInstruction Stroke instruction. - * @return {boolean} The image or label was rendered. + * @param {boolean} fillStroke Background fill or stroke. + * @param {import("../../Feature.js").FeatureLike} feature Feature. + * @return {ImageOrLabelDimensions} Dimensions for positioning and decluttering the image or label. */ - replayImageOrLabel_( - context, - contextScale, - x, - y, - imageOrLabel, + calculateImageOrLabelDimensions_( + sheetWidth, + sheetHeight, + centerX, + centerY, + width, + height, anchorX, anchorY, - height, - opacity, originX, originY, rotation, scale, snapToPixel, - width, padding, - fillInstruction, - strokeInstruction + fillStroke, + feature ) { - const fillStroke = fillInstruction || strokeInstruction; anchorX *= scale[0]; anchorY *= scale[1]; - x -= anchorX; - y -= anchorY; + let x = centerX - anchorX; + let y = centerY - anchorY; - const w = - width + originX > imageOrLabel.width - ? imageOrLabel.width - originX - : width; - const h = - height + originY > imageOrLabel.height - ? imageOrLabel.height - originY - : height; + const w = width + originX > sheetWidth ? sheetWidth - originX : width; + const h = height + originY > sheetHeight ? sheetHeight - originY : height; const boxW = padding[3] + w * scale[0] + padding[1]; const boxH = padding[0] + h * scale[1] + padding[2]; const boxX = x - padding[3]; @@ -363,12 +368,10 @@ class Executor { p4[1] = p3[1]; } - let transform = null; + let transform; if (rotation !== 0) { - const centerX = x + anchorX; - const centerY = y + anchorY; transform = composeTransform( - tmpTransform, + createTransform(), centerX, centerY, 1, @@ -378,10 +381,10 @@ class Executor { -centerY ); - applyTransform(tmpTransform, p1); - applyTransform(tmpTransform, p2); - applyTransform(tmpTransform, p3); - applyTransform(tmpTransform, p4); + applyTransform(transform, p1); + applyTransform(transform, p2); + applyTransform(transform, p3); + applyTransform(transform, p4); createOrUpdate( Math.min(p1[0], p2[0], p3[0], p4[0]), Math.min(p1[1], p2[1], p3[1], p4[1]), @@ -398,24 +401,61 @@ class Executor { tmpExtent ); } - const renderBufferX = 0; // increase this.renderBuffer_ for decluttering - const renderBufferY = 0; // increase this.renderBuffer_ for decluttering - const canvas = context.canvas; - const strokePadding = strokeInstruction - ? (strokeInstruction[2] * scale[0]) / 2 - : 0; - const intersects = - tmpExtent[0] - strokePadding <= - (canvas.width + renderBufferX) / contextScale && - tmpExtent[2] + strokePadding >= -renderBufferX / contextScale && - tmpExtent[1] - strokePadding <= - (canvas.height + renderBufferY) / contextScale && - tmpExtent[3] + strokePadding >= -renderBufferY / contextScale; - if (snapToPixel) { x = Math.round(x); y = Math.round(y); } + return { + drawImageX: x, + drawImageY: y, + drawImageW: w, + drawImageH: h, + originX: originX, + originY: originY, + declutterBox: { + minX: tmpExtent[0], + minY: tmpExtent[1], + maxX: tmpExtent[2], + maxY: tmpExtent[3], + value: feature, + }, + canvasTransform: transform, + scale: scale, + }; + } + + /** + * @private + * @param {CanvasRenderingContext2D} context Context. + * @param {number} contextScale Scale of the context. + * @param {import("../canvas.js").Label|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} imageOrLabel Image. + * @param {ImageOrLabelDimensions} dimensions Dimensions. + * @param {number} opacity Opacity. + * @param {Array<*>} fillInstruction Fill instruction. + * @param {Array<*>} strokeInstruction Stroke instruction. + * @return {boolean} The image or label was rendered. + */ + replayImageOrLabel_( + context, + contextScale, + imageOrLabel, + dimensions, + opacity, + fillInstruction, + strokeInstruction + ) { + const fillStroke = !!(fillInstruction || strokeInstruction); + + const box = dimensions.declutterBox; + const canvas = context.canvas; + const strokePadding = strokeInstruction + ? (strokeInstruction[2] * dimensions.scale[0]) / 2 + : 0; + const intersects = + box.minX - strokePadding <= canvas.width / contextScale && + box.maxX + strokePadding >= 0 && + box.minY - strokePadding <= canvas.height / contextScale && + box.maxY + strokePadding >= 0; if (intersects) { if (fillStroke) { @@ -431,16 +471,16 @@ class Executor { } drawImageOrLabel( context, - transform, + dimensions.canvasTransform, opacity, imageOrLabel, - originX, - originY, - w, - h, - x, - y, - scale + dimensions.originX, + dimensions.originY, + dimensions.drawImageW, + dimensions.drawImageH, + dimensions.drawImageX, + dimensions.drawImageY, + dimensions.scale ); } return true; @@ -470,7 +510,9 @@ class Executor { * @param {Array<*>} instruction Instruction. */ setStrokeStyle_(context, instruction) { - context.strokeStyle = /** @type {import("../../colorlike.js").ColorLike} */ (instruction[1]); + context[ + 'strokeStyle' + ] = /** @type {import("../../colorlike.js").ColorLike} */ (instruction[1]); context.lineWidth = /** @type {number} */ (instruction[2]); context.lineCap = /** @type {CanvasLineCap} */ (instruction[3]); context.lineJoin = /** @type {CanvasLineJoin} */ (instruction[4]); @@ -525,6 +567,7 @@ class Executor { * @param {function(import("../../Feature.js").FeatureLike): T|undefined} featureCallback Feature callback. * @param {import("../../extent.js").Extent=} opt_hitExtent Only check features that intersect this * extent. + * @param {import("rbush").default=} opt_declutterTree Declutter tree. * @return {T|undefined} Callback result. * @template T */ @@ -535,7 +578,8 @@ class Executor { instructions, snapToPixel, featureCallback, - opt_hitExtent + opt_hitExtent, + opt_declutterTree ) { /** @type {Array} */ let pixelCoordinates; @@ -559,8 +603,17 @@ class Executor { const ii = instructions.length; // end of instructions let d = 0; // data index let dd; // end of per-instruction data - let anchorX, anchorY, prevX, prevY, roundX, roundY, image, text, textKey; - let strokeKey, fillKey; + let anchorX, + anchorY, + prevX, + prevY, + roundX, + roundY, + image, + text, + textKey, + strokeKey, + fillKey; let pendingFill = 0; let pendingStroke = 0; let lastFillInstruction = null; @@ -671,13 +724,14 @@ class Executor { let rotation = /** @type {number} */ (instruction[11]); const scale = /** @type {import("../../size.js").Size} */ (instruction[12]); let width = /** @type {number} */ (instruction[13]); + const sharedData = instruction[14]; - if (!image && instruction.length >= 18) { + if (!image && instruction.length >= 19) { // create label images - text = /** @type {string} */ (instruction[17]); - textKey = /** @type {string} */ (instruction[18]); - strokeKey = /** @type {string} */ (instruction[19]); - fillKey = /** @type {string} */ (instruction[20]); + text = /** @type {string} */ (instruction[18]); + textKey = /** @type {string} */ (instruction[19]); + strokeKey = /** @type {string} */ (instruction[20]); + fillKey = /** @type {string} */ (instruction[21]); const labelWithAnchor = this.drawLabelWithPointPlacement_( text, textKey, @@ -686,10 +740,10 @@ class Executor { ); image = labelWithAnchor.label; instruction[3] = image; - const textOffsetX = /** @type {number} */ (instruction[21]); + const textOffsetX = /** @type {number} */ (instruction[22]); anchorX = (labelWithAnchor.anchorX - textOffsetX) * this.pixelRatio; instruction[4] = anchorX; - const textOffsetY = /** @type {number} */ (instruction[22]); + const textOffsetY = /** @type {number} */ (instruction[23]); anchorY = (labelWithAnchor.anchorY - textOffsetY) * this.pixelRatio; instruction[5] = anchorY; height = image.height; @@ -699,15 +753,15 @@ class Executor { } let geometryWidths; - if (instruction.length > 23) { - geometryWidths = /** @type {number} */ (instruction[23]); + if (instruction.length > 24) { + geometryWidths = /** @type {number} */ (instruction[24]); } let padding, backgroundFill, backgroundStroke; - if (instruction.length > 15) { - padding = /** @type {Array} */ (instruction[14]); - backgroundFill = /** @type {boolean} */ (instruction[15]); - backgroundStroke = /** @type {boolean} */ (instruction[16]); + if (instruction.length > 16) { + padding = /** @type {Array} */ (instruction[15]); + backgroundFill = /** @type {boolean} */ (instruction[16]); + backgroundStroke = /** @type {boolean} */ (instruction[17]); } else { padding = defaultPadding; backgroundFill = false; @@ -729,30 +783,67 @@ class Executor { ) { continue; } - this.replayImageOrLabel_( - context, - contextScale, + const dimensions = this.calculateImageOrLabelDimensions_( + image.width, + image.height, pixelCoordinates[d], pixelCoordinates[d + 1], - image, + width, + height, anchorX, anchorY, - height, - opacity, originX, originY, rotation, scale, snapToPixel, - width, padding, + backgroundFill || backgroundStroke, + feature + ); + const args = [ + context, + contextScale, + image, + dimensions, + opacity, backgroundFill ? /** @type {Array<*>} */ (lastFillInstruction) : null, backgroundStroke ? /** @type {Array<*>} */ (lastStrokeInstruction) - : null - ); + : null, + ]; + let imageArgs; + let imageDeclutterBox; + if (opt_declutterTree && sharedData) { + if (!sharedData[d]) { + sharedData[d] = args; + continue; + } + imageArgs = sharedData[d]; + delete sharedData[d]; + imageDeclutterBox = getDeclutterBox(imageArgs); + if (opt_declutterTree.collides(imageDeclutterBox)) { + continue; + } + } + if ( + opt_declutterTree && + opt_declutterTree.collides(dimensions.declutterBox) + ) { + continue; + } + if (imageArgs) { + if (opt_declutterTree) { + opt_declutterTree.insert(imageDeclutterBox); + } + this.replayImageOrLabel_.apply(this, imageArgs); + } + if (opt_declutterTree) { + opt_declutterTree.insert(dimensions.declutterBox); + } + this.replayImageOrLabel_.apply(this, args); } ++i; break; @@ -810,8 +901,8 @@ class Executor { cachedWidths, viewRotationFromTransform ? 0 : this.viewRotation_ ); - if (parts) { - let rendered = false; + drawChars: if (parts) { + const replayImageOrLabelArgs = []; let c, cc, chars, label, part; if (strokeKey) { for (c = 0, cc = parts.length; c < cc; ++c) { @@ -824,27 +915,39 @@ class Executor { ((0.5 - baseline) * 2 * strokeWidth * textScale[1]) / textScale[0] - offsetY; - rendered = - this.replayImageOrLabel_( - context, - contextScale, - /** @type {number} */ (part[0]), - /** @type {number} */ (part[1]), - label, - anchorX, - anchorY, - label.height, - 1, - 0, - 0, - /** @type {number} */ (part[3]), - pixelRatioScale, - false, - label.width, - defaultPadding, - null, - null - ) || rendered; + const dimensions = this.calculateImageOrLabelDimensions_( + label.width, + label.height, + part[0], + part[1], + label.width, + label.height, + anchorX, + anchorY, + 0, + 0, + part[3], + pixelRatioScale, + false, + defaultPadding, + false, + feature + ); + if ( + opt_declutterTree && + opt_declutterTree.collides(dimensions.declutterBox) + ) { + break drawChars; + } + replayImageOrLabelArgs.push([ + context, + contextScale, + label, + dimensions, + 1, + null, + null, + ]); } } if (fillKey) { @@ -854,29 +957,49 @@ class Executor { label = this.createLabel(chars, textKey, fillKey, ''); anchorX = /** @type {number} */ (part[2]); anchorY = baseline * label.height - offsetY; - rendered = - this.replayImageOrLabel_( - context, - contextScale, - /** @type {number} */ (part[0]), - /** @type {number} */ (part[1]), - label, - anchorX, - anchorY, - label.height, - 1, - 0, - 0, - /** @type {number} */ (part[3]), - pixelRatioScale, - false, - label.width, - defaultPadding, - null, - null - ) || rendered; + const dimensions = this.calculateImageOrLabelDimensions_( + label.width, + label.height, + part[0], + part[1], + label.width, + label.height, + anchorX, + anchorY, + 0, + 0, + part[3], + pixelRatioScale, + false, + defaultPadding, + false, + feature + ); + if ( + opt_declutterTree && + opt_declutterTree.collides(dimensions.declutterBox) + ) { + break drawChars; + } + replayImageOrLabelArgs.push([ + context, + contextScale, + label, + dimensions, + 1, + null, + null, + ]); } } + if (opt_declutterTree) { + opt_declutterTree.load( + replayImageOrLabelArgs.map(getDeclutterBox) + ); + } + for (let i = 0, ii = replayImageOrLabelArgs.length; i < ii; ++i) { + this.replayImageOrLabel_.apply(this, replayImageOrLabelArgs[i]); + } } } ++i; @@ -977,8 +1100,16 @@ class Executor { * @param {import("../../transform.js").Transform} transform Transform. * @param {number} viewRotation View rotation. * @param {boolean} snapToPixel Snap point symbols and text to integer pixels. + * @param {import("rbush").default=} opt_declutterTree Declutter tree. */ - execute(context, contextScale, transform, viewRotation, snapToPixel) { + execute( + context, + contextScale, + transform, + viewRotation, + snapToPixel, + opt_declutterTree + ) { this.viewRotation_ = viewRotation; this.execute_( context, @@ -987,7 +1118,8 @@ class Executor { this.instructions, snapToPixel, undefined, - undefined + undefined, + opt_declutterTree ); } diff --git a/src/ol/render/canvas/ExecutorGroup.js b/src/ol/render/canvas/ExecutorGroup.js index 427d17b4a5..f714327b45 100644 --- a/src/ol/render/canvas/ExecutorGroup.js +++ b/src/ol/render/canvas/ExecutorGroup.js @@ -36,7 +36,7 @@ class ExecutorGroup { * @param {number} resolution Resolution. * @param {number} pixelRatio Pixel ratio. * @param {boolean} overlaps The executor group can have overlapping geometries. - * @param {!Object>} allInstructions + * @param {!Object>} allInstructions * The serializable instructions. * @param {number=} opt_renderBuffer Optional rendering buffer. */ @@ -116,7 +116,7 @@ class ExecutorGroup { /** * Create executors and populate them using the provided instructions. * @private - * @param {!Object>} allInstructions The serializable instructions + * @param {!Object>} allInstructions The serializable instructions */ createExecutors_(allInstructions) { for (const zIndex in allInstructions) { @@ -162,6 +162,7 @@ class ExecutorGroup { * @param {number} rotation Rotation. * @param {number} hitTolerance Hit tolerance in pixels. * @param {function(import("../../Feature.js").FeatureLike): T} callback Feature callback. + * @param {Array} declutteredFeatures Decluttered features. * @return {T|undefined} Callback result. * @template T */ @@ -170,7 +171,8 @@ class ExecutorGroup { resolution, rotation, hitTolerance, - callback + callback, + declutteredFeatures ) { hitTolerance = Math.round(hitTolerance); const contextSize = hitTolerance * 2 + 1; @@ -232,7 +234,17 @@ class ExecutorGroup { for (let j = 0; j < contextSize; j++) { if (mask[i][j]) { if (imageData[(j * contextSize + i) * 4 + 3] > 0) { - const result = callback(feature); + let result; + if ( + !( + declutteredFeatures && + (builderType == BuilderType.IMAGE || + builderType == BuilderType.TEXT) + ) || + declutteredFeatures.indexOf(feature) !== -1 + ) { + result = callback(feature); + } if (result) { return result; } else { @@ -306,6 +318,7 @@ class ExecutorGroup { * @param {boolean} snapToPixel Snap point symbols and test to integer pixel. * @param {Array=} opt_builderTypes Ordered replay types to replay. * Default is {@link module:ol/render/replay~ORDER} + * @param {import("rbush").default=} opt_declutterTree Declutter tree. */ execute( context, @@ -313,7 +326,8 @@ class ExecutorGroup { transform, viewRotation, snapToPixel, - opt_builderTypes + opt_builderTypes, + opt_declutterTree ) { /** @type {Array} */ const zs = Object.keys(this.executorsByZIndex_).map(Number); @@ -328,6 +342,9 @@ class ExecutorGroup { const builderTypes = opt_builderTypes ? opt_builderTypes : ORDER; let i, ii, j, jj, replays, replay; + if (opt_declutterTree) { + zs.reverse(); + } for (i = 0, ii = zs.length; i < ii; ++i) { const zIndexKey = zs[i].toString(); replays = this.executorsByZIndex_[zIndexKey]; @@ -340,7 +357,8 @@ class ExecutorGroup { contextScale, transform, viewRotation, - snapToPixel + snapToPixel, + opt_declutterTree ); } } diff --git a/src/ol/render/canvas/ImageBuilder.js b/src/ol/render/canvas/ImageBuilder.js index 28ad1e10ee..855d80e886 100644 --- a/src/ol/render/canvas/ImageBuilder.js +++ b/src/ol/render/canvas/ImageBuilder.js @@ -91,6 +91,13 @@ class CanvasImageBuilder extends CanvasBuilder { * @type {number|undefined} */ this.width_ = undefined; + + /** + * Data shared with a text builder for combined decluttering. + * @private + * @type {Object} + */ + this.sharedData_ = undefined; } /** @@ -125,6 +132,7 @@ class CanvasImageBuilder extends CanvasBuilder { (this.scale_[1] * this.pixelRatio) / this.imagePixelRatio_, ], Math.ceil(this.width_ * this.imagePixelRatio_), + this.sharedData_, ]); this.hitDetectionInstructions.push([ CanvasInstruction.DRAW_IMAGE, @@ -142,6 +150,7 @@ class CanvasImageBuilder extends CanvasBuilder { this.rotation_, this.scale_, this.width_, + this.sharedData_, ]); this.endGeometry(feature); } @@ -178,6 +187,7 @@ class CanvasImageBuilder extends CanvasBuilder { (this.scale_[1] * this.pixelRatio) / this.imagePixelRatio_, ], Math.ceil(this.width_ * this.imagePixelRatio_), + this.sharedData_, ]); this.hitDetectionInstructions.push([ CanvasInstruction.DRAW_IMAGE, @@ -195,12 +205,13 @@ class CanvasImageBuilder extends CanvasBuilder { this.rotation_, this.scale_, this.width_, + this.sharedData_, ]); this.endGeometry(feature); } /** - * @return {import("./Builder.js").SerializableInstructions} the serializable instructions. + * @return {import("../canvas.js").SerializableInstructions} the serializable instructions. */ finish() { this.reverseHitDetectionInstructions(); @@ -223,8 +234,9 @@ class CanvasImageBuilder extends CanvasBuilder { /** * @param {import("../../style/Image.js").default} imageStyle Image style. + * @param {Object=} opt_sharedData Shared data. */ - setImageStyle(imageStyle) { + setImageStyle(imageStyle, opt_sharedData) { const anchor = imageStyle.getAnchor(); const size = imageStyle.getSize(); const hitDetectionImage = imageStyle.getHitDetectionImage(); @@ -243,6 +255,7 @@ class CanvasImageBuilder extends CanvasBuilder { this.rotation_ = imageStyle.getRotation(); this.scale_ = imageStyle.getScaleArray(); this.width_ = size[0]; + this.sharedData_ = opt_sharedData; } } diff --git a/src/ol/render/canvas/LineStringBuilder.js b/src/ol/render/canvas/LineStringBuilder.js index ae8850d894..b64021e722 100644 --- a/src/ol/render/canvas/LineStringBuilder.js +++ b/src/ol/render/canvas/LineStringBuilder.js @@ -127,7 +127,7 @@ class CanvasLineStringBuilder extends CanvasBuilder { } /** - * @return {import("./Builder.js").SerializableInstructions} the serializable instructions. + * @return {import("../canvas.js").SerializableInstructions} the serializable instructions. */ finish() { const state = this.state; diff --git a/src/ol/render/canvas/PolygonBuilder.js b/src/ol/render/canvas/PolygonBuilder.js index 4b182289c1..a09f0ea7ee 100644 --- a/src/ol/render/canvas/PolygonBuilder.js +++ b/src/ol/render/canvas/PolygonBuilder.js @@ -220,7 +220,7 @@ class CanvasPolygonBuilder extends CanvasBuilder { } /** - * @return {import("./Builder.js").SerializableInstructions} the serializable instructions. + * @return {import("../canvas.js").SerializableInstructions} the serializable instructions. */ finish() { this.reverseHitDetectionInstructions(); diff --git a/src/ol/render/canvas/TextBuilder.js b/src/ol/render/canvas/TextBuilder.js index 903d6dcee3..55a602c74a 100644 --- a/src/ol/render/canvas/TextBuilder.js +++ b/src/ol/render/canvas/TextBuilder.js @@ -138,10 +138,17 @@ class CanvasTextBuilder extends CanvasBuilder { * @type {string} */ this.strokeKey_ = ''; + + /** + * Data shared with an image builder for combined decluttering. + * @private + * @type {Object} + */ + this.sharedData_ = undefined; } /** - * @return {import("./Builder.js").SerializableInstructions} the serializable instructions. + * @return {import("../canvas.js").SerializableInstructions} the serializable instructions. */ finish() { const instructions = super.finish(); @@ -328,6 +335,7 @@ class CanvasTextBuilder extends CanvasBuilder { this.textRotation_, [1, 1], NaN, + this.sharedData_, padding == defaultPadding ? defaultPadding : padding.map(function (p) { @@ -359,6 +367,7 @@ class CanvasTextBuilder extends CanvasBuilder { this.textRotation_, [scale, scale], NaN, + this.sharedData_, padding, !!textState.backgroundFill, !!textState.backgroundStroke, @@ -475,8 +484,9 @@ class CanvasTextBuilder extends CanvasBuilder { /** * @param {import("../../style/Text.js").default} textStyle Text style. + * @param {Object=} opt_sharedData Shared data. */ - setTextStyle(textStyle) { + setTextStyle(textStyle, opt_sharedData) { let textState, fillState, strokeState; if (!textStyle) { this.text_ = ''; @@ -576,6 +586,7 @@ class CanvasTextBuilder extends CanvasBuilder { : '|' + getUid(fillState.fillStyle) : ''; } + this.sharedData_ = opt_sharedData; } } diff --git a/src/ol/renderer/Composite.js b/src/ol/renderer/Composite.js index 451dc450e9..40e56e37eb 100644 --- a/src/ol/renderer/Composite.js +++ b/src/ol/renderer/Composite.js @@ -102,6 +102,10 @@ class CompositeMapRenderer extends MapRenderer { const viewState = frameState.viewState; this.children_.length = 0; + /** + * @type {Array} + */ + const declutterLayers = []; let previousElement = null; for (let i = 0, ii = layerStatesArray.length; i < ii; ++i) { const layerState = layerStatesArray[i]; @@ -123,8 +127,13 @@ class CompositeMapRenderer extends MapRenderer { this.children_.push(element); previousElement = element; } + if ('getDeclutter' in layer) { + declutterLayers.push(layer); + } + } + for (let i = declutterLayers.length - 1; i >= 0; --i) { + declutterLayers[i].renderDeclutter(frameState); } - super.renderFrame(frameState); replaceChildren(this.element_, this.children_); diff --git a/src/ol/renderer/Layer.js b/src/ol/renderer/Layer.js index c6efd2711a..1997718683 100644 --- a/src/ol/renderer/Layer.js +++ b/src/ol/renderer/Layer.js @@ -25,6 +25,11 @@ class LayerRenderer extends Observable { * @type {LayerType} */ this.layer_ = layer; + + /** + * @type {import("../render/canvas/ExecutorGroup").default} + */ + this.declutterExecutorGroup = null; } /** diff --git a/src/ol/renderer/Map.js b/src/ol/renderer/Map.js index 8ad03b14ca..28b676b6f4 100644 --- a/src/ol/renderer/Map.js +++ b/src/ol/renderer/Map.js @@ -211,9 +211,12 @@ class MapRenderer extends Disposable { /** * Render. + * @abstract * @param {?import("../PluggableMap.js").FrameState} frameState Frame state. */ - renderFrame(frameState) {} + renderFrame(frameState) { + abstract(); + } /** * @param {import("../PluggableMap.js").FrameState} frameState Frame state. diff --git a/src/ol/renderer/canvas/VectorImageLayer.js b/src/ol/renderer/canvas/VectorImageLayer.js index 5bc4f0540f..bcc4ce4d85 100644 --- a/src/ol/renderer/canvas/VectorImageLayer.js +++ b/src/ol/renderer/canvas/VectorImageLayer.js @@ -6,6 +6,7 @@ import CanvasVectorLayerRenderer from './VectorLayer.js'; import EventType from '../../events/EventType.js'; import ImageCanvas from '../../ImageCanvas.js'; import ImageState from '../../ImageState.js'; +import RBush from 'rbush'; import ViewHint from '../../ViewHint.js'; import {apply, compose, create} from '../../transform.js'; import {assign} from '../../obj.js'; @@ -114,6 +115,7 @@ class CanvasVectorImageLayerRenderer extends CanvasImageLayerRenderer { {}, frameState, { + declutterTree: new RBush(9), extent: renderedExtent, size: [width, height], viewState: /** @type {import("../../View.js").State} */ (assign( @@ -137,6 +139,7 @@ class CanvasVectorImageLayerRenderer extends CanvasImageLayerRenderer { ) { vectorRenderer.clipping = false; vectorRenderer.renderFrame(imageFrameState, null); + vectorRenderer.renderDeclutter(imageFrameState); callback(); } } @@ -183,6 +186,10 @@ class CanvasVectorImageLayerRenderer extends CanvasImageLayerRenderer { */ postRender() {} + /** + */ + renderDeclutter() {} + /** * @param {import("../../coordinate.js").Coordinate} coordinate Coordinate. * @param {import("../../PluggableMap.js").FrameState} frameState Frame state. diff --git a/src/ol/renderer/canvas/VectorLayer.js b/src/ol/renderer/canvas/VectorLayer.js index 4527c4c5d0..0939d7bd3e 100644 --- a/src/ol/renderer/canvas/VectorLayer.js +++ b/src/ol/renderer/canvas/VectorLayer.js @@ -128,6 +128,11 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { */ this.replayGroupChanged = true; + /** + * @type {import("../../render/canvas/ExecutorGroup").default} + */ + this.declutterExecutorGroup = null; + /** * Clipping to be performed by `renderFrame()` * @type {boolean} @@ -148,6 +153,73 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { super.useContainer(target, transform, opacity); } + /** + * @param {ExecutorGroup} executorGroup Executor group. + * @param {import("../../PluggableMap.js").FrameState} frameState Frame state. + * @param {import("rbush").default=} opt_declutterTree Declutter tree. + */ + renderWorlds(executorGroup, frameState, opt_declutterTree) { + const extent = frameState.extent; + const viewState = frameState.viewState; + const center = viewState.center; + const resolution = viewState.resolution; + const projection = viewState.projection; + const rotation = viewState.rotation; + const projectionExtent = projection.getExtent(); + const vectorSource = this.getLayer().getSource(); + const pixelRatio = frameState.pixelRatio; + const viewHints = frameState.viewHints; + const snapToPixel = !( + viewHints[ViewHint.ANIMATING] || viewHints[ViewHint.INTERACTING] + ); + const context = this.context; + const width = Math.round(frameState.size[0] * pixelRatio); + const height = Math.round(frameState.size[1] * pixelRatio); + + const multiWorld = vectorSource.getWrapX() && projection.canWrapX(); + const worldWidth = multiWorld ? getWidth(projectionExtent) : null; + const endWorld = multiWorld + ? Math.ceil((extent[2] - projectionExtent[2]) / worldWidth) + 1 + : 1; + let world = multiWorld + ? Math.floor((extent[0] - projectionExtent[0]) / worldWidth) + : 0; + do { + const transform = this.getRenderTransform( + center, + resolution, + rotation, + pixelRatio, + width, + height, + world * worldWidth + ); + executorGroup.execute( + context, + 1, + transform, + rotation, + snapToPixel, + undefined, + opt_declutterTree + ); + } while (++world < endWorld); + } + + /** + * Render declutter items for this layer + * @param {import("../../PluggableMap.js").FrameState} frameState Frame state. + */ + renderDeclutter(frameState) { + if (this.declutterExecutorGroup) { + this.renderWorlds( + this.declutterExecutorGroup, + frameState, + frameState.declutterTree + ); + } + } + /** * Render the layer. * @param {import("../../PluggableMap.js").FrameState} frameState Frame state. @@ -169,7 +241,11 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { const canvas = context.canvas; const replayGroup = this.replayGroup_; - if (!replayGroup || replayGroup.isEmpty()) { + const declutterExecutorGroup = this.declutterExecutorGroup; + if ( + (!replayGroup || replayGroup.isEmpty()) && + (!declutterExecutorGroup || declutterExecutorGroup.isEmpty()) + ) { if (!this.containerReused && canvas.width > 0) { canvas.width = 0; } @@ -191,14 +267,8 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { this.preRender(context, frameState); - const extent = frameState.extent; const viewState = frameState.viewState; - const center = viewState.center; - const resolution = viewState.resolution; const projection = viewState.projection; - const rotation = viewState.rotation; - const projectionExtent = projection.getExtent(); - const vectorSource = this.getLayer().getSource(); // clipped rendering if layer extent is set let clipped = false; @@ -212,38 +282,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { } } - const viewHints = frameState.viewHints; - const snapToPixel = !( - viewHints[ViewHint.ANIMATING] || viewHints[ViewHint.INTERACTING] - ); - - const multiWorld = vectorSource.getWrapX() && projection.canWrapX(); - const worldWidth = multiWorld ? getWidth(projectionExtent) : null; - const endWorld = multiWorld - ? Math.ceil((extent[2] - projectionExtent[2]) / worldWidth) + 1 - : 1; - let world = multiWorld - ? Math.floor((extent[0] - projectionExtent[0]) / worldWidth) - : 0; - do { - const transform = this.getRenderTransform( - center, - resolution, - rotation, - pixelRatio, - width, - height, - world * worldWidth - ); - replayGroup.execute( - context, - 1, - transform, - rotation, - snapToPixel, - undefined - ); - } while (++world < endWorld); + this.renderWorlds(replayGroup, frameState); if (clipped) { context.restore(); @@ -378,26 +417,41 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { const resolution = frameState.viewState.resolution; const rotation = frameState.viewState.rotation; const layer = this.getLayer(); + /** @type {!Object} */ const features = {}; - const result = this.replayGroup_.forEachFeatureAtCoordinate( - coordinate, - resolution, - rotation, - hitTolerance, - /** - * @param {import("../../Feature.js").FeatureLike} feature Feature. - * @return {?} Callback result. - */ - function (feature) { - const key = getUid(feature); - if (!(key in features)) { - features[key] = true; - return callback(feature, layer); - } + /** + * @param {import("../../Feature.js").FeatureLike} feature Feature. + * @return {?} Callback result. + */ + const featureCallback = function (feature) { + const key = getUid(feature); + if (!(key in features)) { + features[key] = true; + return callback(feature, layer); } - ); + }; + + let result; + const executorGroups = [this.replayGroup_]; + if (this.declutterExecutorGroup) { + executorGroups.push(this.declutterExecutorGroup); + } + executorGroups.forEach((executorGroup) => { + result = + result || + executorGroup.forEachFeatureAtCoordinate( + coordinate, + resolution, + rotation, + hitTolerance, + featureCallback, + executorGroup === this.declutterExecutorGroup + ? frameState.declutterTree.all().map((item) => item.value) + : null + ); + }); return result; } @@ -531,6 +585,16 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { pixelRatio ); + let declutterBuilderGroup; + if (this.getLayer().getDeclutter()) { + declutterBuilderGroup = new CanvasBuilderGroup( + getRenderTolerance(resolution, pixelRatio), + extent, + resolution, + pixelRatio + ); + } + const userProjection = getUserProjection(); let userTransform; if (userProjection) { @@ -568,7 +632,8 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { squaredTolerance, styles, replayGroup, - userTransform + userTransform, + declutterBuilderGroup ); this.dirty_ = this.dirty_ || dirty; } @@ -595,6 +660,17 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { vectorLayer.getRenderBuffer() ); + if (declutterBuilderGroup) { + this.declutterExecutorGroup = new ExecutorGroup( + extent, + resolution, + pixelRatio, + vectorSource.getOverlaps(), + declutterBuilderGroup.finish(), + vectorLayer.getRenderBuffer() + ); + } + this.renderedResolution_ = resolution; this.renderedRevision_ = vectorLayerRevision; this.renderedRenderOrder_ = vectorLayerRenderOrder; @@ -614,6 +690,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { * @param {import("../../style/Style.js").default|Array} styles The style or array of styles. * @param {import("../../render/canvas/BuilderGroup.js").default} builderGroup Builder group. * @param {import("../../proj.js").TransformFunction=} opt_transform Transform from user to view projection. + * @param {import("../../render/canvas/BuilderGroup.js").default=} opt_declutterBuilderGroup Builder for decluttering. * @return {boolean} `true` if an image is loading. */ renderFeature( @@ -621,7 +698,8 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { squaredTolerance, styles, builderGroup, - opt_transform + opt_transform, + opt_declutterBuilderGroup ) { if (!styles) { return false; @@ -636,7 +714,8 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { styles[i], squaredTolerance, this.boundHandleStyleImageChange_, - opt_transform + opt_transform, + opt_declutterBuilderGroup ) || loading; } } else { @@ -646,7 +725,8 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { styles, squaredTolerance, this.boundHandleStyleImageChange_, - opt_transform + opt_transform, + opt_declutterBuilderGroup ); } return loading; diff --git a/src/ol/renderer/canvas/VectorTileLayer.js b/src/ol/renderer/canvas/VectorTileLayer.js index f17e3cfb64..71d2a9a6f9 100644 --- a/src/ol/renderer/canvas/VectorTileLayer.js +++ b/src/ol/renderer/canvas/VectorTileLayer.js @@ -260,6 +260,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { } const source = layer.getSource(); + const declutter = layer.getDeclutter(); const sourceTileGrid = source.getTileGrid(); const tileGrid = source.getTileGridForProjection(projection); const tileExtent = tileGrid.getTileCoordExtent(tile.wrappedTileCoord); @@ -268,6 +269,9 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { const layerUid = getUid(layer); delete tile.hitDetectionImageData[layerUid]; tile.executorGroups[layerUid] = []; + if (declutter) { + tile.declutterExecutorGroups[layerUid] = []; + } for (let t = 0, tt = sourceTiles.length; t < tt; ++t) { const sourceTile = sourceTiles[t]; if (sourceTile.getState() != TileState.LOADED) { @@ -292,6 +296,9 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { resolution, pixelRatio ); + const declutterBuilderGroup = declutter + ? new CanvasBuilderGroup(0, sharedExtent, resolution, pixelRatio) + : undefined; const squaredTolerance = getSquaredRenderTolerance( resolution, pixelRatio @@ -313,7 +320,8 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { feature, squaredTolerance, styles, - builderGroup + builderGroup, + declutterBuilderGroup ); this.dirty_ = this.dirty_ || dirty; builderState.dirty = builderState.dirty || dirty; @@ -337,7 +345,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { // no need to clip when the render tile is covered by a single source tile const replayExtent = layer.getRenderMode() !== VectorTileRenderType.VECTOR && - layer.getDeclutter() && + declutter && sourceTiles.length === 1 ? null : sharedExtent; @@ -350,6 +358,17 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { layer.getRenderBuffer() ); tile.executorGroups[layerUid].push(renderingReplayGroup); + if (declutterBuilderGroup) { + const declutterExecutorGroup = new CanvasExecutorGroup( + replayExtent, + resolution, + pixelRatio, + source.getOverlaps(), + declutterBuilderGroup.finish(), + layer.getRenderBuffer() + ); + tile.declutterExecutorGroups[layerUid].push(declutterExecutorGroup); + } } builderState.renderedRevision = revision; builderState.renderedZ = tile.sourceZ; @@ -395,34 +414,44 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { continue; } } - const executorGroups = tile.executorGroups[getUid(layer)]; - for (let t = 0, tt = executorGroups.length; t < tt; ++t) { - const executorGroup = executorGroups[t]; - found = - found || - executorGroup.forEachFeatureAtCoordinate( - coordinate, - resolution, - rotation, - hitTolerance, - /** - * @param {import("../../Feature.js").FeatureLike} feature Feature. - * @return {?} Callback result. - */ - function (feature) { - if (tileContainsCoordinate) { - let key = feature.getId(); - if (key === undefined) { - key = getUid(feature); - } - if (!(key in features)) { - features[key] = true; - return callback(feature, layer); - } - } - } - ); + const layerUid = getUid(layer); + const executorGroups = [tile.executorGroups[layerUid]]; + const declutterExecutorGroups = tile.declutterExecutorGroups[layerUid]; + if (declutterExecutorGroups) { + executorGroups.push(declutterExecutorGroups); } + executorGroups.forEach((executorGroups) => { + for (let t = 0, tt = executorGroups.length; t < tt; ++t) { + const executorGroup = executorGroups[t]; + found = + found || + executorGroup.forEachFeatureAtCoordinate( + coordinate, + resolution, + rotation, + hitTolerance, + /** + * @param {import("../../Feature.js").FeatureLike} feature Feature. + * @return {?} Callback result. + */ + function (feature) { + if (tileContainsCoordinate) { + let key = feature.getId(); + if (key === undefined) { + key = getUid(feature); + } + if (!(key in features)) { + features[key] = true; + return callback(feature, layer); + } + } + }, + executorGroups === declutterExecutorGroups + ? frameState.declutterTree.all().map((item) => item.value) + : null + ); + } + }); } return found; } @@ -539,6 +568,70 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { this.renderIfReadyAndVisible(); } + /** + * Render declutter items for this layer + * @param {import("../../PluggableMap.js").FrameState} frameState Frame state. + */ + renderDeclutter(frameState) { + const viewHints = frameState.viewHints; + const hifi = !( + viewHints[ViewHint.ANIMATING] || viewHints[ViewHint.INTERACTING] + ); + const tiles = /** @type {Array} */ (this + .renderedTiles); + for (let i = 0, ii = tiles.length; i < ii; ++i) { + const tile = tiles[i]; + const declutterExecutorGroups = + tile.declutterExecutorGroups[getUid(this.getLayer())]; + if (declutterExecutorGroups) { + for (let j = declutterExecutorGroups.length - 1; j >= 0; --j) { + declutterExecutorGroups[j].execute( + this.context, + 1, + this.getTileRenderTransform(tile, frameState), + frameState.viewState.rotation, + hifi, + undefined, + frameState.declutterTree + ); + } + } + } + } + + getTileRenderTransform(tile, frameState) { + const pixelRatio = frameState.pixelRatio; + const viewState = frameState.viewState; + const center = viewState.center; + const resolution = viewState.resolution; + const rotation = viewState.rotation; + const size = frameState.size; + const width = Math.round(size[0] * pixelRatio); + const height = Math.round(size[1] * pixelRatio); + + const source = this.getLayer().getSource(); + const tileGrid = source.getTileGridForProjection( + frameState.viewState.projection + ); + const tileCoord = tile.tileCoord; + const tileExtent = tileGrid.getTileCoordExtent(tile.wrappedTileCoord); + const worldOffset = + tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent)[0] - tileExtent[0]; + const transform = multiply( + scale(this.inversePixelTransform.slice(), 1 / pixelRatio, 1 / pixelRatio), + this.getRenderTransform( + center, + resolution, + rotation, + pixelRatio, + width, + height, + worldOffset + ) + ); + return transform; + } + /** * Render the layer. * @param {import("../../PluggableMap.js").FrameState} frameState Frame state. @@ -573,47 +666,17 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { const context = this.context; const replayTypes = VECTOR_REPLAYS[renderMode]; - const pixelRatio = frameState.pixelRatio; const viewState = frameState.viewState; - const center = viewState.center; - const resolution = viewState.resolution; const rotation = viewState.rotation; - const size = frameState.size; - - const width = Math.round(size[0] * pixelRatio); - const height = Math.round(size[1] * pixelRatio); const tiles = this.renderedTiles; - const tileGrid = source.getTileGridForProjection( - frameState.viewState.projection - ); const clips = []; const clipZs = []; for (let i = tiles.length - 1; i >= 0; --i) { const tile = /** @type {import("../../VectorRenderTile.js").default} */ (tiles[ i ]); - const tileCoord = tile.tileCoord; - const tileExtent = tileGrid.getTileCoordExtent(tile.wrappedTileCoord); - const worldOffset = - tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent)[0] - - tileExtent[0]; - const transform = multiply( - scale( - this.inversePixelTransform.slice(), - 1 / pixelRatio, - 1 / pixelRatio - ), - this.getRenderTransform( - center, - resolution, - rotation, - pixelRatio, - width, - height, - worldOffset - ) - ); + const transform = this.getTileRenderTransform(tile, frameState); const executorGroups = tile.executorGroups[getUid(layer)]; let clipped = false; for (let t = 0, tt = executorGroups.length; t < tt; ++t) { @@ -692,10 +755,17 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { * @param {import("../../Feature.js").FeatureLike} feature Feature. * @param {number} squaredTolerance Squared tolerance. * @param {import("../../style/Style.js").default|Array} styles The style or array of styles. - * @param {import("../../render/canvas/BuilderGroup.js").default} executorGroup Replay group. + * @param {import("../../render/canvas/BuilderGroup.js").default} builderGroup Replay group. + * @param {import("../../render/canvas/BuilderGroup.js").default=} opt_declutterBuilderGroup Builder group for decluttering. * @return {boolean} `true` if an image is loading. */ - renderFeature(feature, squaredTolerance, styles, executorGroup) { + renderFeature( + feature, + squaredTolerance, + styles, + builderGroup, + opt_declutterBuilderGroup + ) { if (!styles) { return false; } @@ -704,20 +774,24 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { for (let i = 0, ii = styles.length; i < ii; ++i) { loading = renderFeature( - executorGroup, + builderGroup, feature, styles[i], squaredTolerance, - this.boundHandleStyleImageChange_ + this.boundHandleStyleImageChange_, + undefined, + opt_declutterBuilderGroup ) || loading; } } else { loading = renderFeature( - executorGroup, + builderGroup, feature, styles, squaredTolerance, - this.boundHandleStyleImageChange_ + this.boundHandleStyleImageChange_, + undefined, + opt_declutterBuilderGroup ); } return loading; diff --git a/src/ol/renderer/vector.js b/src/ol/renderer/vector.js index b293c53f9f..0e01263ac0 100644 --- a/src/ol/renderer/vector.js +++ b/src/ol/renderer/vector.js @@ -62,8 +62,15 @@ export function getTolerance(resolution, pixelRatio) { * @param {import("../geom/Circle.js").default} geometry Geometry. * @param {import("../style/Style.js").default} style Style. * @param {import("../Feature.js").default} feature Feature. + * @param {import("../render/canvas/BuilderGroup.js").default=} opt_declutterBuilderGroup Builder for decluttering. */ -function renderCircleGeometry(builderGroup, geometry, style, feature) { +function renderCircleGeometry( + builderGroup, + geometry, + style, + feature, + opt_declutterBuilderGroup +) { const fillStyle = style.getFill(); const strokeStyle = style.getStroke(); if (fillStyle || strokeStyle) { @@ -76,7 +83,7 @@ function renderCircleGeometry(builderGroup, geometry, style, feature) { } const textStyle = style.getText(); if (textStyle) { - const textReplay = builderGroup.getBuilder( + const textReplay = (opt_declutterBuilderGroup || builderGroup).getBuilder( style.getZIndex(), BuilderType.TEXT ); @@ -92,8 +99,8 @@ function renderCircleGeometry(builderGroup, geometry, style, feature) { * @param {number} squaredTolerance Squared tolerance. * @param {function(import("../events/Event.js").default): void} listener Listener function. * @param {import("../proj.js").TransformFunction} [opt_transform] Transform from user to view projection. + * @param {import("../render/canvas/BuilderGroup.js").default=} opt_declutterBuilderGroup Builder for decluttering. * @return {boolean} `true` if style is loading. - * @template T */ export function renderFeature( replayGroup, @@ -101,7 +108,8 @@ export function renderFeature( style, squaredTolerance, listener, - opt_transform + opt_transform, + opt_declutterBuilderGroup ) { let loading = false; const imageStyle = style.getImage(); @@ -123,7 +131,8 @@ export function renderFeature( feature, style, squaredTolerance, - opt_transform + opt_transform, + opt_declutterBuilderGroup ); return loading; @@ -135,13 +144,15 @@ export function renderFeature( * @param {import("../style/Style.js").default} style Style. * @param {number} squaredTolerance Squared tolerance. * @param {import("../proj.js").TransformFunction} [opt_transform] Optional transform function. + * @param {import("../render/canvas/BuilderGroup.js").default=} opt_declutterBuilderGroup Builder for decluttering. */ function renderFeatureInternal( replayGroup, feature, style, squaredTolerance, - opt_transform + opt_transform, + opt_declutterBuilderGroup ) { const geometry = style.getGeometryFunction()(feature); if (!geometry) { @@ -156,7 +167,13 @@ function renderFeatureInternal( renderGeometry(replayGroup, simplifiedGeometry, style, feature); } else { const geometryRenderer = GEOMETRY_RENDERERS[simplifiedGeometry.getType()]; - geometryRenderer(replayGroup, simplifiedGeometry, style, feature); + geometryRenderer( + replayGroup, + simplifiedGeometry, + style, + feature, + opt_declutterBuilderGroup + ); } } @@ -187,18 +204,26 @@ function renderGeometry(replayGroup, geometry, style, feature) { * @param {import("../geom/GeometryCollection.js").default} geometry Geometry. * @param {import("../style/Style.js").default} style Style. * @param {import("../Feature.js").default} feature Feature. + * @param {import("../render/canvas/BuilderGroup.js").default=} opt_declutterBuilderGroup Builder for decluttering. */ function renderGeometryCollectionGeometry( replayGroup, geometry, style, - feature + feature, + opt_declutterBuilderGroup ) { const geometries = geometry.getGeometriesArray(); let i, ii; for (i = 0, ii = geometries.length; i < ii; ++i) { const geometryRenderer = GEOMETRY_RENDERERS[geometries[i].getType()]; - geometryRenderer(replayGroup, geometries[i], style, feature); + geometryRenderer( + replayGroup, + geometries[i], + style, + feature, + opt_declutterBuilderGroup + ); } } @@ -207,8 +232,15 @@ function renderGeometryCollectionGeometry( * @param {import("../geom/LineString.js").default|import("../render/Feature.js").default} geometry Geometry. * @param {import("../style/Style.js").default} style Style. * @param {import("../Feature.js").FeatureLike} feature Feature. + * @param {import("../render/canvas/BuilderGroup.js").default=} opt_declutterBuilderGroup Builder for decluttering. */ -function renderLineStringGeometry(builderGroup, geometry, style, feature) { +function renderLineStringGeometry( + builderGroup, + geometry, + style, + feature, + opt_declutterBuilderGroup +) { const strokeStyle = style.getStroke(); if (strokeStyle) { const lineStringReplay = builderGroup.getBuilder( @@ -220,7 +252,7 @@ function renderLineStringGeometry(builderGroup, geometry, style, feature) { } const textStyle = style.getText(); if (textStyle) { - const textReplay = builderGroup.getBuilder( + const textReplay = (opt_declutterBuilderGroup || builderGroup).getBuilder( style.getZIndex(), BuilderType.TEXT ); @@ -234,8 +266,15 @@ function renderLineStringGeometry(builderGroup, geometry, style, feature) { * @param {import("../geom/MultiLineString.js").default|import("../render/Feature.js").default} geometry Geometry. * @param {import("../style/Style.js").default} style Style. * @param {import("../Feature.js").FeatureLike} feature Feature. + * @param {import("../render/canvas/BuilderGroup.js").default=} opt_declutterBuilderGroup Builder for decluttering. */ -function renderMultiLineStringGeometry(builderGroup, geometry, style, feature) { +function renderMultiLineStringGeometry( + builderGroup, + geometry, + style, + feature, + opt_declutterBuilderGroup +) { const strokeStyle = style.getStroke(); if (strokeStyle) { const lineStringReplay = builderGroup.getBuilder( @@ -247,7 +286,7 @@ function renderMultiLineStringGeometry(builderGroup, geometry, style, feature) { } const textStyle = style.getText(); if (textStyle) { - const textReplay = builderGroup.getBuilder( + const textReplay = (opt_declutterBuilderGroup || builderGroup).getBuilder( style.getZIndex(), BuilderType.TEXT ); @@ -261,8 +300,15 @@ function renderMultiLineStringGeometry(builderGroup, geometry, style, feature) { * @param {import("../geom/MultiPolygon.js").default} geometry Geometry. * @param {import("../style/Style.js").default} style Style. * @param {import("../Feature.js").default} feature Feature. + * @param {import("../render/canvas/BuilderGroup.js").default=} opt_declutterBuilderGroup Builder for decluttering. */ -function renderMultiPolygonGeometry(builderGroup, geometry, style, feature) { +function renderMultiPolygonGeometry( + builderGroup, + geometry, + style, + feature, + opt_declutterBuilderGroup +) { const fillStyle = style.getFill(); const strokeStyle = style.getStroke(); if (strokeStyle || fillStyle) { @@ -275,7 +321,7 @@ function renderMultiPolygonGeometry(builderGroup, geometry, style, feature) { } const textStyle = style.getText(); if (textStyle) { - const textReplay = builderGroup.getBuilder( + const textReplay = (opt_declutterBuilderGroup || builderGroup).getBuilder( style.getZIndex(), BuilderType.TEXT ); @@ -289,9 +335,22 @@ function renderMultiPolygonGeometry(builderGroup, geometry, style, feature) { * @param {import("../geom/Point.js").default|import("../render/Feature.js").default} geometry Geometry. * @param {import("../style/Style.js").default} style Style. * @param {import("../Feature.js").FeatureLike} feature Feature. + * @param {import("../render/canvas/BuilderGroup.js").default=} opt_declutterBuilderGroup Builder for decluttering. */ -function renderPointGeometry(builderGroup, geometry, style, feature) { +function renderPointGeometry( + builderGroup, + geometry, + style, + feature, + opt_declutterBuilderGroup +) { const imageStyle = style.getImage(); + const textStyle = style.getText(); + let sharedData; + if (opt_declutterBuilderGroup) { + builderGroup = opt_declutterBuilderGroup; + sharedData = imageStyle && textStyle ? {} : undefined; + } if (imageStyle) { if (imageStyle.getImageState() != ImageState.LOADED) { return; @@ -300,16 +359,15 @@ function renderPointGeometry(builderGroup, geometry, style, feature) { style.getZIndex(), BuilderType.IMAGE ); - imageReplay.setImageStyle(imageStyle); + imageReplay.setImageStyle(imageStyle, sharedData); imageReplay.drawPoint(geometry, feature); } - const textStyle = style.getText(); if (textStyle) { const textReplay = builderGroup.getBuilder( style.getZIndex(), BuilderType.TEXT ); - textReplay.setTextStyle(textStyle); + textReplay.setTextStyle(textStyle, sharedData); textReplay.drawText(geometry, feature); } } @@ -319,9 +377,22 @@ function renderPointGeometry(builderGroup, geometry, style, feature) { * @param {import("../geom/MultiPoint.js").default|import("../render/Feature.js").default} geometry Geometry. * @param {import("../style/Style.js").default} style Style. * @param {import("../Feature.js").FeatureLike} feature Feature. + * @param {import("../render/canvas/BuilderGroup.js").default=} opt_declutterBuilderGroup Builder for decluttering. */ -function renderMultiPointGeometry(builderGroup, geometry, style, feature) { +function renderMultiPointGeometry( + builderGroup, + geometry, + style, + feature, + opt_declutterBuilderGroup +) { const imageStyle = style.getImage(); + const textStyle = style.getText(); + let sharedData; + if (opt_declutterBuilderGroup) { + builderGroup = opt_declutterBuilderGroup; + sharedData = imageStyle && textStyle ? {} : undefined; + } if (imageStyle) { if (imageStyle.getImageState() != ImageState.LOADED) { return; @@ -330,16 +401,15 @@ function renderMultiPointGeometry(builderGroup, geometry, style, feature) { style.getZIndex(), BuilderType.IMAGE ); - imageReplay.setImageStyle(imageStyle); + imageReplay.setImageStyle(imageStyle, sharedData); imageReplay.drawMultiPoint(geometry, feature); } - const textStyle = style.getText(); if (textStyle) { - const textReplay = builderGroup.getBuilder( + const textReplay = (opt_declutterBuilderGroup || builderGroup).getBuilder( style.getZIndex(), BuilderType.TEXT ); - textReplay.setTextStyle(textStyle); + textReplay.setTextStyle(textStyle, sharedData); textReplay.drawText(geometry, feature); } } @@ -349,8 +419,15 @@ function renderMultiPointGeometry(builderGroup, geometry, style, feature) { * @param {import("../geom/Polygon.js").default|import("../render/Feature.js").default} geometry Geometry. * @param {import("../style/Style.js").default} style Style. * @param {import("../Feature.js").FeatureLike} feature Feature. + * @param {import("../render/canvas/BuilderGroup.js").default=} opt_declutterBuilderGroup Builder for decluttering. */ -function renderPolygonGeometry(builderGroup, geometry, style, feature) { +function renderPolygonGeometry( + builderGroup, + geometry, + style, + feature, + opt_declutterBuilderGroup +) { const fillStyle = style.getFill(); const strokeStyle = style.getStroke(); if (fillStyle || strokeStyle) { @@ -363,7 +440,7 @@ function renderPolygonGeometry(builderGroup, geometry, style, feature) { } const textStyle = style.getText(); if (textStyle) { - const textReplay = builderGroup.getBuilder( + const textReplay = (opt_declutterBuilderGroup || builderGroup).getBuilder( style.getZIndex(), BuilderType.TEXT ); diff --git a/src/ol/source/Raster.js b/src/ol/source/Raster.js index abdbd161cb..67ab9b59c1 100644 --- a/src/ol/source/Raster.js +++ b/src/ol/source/Raster.js @@ -549,6 +549,7 @@ class RasterSource extends ImageSource { this.frameState_ = { animate: false, coordinateToPixelTransform: createTransform(), + declutterTree: null, extent: null, index: 0, layerIndex: 0,