From 7a5e0db59fb23baf04b5c5430d00e0b851c167dd Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Fri, 18 Sep 2020 23:31:20 +0200 Subject: [PATCH 1/6] Remove decluttering for a fresh start --- examples/offscreen-canvas.worker.js | 2 - src/ol/PluggableMap.js | 10 - src/ol/render.js | 26 --- src/ol/render/VectorContext.js | 6 +- src/ol/render/canvas.js | 17 -- src/ol/render/canvas/BuilderGroup.js | 34 +-- src/ol/render/canvas/Executor.js | 246 ++++----------------- src/ol/render/canvas/ExecutorGroup.js | 84 +------ src/ol/render/canvas/ImageBuilder.js | 14 +- src/ol/render/canvas/TextBuilder.js | 25 +-- src/ol/renderer/Layer.js | 9 +- src/ol/renderer/Map.js | 19 +- src/ol/renderer/canvas/VectorImageLayer.js | 18 +- src/ol/renderer/canvas/VectorLayer.js | 39 +--- src/ol/renderer/canvas/VectorTileLayer.js | 82 +++---- src/ol/renderer/vector.js | 18 +- src/ol/renderer/webgl/PointsLayer.js | 9 +- src/ol/source/Raster.js | 1 - 18 files changed, 108 insertions(+), 551 deletions(-) diff --git a/examples/offscreen-canvas.worker.js b/examples/offscreen-canvas.worker.js index ce33a656c5..f37f334743 100644 --- a/examples/offscreen-canvas.worker.js +++ b/examples/offscreen-canvas.worker.js @@ -6,7 +6,6 @@ import stringify from 'json-stringify-safe'; import styleFunction from 'ol-mapbox-style/dist/stylefunction.js'; import {Projection} from '../src/ol/proj.js'; import {inView} from '../src/ol/layer/Layer.js'; -import {renderDeclutterItems} from '../src/ol/render.js'; import {getTilePriority as tilePriorityFunction} from '../src/ol/TileQueue.js'; /** @type {any} */ @@ -145,7 +144,6 @@ worker.addEventListener('message', (event) => { renderer.renderFrame(frameState, canvas); } }); - renderDeclutterItems(frameState, null); if (tileQueue.getTilesLoading() < maxTotalLoading) { tileQueue.reprioritize(); tileQueue.loadMoreTiles(maxTotalLoading, maxNewLoads); diff --git a/src/ol/PluggableMap.js b/src/ol/PluggableMap.js index d25adf8d21..3bf3509ea7 100644 --- a/src/ol/PluggableMap.js +++ b/src/ol/PluggableMap.js @@ -51,7 +51,6 @@ import {removeNode} from './dom.js'; * @property {boolean} animate * @property {import("./transform.js").Transform} coordinateToPixelTransform * @property {null|import("./extent.js").Extent} extent - * @property {Array} declutterItems * @property {number} index * @property {Array} layerStatesArray * @property {number} layerIndex @@ -64,12 +63,6 @@ import {removeNode} from './dom.js'; * @property {!Object>} wantedTiles */ -/** - * @typedef {Object} DeclutterItems - * @property {Array<*>} items Declutter items of an executor. - * @property {number} opacity Layer opacity. - */ - /** * @typedef {function(PluggableMap, ?FrameState): any} PostRenderFunction */ @@ -1379,9 +1372,6 @@ class PluggableMap extends BaseObject { frameState = { animate: false, coordinateToPixelTransform: this.coordinateToPixelTransform_, - declutterItems: previousFrameState - ? previousFrameState.declutterItems - : [], extent: getForViewAndSize( viewState.center, viewState.resolution, diff --git a/src/ol/render.js b/src/ol/render.js index f964ec5469..22ecd658ae 100644 --- a/src/ol/render.js +++ b/src/ol/render.js @@ -129,29 +129,3 @@ export function getRenderPixel(event, pixel) { applyTransform(event.inversePixelTransform.slice(), result); return result; } - -/** - * @param {import("./PluggableMap.js").FrameState} frameState Frame state. - * @param {?} declutterTree Declutter tree. - * @returns {?} Declutter tree. - */ -export function renderDeclutterItems(frameState, declutterTree) { - if (declutterTree) { - declutterTree.clear(); - } - const items = frameState.declutterItems; - for (let z = items.length - 1; z >= 0; --z) { - const item = items[z]; - const zIndexItems = item.items; - for (let i = 0, ii = zIndexItems.length; i < ii; i += 3) { - declutterTree = zIndexItems[i].renderDeclutter( - zIndexItems[i + 1], - zIndexItems[i + 2], - item.opacity, - declutterTree - ); - } - } - items.length = 0; - return declutterTree; -} diff --git a/src/ol/render/VectorContext.js b/src/ol/render/VectorContext.js index e00d586363..4fc3a1f39e 100644 --- a/src/ol/render/VectorContext.js +++ b/src/ol/render/VectorContext.js @@ -100,15 +100,13 @@ class VectorContext { /** * @param {import("../style/Image.js").default} imageStyle Image style. - * @param {import("./canvas.js").DeclutterGroup=} opt_declutterGroup Declutter. */ - setImageStyle(imageStyle, opt_declutterGroup) {} + setImageStyle(imageStyle) {} /** * @param {import("../style/Text.js").default} textStyle Text style. - * @param {import("./canvas.js").DeclutterGroups=} opt_declutterGroups Declutter. */ - setTextStyle(textStyle, opt_declutterGroups) {} + setTextStyle(textStyle) {} } export default VectorContext; diff --git a/src/ol/render/canvas.js b/src/ol/render/canvas.js index 97cf9df9f5..7c4b00db70 100644 --- a/src/ol/render/canvas.js +++ b/src/ol/render/canvas.js @@ -67,23 +67,6 @@ import {toString} from '../transform.js'; * @property {Array} [padding] */ -/** - * Container for decluttered replay instructions that need to be rendered or - * omitted together, i.e. when styles render both an image and text, or for the - * characters that form text along lines. The basic elements of this array are - * `[minX, minY, maxX, maxY, count]`, where the first four entries are the - * rendered extent of the group in pixel space. `count` is the number of styles - * in the group, i.e. 2 when an image and a text are grouped, or 1 otherwise. - * In addition to these four elements, declutter instruction arrays (i.e. the - * arguments to {@link module:ol/render/canvas~drawImage} are appended to the array. - * @typedef {Array<*>} DeclutterGroup - */ - -/** - * Declutter groups for support of multi geometries. - * @typedef {Array} DeclutterGroups - */ - /** * @const * @type {string} diff --git a/src/ol/render/canvas/BuilderGroup.js b/src/ol/render/canvas/BuilderGroup.js index d588727215..49aa6c44c5 100644 --- a/src/ol/render/canvas/BuilderGroup.js +++ b/src/ol/render/canvas/BuilderGroup.js @@ -26,21 +26,8 @@ class BuilderGroup { * @param {import("../../extent.js").Extent} maxExtent Max extent. * @param {number} resolution Resolution. * @param {number} pixelRatio Pixel ratio. - * @param {boolean} declutter Decluttering enabled. */ - constructor(tolerance, maxExtent, resolution, pixelRatio, declutter) { - /** - * @type {boolean} - * @private - */ - this.declutter_ = declutter; - - /** - * @type {import("../canvas.js").DeclutterGroups} - * @private - */ - this.declutterGroups_ = null; - + constructor(tolerance, maxExtent, resolution, pixelRatio) { /** * @private * @type {number} @@ -72,25 +59,6 @@ class BuilderGroup { this.buildersByZIndex_ = {}; } - /** - * @param {boolean} group Group with previous builder. - * @return {import("../canvas").DeclutterGroups} The resulting instruction groups. - */ - addDeclutter(group) { - /** @type {Array<*>} */ - let declutter = null; - if (this.declutter_) { - if (group) { - declutter = this.declutterGroups_; - /** @type {number} */ (declutter[0][0])++; - } else { - declutter = [[1]]; - this.declutterGroups_ = declutter; - } - } - return declutter; - } - /** * @return {!Object>} The serializable instructions */ diff --git a/src/ol/render/canvas/Executor.js b/src/ol/render/canvas/Executor.js index 461bbd3896..b37ca24863 100644 --- a/src/ol/render/canvas/Executor.js +++ b/src/ol/render/canvas/Executor.js @@ -2,7 +2,6 @@ * @module ol/render/canvas/Executor */ import CanvasInstruction from './Instruction.js'; -import RBush from 'rbush'; import {TEXT_ALIGN} from './TextBuilder.js'; import {WORKER_OFFSCREEN_CANVAS} from '../../has.js'; import { @@ -11,13 +10,7 @@ import { create as createTransform, setFromArray as transformSetFromArray, } from '../../transform.js'; -import { - createEmpty, - createOrUpdate, - getHeight, - getWidth, - intersects, -} from '../../extent.js'; +import {createEmpty, createOrUpdate, intersects} from '../../extent.js'; import { defaultPadding, defaultTextBaseline, @@ -97,11 +90,6 @@ class Executor { */ this.alignFill_; - /** - * @type {Array<*>} - */ - this.declutterItems = []; - /** * @protected * @type {Array<*>} @@ -274,7 +262,6 @@ class Executor { * @param {import("../../coordinate.js").Coordinate} p4 4th point of the background box. * @param {Array<*>} fillInstruction Fill instruction. * @param {Array<*>} strokeInstruction Stroke instruction. - * @param {boolean} declutter Declutter. */ replayTextBackground_( context, @@ -283,8 +270,7 @@ class Executor { p3, p4, fillInstruction, - strokeInstruction, - declutter + strokeInstruction ) { context.beginPath(); context.moveTo.apply(context, p1); @@ -294,9 +280,6 @@ class Executor { context.lineTo.apply(context, p1); if (fillInstruction) { this.alignFill_ = /** @type {boolean} */ (fillInstruction[2]); - if (declutter) { - context.fillStyle = /** @type {import("../../colorlike.js").ColorLike} */ (fillInstruction[1]); - } this.fill_(context); } if (strokeInstruction) { @@ -317,7 +300,6 @@ class Executor { * @param {import("../canvas.js").Label|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} imageOrLabel Image. * @param {number} anchorX Anchor X. * @param {number} anchorY Anchor Y. - * @param {import("../canvas.js").DeclutterGroup} declutterGroup Declutter group. * @param {number} height Height. * @param {number} opacity Opacity. * @param {number} originX Origin X. @@ -339,7 +321,6 @@ class Executor { imageOrLabel, anchorX, anchorY, - declutterGroup, height, opacity, originX, @@ -417,15 +398,8 @@ class Executor { tmpExtent ); } - let renderBufferX = 0; - let renderBufferY = 0; - if (declutterGroup) { - const renderBuffer = this.renderBuffer_; - renderBuffer[0] = Math.max(renderBuffer[0], getWidth(tmpExtent)); - renderBufferX = renderBuffer[0]; - renderBuffer[1] = Math.max(renderBuffer[1], getHeight(tmpExtent)); - renderBufferY = renderBuffer[1]; - } + 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 @@ -443,40 +417,7 @@ class Executor { y = Math.round(y); } - if (declutterGroup) { - if (!intersects && declutterGroup[0] == 1) { - return false; - } - const declutterArgs = intersects - ? [ - context, - transform ? transform.slice(0) : null, - opacity, - imageOrLabel, - originX, - originY, - w, - h, - x, - y, - scale, - tmpExtent.slice(), - ] - : null; - if (declutterArgs) { - if (fillStroke) { - declutterArgs.push( - fillInstruction, - strokeInstruction, - p1.slice(0), - p2.slice(0), - p3.slice(0), - p4.slice(0) - ); - } - declutterGroup.push(declutterArgs); - } - } else if (intersects) { + if (intersects) { if (fillStroke) { this.replayTextBackground_( context, @@ -485,8 +426,7 @@ class Executor { p3, p4, /** @type {Array<*>} */ (fillInstruction), - /** @type {Array<*>} */ (strokeInstruction), - false + /** @type {Array<*>} */ (strokeInstruction) ); } drawImageOrLabel( @@ -541,68 +481,6 @@ class Executor { } } - /** - * @param {import("../canvas.js").DeclutterGroup} declutterGroup Declutter group. - * @param {import("../../Feature.js").FeatureLike} feature Feature. - * @param {number} opacity Layer opacity. - * @param {?} declutterTree Declutter tree. - * @return {?} Declutter tree. - */ - renderDeclutter(declutterGroup, feature, opacity, declutterTree) { - /** @type {Array} */ - const boxes = []; - for (let i = 1, ii = declutterGroup.length; i < ii; ++i) { - const declutterData = declutterGroup[i]; - const box = declutterData[11]; - boxes.push({ - minX: box[0], - minY: box[1], - maxX: box[2], - maxY: box[3], - value: feature, - }); - } - if (!declutterTree) { - declutterTree = new RBush(9); - } - let collides = false; - for (let i = 0, ii = boxes.length; i < ii; ++i) { - if (declutterTree.collides(boxes[i])) { - collides = true; - break; - } - } - if (!collides) { - declutterTree.load(boxes); - for (let j = 1, jj = declutterGroup.length; j < jj; ++j) { - const declutterData = /** @type {Array} */ (declutterGroup[j]); - const context = declutterData[0]; - const currentAlpha = context.globalAlpha; - if (currentAlpha !== opacity) { - context.globalAlpha = opacity; - } - if (declutterData.length > 12) { - this.replayTextBackground_( - declutterData[0], - declutterData[14], - declutterData[15], - declutterData[16], - declutterData[17], - declutterData[12], - declutterData[13], - true - ); - } - drawImageOrLabel.apply(undefined, declutterData); - if (currentAlpha !== opacity) { - context.globalAlpha = currentAlpha; - } - } - } - declutterGroup.length = 1; - return declutterTree; - } - /** * @private * @param {string} text The text to draw. @@ -659,7 +537,6 @@ class Executor { featureCallback, opt_hitExtent ) { - this.declutterItems.length = 0; /** @type {Array} */ let pixelCoordinates; if (this.pixelCoordinates_ && equals(transform, this.renderedTransform_)) { @@ -682,17 +559,7 @@ 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, - declutterGroup, - declutterGroups, - image, - text, - textKey; + let anchorX, anchorY, prevX, prevY, roundX, roundY, image, text, textKey; let strokeKey, fillKey; let pendingFill = 0; let pendingStroke = 0; @@ -796,22 +663,21 @@ class Executor { // Remaining arguments in DRAW_IMAGE are in alphabetical order anchorX = /** @type {number} */ (instruction[4]); anchorY = /** @type {number} */ (instruction[5]); - declutterGroups = featureCallback ? null : instruction[6]; - let height = /** @type {number} */ (instruction[7]); - const opacity = /** @type {number} */ (instruction[8]); - const originX = /** @type {number} */ (instruction[9]); - const originY = /** @type {number} */ (instruction[10]); - const rotateWithView = /** @type {boolean} */ (instruction[11]); - let rotation = /** @type {number} */ (instruction[12]); - const scale = /** @type {import("../../size.js").Size} */ (instruction[13]); - let width = /** @type {number} */ (instruction[14]); + let height = /** @type {number} */ (instruction[6]); + const opacity = /** @type {number} */ (instruction[7]); + const originX = /** @type {number} */ (instruction[8]); + const originY = /** @type {number} */ (instruction[9]); + const rotateWithView = /** @type {boolean} */ (instruction[10]); + let rotation = /** @type {number} */ (instruction[11]); + const scale = /** @type {import("../../size.js").Size} */ (instruction[12]); + let width = /** @type {number} */ (instruction[13]); - if (!image && instruction.length >= 19) { + if (!image && instruction.length >= 18) { // create label images - text = /** @type {string} */ (instruction[18]); - textKey = /** @type {string} */ (instruction[19]); - strokeKey = /** @type {string} */ (instruction[20]); - fillKey = /** @type {string} */ (instruction[21]); + text = /** @type {string} */ (instruction[17]); + textKey = /** @type {string} */ (instruction[18]); + strokeKey = /** @type {string} */ (instruction[19]); + fillKey = /** @type {string} */ (instruction[20]); const labelWithAnchor = this.drawLabelWithPointPlacement_( text, textKey, @@ -820,28 +686,28 @@ class Executor { ); image = labelWithAnchor.label; instruction[3] = image; - const textOffsetX = /** @type {number} */ (instruction[22]); + const textOffsetX = /** @type {number} */ (instruction[21]); anchorX = (labelWithAnchor.anchorX - textOffsetX) * this.pixelRatio; instruction[4] = anchorX; - const textOffsetY = /** @type {number} */ (instruction[23]); + const textOffsetY = /** @type {number} */ (instruction[22]); anchorY = (labelWithAnchor.anchorY - textOffsetY) * this.pixelRatio; instruction[5] = anchorY; height = image.height; - instruction[7] = height; + instruction[6] = height; width = image.width; - instruction[14] = width; + instruction[13] = width; } let geometryWidths; - if (instruction.length > 24) { - geometryWidths = /** @type {number} */ (instruction[24]); + if (instruction.length > 23) { + geometryWidths = /** @type {number} */ (instruction[23]); } let padding, backgroundFill, backgroundStroke; - if (instruction.length > 16) { - padding = /** @type {Array} */ (instruction[15]); - backgroundFill = /** @type {boolean} */ (instruction[16]); - backgroundStroke = /** @type {boolean} */ (instruction[17]); + if (instruction.length > 15) { + padding = /** @type {Array} */ (instruction[14]); + backgroundFill = /** @type {boolean} */ (instruction[15]); + backgroundStroke = /** @type {boolean} */ (instruction[16]); } else { padding = defaultPadding; backgroundFill = false; @@ -856,7 +722,6 @@ class Executor { rotation -= viewRotation; } let widthIndex = 0; - let declutterGroupIndex = 0; for (; d < dd; d += 2) { if ( geometryWidths && @@ -864,14 +729,7 @@ class Executor { ) { continue; } - if (declutterGroups) { - const index = Math.floor(declutterGroupIndex); - declutterGroup = - declutterGroups.length < index + 1 - ? [declutterGroups[0][0]] - : declutterGroups[index]; - } - const rendered = this.replayImageOrLabel_( + this.replayImageOrLabel_( context, contextScale, pixelCoordinates[d], @@ -879,7 +737,6 @@ class Executor { image, anchorX, anchorY, - declutterGroup, height, opacity, originX, @@ -896,19 +753,6 @@ class Executor { ? /** @type {Array<*>} */ (lastStrokeInstruction) : null ); - if ( - rendered && - declutterGroup && - declutterGroups[declutterGroups.length - 1] !== declutterGroup - ) { - declutterGroups.push(declutterGroup); - } - if (declutterGroup) { - if (declutterGroup.length - 1 === declutterGroup[0]) { - this.declutterItems.push(this, declutterGroup, feature); - } - declutterGroupIndex += 1 / declutterGroup[0]; - } } ++i; break; @@ -916,19 +760,18 @@ class Executor { const begin = /** @type {number} */ (instruction[1]); const end = /** @type {number} */ (instruction[2]); const baseline = /** @type {number} */ (instruction[3]); - declutterGroup = featureCallback ? null : instruction[4]; - const overflow = /** @type {number} */ (instruction[5]); - fillKey = /** @type {string} */ (instruction[6]); - const maxAngle = /** @type {number} */ (instruction[7]); - const measurePixelRatio = /** @type {number} */ (instruction[8]); - const offsetY = /** @type {number} */ (instruction[9]); - strokeKey = /** @type {string} */ (instruction[10]); - const strokeWidth = /** @type {number} */ (instruction[11]); - text = /** @type {string} */ (instruction[12]); - textKey = /** @type {string} */ (instruction[13]); + const overflow = /** @type {number} */ (instruction[4]); + fillKey = /** @type {string} */ (instruction[5]); + const maxAngle = /** @type {number} */ (instruction[6]); + const measurePixelRatio = /** @type {number} */ (instruction[7]); + const offsetY = /** @type {number} */ (instruction[8]); + strokeKey = /** @type {string} */ (instruction[9]); + const strokeWidth = /** @type {number} */ (instruction[10]); + text = /** @type {string} */ (instruction[11]); + textKey = /** @type {string} */ (instruction[12]); const pixelRatioScale = [ - /** @type {number} */ (instruction[14]), - /** @type {number} */ (instruction[14]), + /** @type {number} */ (instruction[13]), + /** @type {number} */ (instruction[13]), ]; const textState = this.textStates[textKey]; @@ -990,7 +833,6 @@ class Executor { label, anchorX, anchorY, - declutterGroup, label.height, 1, 0, @@ -1021,7 +863,6 @@ class Executor { label, anchorX, anchorY, - declutterGroup, label.height, 1, 0, @@ -1036,9 +877,6 @@ class Executor { ) || rendered; } } - if (rendered) { - this.declutterItems.push(this, declutterGroup, feature); - } } } ++i; diff --git a/src/ol/render/canvas/ExecutorGroup.js b/src/ol/render/canvas/ExecutorGroup.js index a14d6bc8a7..427d17b4a5 100644 --- a/src/ol/render/canvas/ExecutorGroup.js +++ b/src/ol/render/canvas/ExecutorGroup.js @@ -162,7 +162,6 @@ 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 */ @@ -171,8 +170,7 @@ class ExecutorGroup { resolution, rotation, hitTolerance, - callback, - declutteredFeatures + callback ) { hitTolerance = Math.round(hitTolerance); const contextSize = hitTolerance * 2 + 1; @@ -234,17 +232,7 @@ class ExecutorGroup { for (let j = 0; j < contextSize; j++) { if (mask[i][j]) { if (imageData[(j * contextSize + i) * 4 + 3] > 0) { - let result; - if ( - !( - declutteredFeatures && - (builderType == BuilderType.IMAGE || - builderType == BuilderType.TEXT) - ) || - declutteredFeatures.indexOf(feature) !== -1 - ) { - result = callback(feature); - } + const result = callback(feature); if (result) { return result; } else { @@ -318,7 +306,6 @@ 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 {Object=} opt_declutterReplays Declutter replays. */ execute( context, @@ -326,8 +313,7 @@ class ExecutorGroup { transform, viewRotation, snapToPixel, - opt_builderTypes, - opt_declutterReplays + opt_builderTypes ) { /** @type {Array} */ const zs = Object.keys(this.executorsByZIndex_).map(Number); @@ -349,26 +335,13 @@ class ExecutorGroup { const builderType = builderTypes[j]; replay = replays[builderType]; if (replay !== undefined) { - if ( - opt_declutterReplays && - (builderType == BuilderType.IMAGE || - builderType == BuilderType.TEXT) - ) { - const declutter = opt_declutterReplays[zIndexKey]; - if (!declutter) { - opt_declutterReplays[zIndexKey] = [replay, transform.slice(0)]; - } else { - declutter.push(replay, transform.slice(0)); - } - } else { - replay.execute( - context, - contextScale, - transform, - viewRotation, - snapToPixel - ); - } + replay.execute( + context, + contextScale, + transform, + viewRotation, + snapToPixel + ); } } } @@ -454,41 +427,4 @@ export function getCircleArray(radius) { return arr; } -/** - * @param {!Object>} declutterReplays Declutter replays. - * @param {CanvasRenderingContext2D} context Context. - * @param {number} rotation Rotation. - * @param {number} opacity Opacity. - * @param {boolean} snapToPixel Snap point symbols and text to integer pixels. - * @param {Array} declutterItems Declutter items. - */ -export function replayDeclutter( - declutterReplays, - context, - rotation, - opacity, - snapToPixel, - declutterItems -) { - const zs = Object.keys(declutterReplays) - .map(Number) - .sort(numberSafeCompareFunction); - for (let z = 0, zz = zs.length; z < zz; ++z) { - const executorData = declutterReplays[zs[z].toString()]; - let currentExecutor; - for (let i = 0, ii = executorData.length; i < ii; ) { - const executor = executorData[i++]; - const transform = executorData[i++]; - executor.execute(context, 1, transform, rotation, snapToPixel); - if (executor !== currentExecutor && executor.declutterItems.length > 0) { - currentExecutor = executor; - declutterItems.push({ - items: executor.declutterItems, - opacity: opacity, - }); - } - } - } -} - export default ExecutorGroup; diff --git a/src/ol/render/canvas/ImageBuilder.js b/src/ol/render/canvas/ImageBuilder.js index e50f2c88a7..28ad1e10ee 100644 --- a/src/ol/render/canvas/ImageBuilder.js +++ b/src/ol/render/canvas/ImageBuilder.js @@ -14,12 +14,6 @@ class CanvasImageBuilder extends CanvasBuilder { constructor(tolerance, maxExtent, resolution, pixelRatio) { super(tolerance, maxExtent, resolution, pixelRatio); - /** - * @private - * @type {import("../canvas.js").DeclutterGroups} - */ - this.declutterGroups_ = null; - /** * @private * @type {HTMLCanvasElement|HTMLVideoElement|HTMLImageElement} @@ -120,7 +114,6 @@ class CanvasImageBuilder extends CanvasBuilder { // Remaining arguments to DRAW_IMAGE are in alphabetical order this.anchorX_ * this.imagePixelRatio_, this.anchorY_ * this.imagePixelRatio_, - this.declutterGroups_, Math.ceil(this.height_ * this.imagePixelRatio_), this.opacity_, this.originX_, @@ -141,7 +134,6 @@ class CanvasImageBuilder extends CanvasBuilder { // Remaining arguments to DRAW_IMAGE are in alphabetical order this.anchorX_, this.anchorY_, - this.declutterGroups_, this.height_, this.opacity_, this.originX_, @@ -175,7 +167,6 @@ class CanvasImageBuilder extends CanvasBuilder { // Remaining arguments to DRAW_IMAGE are in alphabetical order this.anchorX_ * this.imagePixelRatio_, this.anchorY_ * this.imagePixelRatio_, - this.declutterGroups_, Math.ceil(this.height_ * this.imagePixelRatio_), this.opacity_, this.originX_, @@ -196,7 +187,6 @@ class CanvasImageBuilder extends CanvasBuilder { // Remaining arguments to DRAW_IMAGE are in alphabetical order this.anchorX_, this.anchorY_, - this.declutterGroups_, this.height_, this.opacity_, this.originX_, @@ -233,9 +223,8 @@ class CanvasImageBuilder extends CanvasBuilder { /** * @param {import("../../style/Image.js").default} imageStyle Image style. - * @param {import("../canvas.js").DeclutterGroup} declutterGroups Declutter. */ - setImageStyle(imageStyle, declutterGroups) { + setImageStyle(imageStyle) { const anchor = imageStyle.getAnchor(); const size = imageStyle.getSize(); const hitDetectionImage = imageStyle.getHitDetectionImage(); @@ -244,7 +233,6 @@ class CanvasImageBuilder extends CanvasBuilder { this.imagePixelRatio_ = imageStyle.getPixelRatio(this.pixelRatio); this.anchorX_ = anchor[0]; this.anchorY_ = anchor[1]; - this.declutterGroups_ = declutterGroups; this.hitDetectionImage_ = hitDetectionImage; this.image_ = image; this.height_ = size[1]; diff --git a/src/ol/render/canvas/TextBuilder.js b/src/ol/render/canvas/TextBuilder.js index a5235a2a61..903d6dcee3 100644 --- a/src/ol/render/canvas/TextBuilder.js +++ b/src/ol/render/canvas/TextBuilder.js @@ -52,12 +52,6 @@ class CanvasTextBuilder extends CanvasBuilder { constructor(tolerance, maxExtent, resolution, pixelRatio) { super(tolerance, maxExtent, resolution, pixelRatio); - /** - * @private - * @type {import("../canvas.js").DeclutterGroups} - */ - this.declutterGroups_; - /** * @private * @type {Array} @@ -226,12 +220,7 @@ class CanvasTextBuilder extends CanvasBuilder { } const end = coordinates.length; flatOffset = ends[o]; - const declutterGroup = this.declutterGroups_ - ? o === 0 - ? this.declutterGroups_[0] - : [].concat(this.declutterGroups_[0]) - : null; - this.drawChars_(begin, end, declutterGroup); + this.drawChars_(begin, end); begin = end; } this.endGeometry(feature); @@ -331,7 +320,6 @@ class CanvasTextBuilder extends CanvasBuilder { null, NaN, NaN, - this.declutterGroups_, NaN, 1, 0, @@ -363,7 +351,6 @@ class CanvasTextBuilder extends CanvasBuilder { null, NaN, NaN, - this.declutterGroups_, NaN, 1, 0, @@ -433,9 +420,8 @@ class CanvasTextBuilder extends CanvasBuilder { * @private * @param {number} begin Begin. * @param {number} end End. - * @param {import("../canvas.js").DeclutterGroup} declutterGroup Declutter group. */ - drawChars_(begin, end, declutterGroup) { + drawChars_(begin, end) { const strokeState = this.textStrokeState_; const textState = this.textState_; @@ -458,7 +444,6 @@ class CanvasTextBuilder extends CanvasBuilder { begin, end, baseline, - declutterGroup, textState.overflow, fillKey, textState.maxAngle, @@ -475,7 +460,6 @@ class CanvasTextBuilder extends CanvasBuilder { begin, end, baseline, - declutterGroup, textState.overflow, fillKey, textState.maxAngle, @@ -491,15 +475,12 @@ class CanvasTextBuilder extends CanvasBuilder { /** * @param {import("../../style/Text.js").default} textStyle Text style. - * @param {import("../canvas.js").DeclutterGroups} declutterGroups Declutter. */ - setTextStyle(textStyle, declutterGroups) { + setTextStyle(textStyle) { let textState, fillState, strokeState; if (!textStyle) { this.text_ = ''; } else { - this.declutterGroups_ = declutterGroups; - const textFillStyle = textStyle.getFill(); if (!textFillStyle) { fillState = null; diff --git a/src/ol/renderer/Layer.js b/src/ol/renderer/Layer.js index b814c60b64..c6efd2711a 100644 --- a/src/ol/renderer/Layer.js +++ b/src/ol/renderer/Layer.js @@ -101,17 +101,10 @@ class LayerRenderer extends Observable { * @param {import("../PluggableMap.js").FrameState} frameState Frame state. * @param {number} hitTolerance Hit tolerance in pixels. * @param {function(import("../Feature.js").FeatureLike, import("../layer/Layer.js").default): T} callback Feature callback. - * @param {Array} declutteredFeatures Decluttered features. * @return {T|void} Callback result. * @template T */ - forEachFeatureAtCoordinate( - coordinate, - frameState, - hitTolerance, - callback, - declutteredFeatures - ) {} + forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback) {} /** * @abstract diff --git a/src/ol/renderer/Map.js b/src/ol/renderer/Map.js index d6f185459d..8ad03b14ca 100644 --- a/src/ol/renderer/Map.js +++ b/src/ol/renderer/Map.js @@ -8,7 +8,6 @@ import {compose as composeTransform, makeInverse} from '../transform.js'; import {getWidth} from '../extent.js'; import {shared as iconImageCache} from '../style/IconImageCache.js'; import {inView} from '../layer/Layer.js'; -import {renderDeclutterItems} from '../render.js'; import {wrapX} from '../coordinate.js'; /** @@ -26,11 +25,6 @@ class MapRenderer extends Disposable { * @type {import("../PluggableMap.js").default} */ this.map_ = map; - - /** - * @private - */ - this.declutterTree_ = null; } /** @@ -116,12 +110,6 @@ class MapRenderer extends Disposable { const layerStates = frameState.layerStatesArray; const numLayers = layerStates.length; - let declutteredFeatures; - if (this.declutterTree_) { - declutteredFeatures = this.declutterTree_.all().map(function (entry) { - return entry.value; - }); - } const tmpCoord = []; for (let i = 0; i < offsets.length; i++) { @@ -149,8 +137,7 @@ class MapRenderer extends Disposable { tmpCoord, frameState, hitTolerance, - callback, - declutteredFeatures + callback ); } if (result) { @@ -226,9 +213,7 @@ class MapRenderer extends Disposable { * Render. * @param {?import("../PluggableMap.js").FrameState} frameState Frame state. */ - renderFrame(frameState) { - this.declutterTree_ = renderDeclutterItems(frameState, this.declutterTree_); - } + renderFrame(frameState) {} /** * @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 76ec2f8e0f..5bc4f0540f 100644 --- a/src/ol/renderer/canvas/VectorImageLayer.js +++ b/src/ol/renderer/canvas/VectorImageLayer.js @@ -10,7 +10,6 @@ import ViewHint from '../../ViewHint.js'; import {apply, compose, create} from '../../transform.js'; import {assign} from '../../obj.js'; import {getHeight, getWidth, isEmpty, scaleFromCenter} from '../../extent.js'; -import {renderDeclutterItems} from '../../render.js'; /** * @classdesc @@ -115,7 +114,6 @@ class CanvasVectorImageLayerRenderer extends CanvasImageLayerRenderer { {}, frameState, { - declutterItems: [], extent: renderedExtent, size: [width, height], viewState: /** @type {import("../../View.js").State} */ (assign( @@ -139,7 +137,6 @@ class CanvasVectorImageLayerRenderer extends CanvasImageLayerRenderer { ) { vectorRenderer.clipping = false; vectorRenderer.renderFrame(imageFrameState, null); - renderDeclutterItems(imageFrameState, null); callback(); } } @@ -191,32 +188,23 @@ class CanvasVectorImageLayerRenderer extends CanvasImageLayerRenderer { * @param {import("../../PluggableMap.js").FrameState} frameState Frame state. * @param {number} hitTolerance Hit tolerance in pixels. * @param {function(import("../../Feature.js").FeatureLike, import("../../layer/Layer.js").default): T} callback Feature callback. - * @param {Array} declutteredFeatures Decluttered features. * @return {T|void} Callback result. * @template T */ - forEachFeatureAtCoordinate( - coordinate, - frameState, - hitTolerance, - callback, - declutteredFeatures - ) { + forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback) { if (this.vectorRenderer_) { return this.vectorRenderer_.forEachFeatureAtCoordinate( coordinate, frameState, hitTolerance, - callback, - declutteredFeatures + callback ); } else { return super.forEachFeatureAtCoordinate( coordinate, frameState, hitTolerance, - callback, - declutteredFeatures + callback ); } } diff --git a/src/ol/renderer/canvas/VectorLayer.js b/src/ol/renderer/canvas/VectorLayer.js index 3bde6ce1ce..4527c4c5d0 100644 --- a/src/ol/renderer/canvas/VectorLayer.js +++ b/src/ol/renderer/canvas/VectorLayer.js @@ -3,9 +3,7 @@ */ import CanvasBuilderGroup from '../../render/canvas/BuilderGroup.js'; import CanvasLayerRenderer from './Layer.js'; -import ExecutorGroup, { - replayDeclutter, -} from '../../render/canvas/ExecutorGroup.js'; +import ExecutorGroup from '../../render/canvas/ExecutorGroup.js'; import ViewHint from '../../ViewHint.js'; import { apply, @@ -219,8 +217,6 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { viewHints[ViewHint.ANIMATING] || viewHints[ViewHint.INTERACTING] ); - const declutterReplays = this.getLayer().getDeclutter() ? {} : null; - const multiWorld = vectorSource.getWrapX() && projection.canWrapX(); const worldWidth = multiWorld ? getWidth(projectionExtent) : null; const endWorld = multiWorld @@ -245,26 +241,10 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { transform, rotation, snapToPixel, - undefined, - declutterReplays + undefined ); } while (++world < endWorld); - if (declutterReplays) { - const viewHints = frameState.viewHints; - const hifi = !( - viewHints[ViewHint.ANIMATING] || viewHints[ViewHint.INTERACTING] - ); - replayDeclutter( - declutterReplays, - context, - rotation, - 1, - hifi, - frameState.declutterItems - ); - } - if (clipped) { context.restore(); } @@ -388,17 +368,10 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { * @param {import("../../PluggableMap.js").FrameState} frameState Frame state. * @param {number} hitTolerance Hit tolerance in pixels. * @param {function(import("../../Feature.js").FeatureLike, import("../../layer/Layer.js").default): T} callback Feature callback. - * @param {Array} declutteredFeatures Decluttered features. * @return {T|void} Callback result. * @template T */ - forEachFeatureAtCoordinate( - coordinate, - frameState, - hitTolerance, - callback, - declutteredFeatures - ) { + forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback) { if (!this.replayGroup_) { return undefined; } else { @@ -423,8 +396,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { features[key] = true; return callback(feature, layer); } - }, - layer.getDeclutter() ? declutteredFeatures : null + } ); return result; @@ -556,8 +528,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { getRenderTolerance(resolution, pixelRatio), extent, resolution, - pixelRatio, - vectorLayer.getDeclutter() + pixelRatio ); const userProjection = getUserProjection(); diff --git a/src/ol/renderer/canvas/VectorTileLayer.js b/src/ol/renderer/canvas/VectorTileLayer.js index efb3763063..f17e3cfb64 100644 --- a/src/ol/renderer/canvas/VectorTileLayer.js +++ b/src/ol/renderer/canvas/VectorTileLayer.js @@ -2,9 +2,7 @@ * @module ol/renderer/canvas/VectorTileLayer */ import CanvasBuilderGroup from '../../render/canvas/BuilderGroup.js'; -import CanvasExecutorGroup, { - replayDeclutter, -} from '../../render/canvas/ExecutorGroup.js'; +import CanvasExecutorGroup from '../../render/canvas/ExecutorGroup.js'; import CanvasTileLayerRenderer from './TileLayer.js'; import EventType from '../../events/EventType.js'; import ReplayType from '../../render/canvas/BuilderType.js'; @@ -292,8 +290,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { 0, sharedExtent, resolution, - pixelRatio, - layer.getDeclutter() + pixelRatio ); const squaredTolerance = getSquaredRenderTolerance( resolution, @@ -365,17 +362,10 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { * @param {import("../../PluggableMap.js").FrameState} frameState Frame state. * @param {number} hitTolerance Hit tolerance in pixels. * @param {function(import("../../Feature.js").FeatureLike, import("../../layer/Layer.js").default): T} callback Feature callback. - * @param {Array} declutteredFeatures Decluttered features. * @return {T|void} Callback result. * @template T */ - forEachFeatureAtCoordinate( - coordinate, - frameState, - hitTolerance, - callback, - declutteredFeatures - ) { + forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback) { const resolution = frameState.viewState.resolution; const rotation = frameState.viewState.rotation; hitTolerance = hitTolerance == undefined ? 0 : hitTolerance; @@ -420,11 +410,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { * @return {?} Callback result. */ function (feature) { - if ( - tileContainsCoordinate || - (declutteredFeatures && - declutteredFeatures.indexOf(feature) !== -1) - ) { + if (tileContainsCoordinate) { let key = feature.getId(); if (key === undefined) { key = getUid(feature); @@ -434,8 +420,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { return callback(feature, layer); } } - }, - layer.getDeclutter() ? declutteredFeatures : null + } ); } } @@ -587,7 +572,6 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { } const context = this.context; - const declutterReplays = layer.getDeclutter() ? {} : null; const replayTypes = VECTOR_REPLAYS[renderMode]; const pixelRatio = frameState.pixelRatio; const viewState = frameState.viewState; @@ -640,27 +624,29 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { } const currentZ = tile.tileCoord[0]; let currentClip; - if (!declutterReplays && !clipped) { + if (!clipped) { currentClip = executorGroup.getClipCoords(transform); - context.save(); + if (currentClip) { + context.save(); - // Create a clip mask for regions in this low resolution tile that are - // already filled by a higher resolution tile - for (let j = 0, jj = clips.length; j < jj; ++j) { - const clip = clips[j]; - if (currentZ < clipZs[j]) { - context.beginPath(); - // counter-clockwise (outer ring) for current tile - context.moveTo(currentClip[0], currentClip[1]); - context.lineTo(currentClip[2], currentClip[3]); - context.lineTo(currentClip[4], currentClip[5]); - context.lineTo(currentClip[6], currentClip[7]); - // clockwise (inner ring) for higher resolution tile - context.moveTo(clip[6], clip[7]); - context.lineTo(clip[4], clip[5]); - context.lineTo(clip[2], clip[3]); - context.lineTo(clip[0], clip[1]); - context.clip(); + // Create a clip mask for regions in this low resolution tile that are + // already filled by a higher resolution tile + for (let j = 0, jj = clips.length; j < jj; ++j) { + const clip = clips[j]; + if (currentZ < clipZs[j]) { + context.beginPath(); + // counter-clockwise (outer ring) for current tile + context.moveTo(currentClip[0], currentClip[1]); + context.lineTo(currentClip[2], currentClip[3]); + context.lineTo(currentClip[4], currentClip[5]); + context.lineTo(currentClip[6], currentClip[7]); + // clockwise (inner ring) for higher resolution tile + context.moveTo(clip[6], clip[7]); + context.lineTo(clip[4], clip[5]); + context.lineTo(clip[2], clip[3]); + context.lineTo(clip[0], clip[1]); + context.clip(); + } } } } @@ -670,10 +656,9 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { transform, rotation, hifi, - replayTypes, - declutterReplays + replayTypes ); - if (!declutterReplays && !clipped) { + if (!clipped && currentClip) { context.restore(); clips.push(currentClip); clipZs.push(currentZ); @@ -681,17 +666,6 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { } } } - if (declutterReplays) { - const layerState = frameState.layerStatesArray[frameState.layerIndex]; - replayDeclutter( - declutterReplays, - context, - rotation, - layerState.opacity, - hifi, - frameState.declutterItems - ); - } return this.container; } diff --git a/src/ol/renderer/vector.js b/src/ol/renderer/vector.js index 579f15960f..b293c53f9f 100644 --- a/src/ol/renderer/vector.js +++ b/src/ol/renderer/vector.js @@ -80,7 +80,7 @@ function renderCircleGeometry(builderGroup, geometry, style, feature) { style.getZIndex(), BuilderType.TEXT ); - textReplay.setTextStyle(textStyle, builderGroup.addDeclutter(false)); + textReplay.setTextStyle(textStyle); textReplay.drawText(geometry, feature); } } @@ -224,7 +224,7 @@ function renderLineStringGeometry(builderGroup, geometry, style, feature) { style.getZIndex(), BuilderType.TEXT ); - textReplay.setTextStyle(textStyle, builderGroup.addDeclutter(false)); + textReplay.setTextStyle(textStyle); textReplay.drawText(geometry, feature); } } @@ -251,7 +251,7 @@ function renderMultiLineStringGeometry(builderGroup, geometry, style, feature) { style.getZIndex(), BuilderType.TEXT ); - textReplay.setTextStyle(textStyle, builderGroup.addDeclutter(false)); + textReplay.setTextStyle(textStyle); textReplay.drawText(geometry, feature); } } @@ -279,7 +279,7 @@ function renderMultiPolygonGeometry(builderGroup, geometry, style, feature) { style.getZIndex(), BuilderType.TEXT ); - textReplay.setTextStyle(textStyle, builderGroup.addDeclutter(false)); + textReplay.setTextStyle(textStyle); textReplay.drawText(geometry, feature); } } @@ -300,7 +300,7 @@ function renderPointGeometry(builderGroup, geometry, style, feature) { style.getZIndex(), BuilderType.IMAGE ); - imageReplay.setImageStyle(imageStyle, builderGroup.addDeclutter(false)); + imageReplay.setImageStyle(imageStyle); imageReplay.drawPoint(geometry, feature); } const textStyle = style.getText(); @@ -309,7 +309,7 @@ function renderPointGeometry(builderGroup, geometry, style, feature) { style.getZIndex(), BuilderType.TEXT ); - textReplay.setTextStyle(textStyle, builderGroup.addDeclutter(!!imageStyle)); + textReplay.setTextStyle(textStyle); textReplay.drawText(geometry, feature); } } @@ -330,7 +330,7 @@ function renderMultiPointGeometry(builderGroup, geometry, style, feature) { style.getZIndex(), BuilderType.IMAGE ); - imageReplay.setImageStyle(imageStyle, builderGroup.addDeclutter(false)); + imageReplay.setImageStyle(imageStyle); imageReplay.drawMultiPoint(geometry, feature); } const textStyle = style.getText(); @@ -339,7 +339,7 @@ function renderMultiPointGeometry(builderGroup, geometry, style, feature) { style.getZIndex(), BuilderType.TEXT ); - textReplay.setTextStyle(textStyle, builderGroup.addDeclutter(!!imageStyle)); + textReplay.setTextStyle(textStyle); textReplay.drawText(geometry, feature); } } @@ -367,7 +367,7 @@ function renderPolygonGeometry(builderGroup, geometry, style, feature) { style.getZIndex(), BuilderType.TEXT ); - textReplay.setTextStyle(textStyle, builderGroup.addDeclutter(false)); + textReplay.setTextStyle(textStyle); textReplay.drawText(geometry, feature); } } diff --git a/src/ol/renderer/webgl/PointsLayer.js b/src/ol/renderer/webgl/PointsLayer.js index a506d8dd5b..5305da78d9 100644 --- a/src/ol/renderer/webgl/PointsLayer.js +++ b/src/ol/renderer/webgl/PointsLayer.js @@ -598,17 +598,10 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer { * @param {import("../../PluggableMap.js").FrameState} frameState Frame state. * @param {number} hitTolerance Hit tolerance in pixels. * @param {function(import("../../Feature.js").FeatureLike, import("../../layer/Layer.js").default): T} callback Feature callback. - * @param {Array} declutteredFeatures Decluttered features. * @return {T|void} Callback result. * @template T */ - forEachFeatureAtCoordinate( - coordinate, - frameState, - hitTolerance, - callback, - declutteredFeatures - ) { + forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback) { assert(this.hitDetectionEnabled_, 66); if (!this.hitRenderInstructions_) { return; diff --git a/src/ol/source/Raster.js b/src/ol/source/Raster.js index fb61fc6ed1..abdbd161cb 100644 --- a/src/ol/source/Raster.js +++ b/src/ol/source/Raster.js @@ -565,7 +565,6 @@ class RasterSource extends ImageSource { }), viewHints: [], wantedTiles: {}, - declutterItems: [], }; this.setAttributions(function (frameState) { From 8e862766fcb49553b644f7f82a01e54b4bcaa68e Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Sun, 20 Sep 2020 23:36:27 +0200 Subject: [PATCH 2/6] 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, From f63d0741b98b9f78e8a481a8d7aa8eea36ccb704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kr=C3=B6g?= Date: Sun, 27 Sep 2020 12:07:17 +0200 Subject: [PATCH 3/6] Add rendering test for multipoint with icon and text --- .../expected.png | Bin 0 -> 11498 bytes .../main.js | 53 ++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 rendering/cases/layer-vector-multipoint-decluttering/expected.png create mode 100644 rendering/cases/layer-vector-multipoint-decluttering/main.js diff --git a/rendering/cases/layer-vector-multipoint-decluttering/expected.png b/rendering/cases/layer-vector-multipoint-decluttering/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..3aa4409939772d9286fa49bfa2f09d44ed15b2c4 GIT binary patch literal 11498 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 Date: Sun, 27 Sep 2020 21:12:38 +0200 Subject: [PATCH 4/6] Use locally available font for rendering test --- .../expected.png | Bin 11498 -> 12196 bytes .../main.js | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/rendering/cases/layer-vector-multipoint-decluttering/expected.png b/rendering/cases/layer-vector-multipoint-decluttering/expected.png index 3aa4409939772d9286fa49bfa2f09d44ed15b2c4..f6509633740a3dee68534a313e5543cf894f23d1 100644 GIT binary patch literal 12196 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 lPq+Vb4*vh~`5~u?k>UI-Gcjca=w{4_sp5| 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 Date: Sun, 4 Oct 2020 19:01:45 +0200 Subject: [PATCH 5/6] Do not draw empty text styles This fixes a rendering/flicker issue when an empty text style is decluttered together with an image style. --- src/ol/renderer/vector.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/ol/renderer/vector.js b/src/ol/renderer/vector.js index 0e01263ac0..7452b56b9e 100644 --- a/src/ol/renderer/vector.js +++ b/src/ol/renderer/vector.js @@ -82,7 +82,7 @@ function renderCircleGeometry( circleReplay.drawCircle(geometry, feature); } const textStyle = style.getText(); - if (textStyle) { + if (textStyle && textStyle.getText()) { const textReplay = (opt_declutterBuilderGroup || builderGroup).getBuilder( style.getZIndex(), BuilderType.TEXT @@ -251,7 +251,7 @@ function renderLineStringGeometry( lineStringReplay.drawLineString(geometry, feature); } const textStyle = style.getText(); - if (textStyle) { + if (textStyle && textStyle.getText()) { const textReplay = (opt_declutterBuilderGroup || builderGroup).getBuilder( style.getZIndex(), BuilderType.TEXT @@ -285,7 +285,7 @@ function renderMultiLineStringGeometry( lineStringReplay.drawMultiLineString(geometry, feature); } const textStyle = style.getText(); - if (textStyle) { + if (textStyle && textStyle.getText()) { const textReplay = (opt_declutterBuilderGroup || builderGroup).getBuilder( style.getZIndex(), BuilderType.TEXT @@ -320,7 +320,7 @@ function renderMultiPolygonGeometry( polygonReplay.drawMultiPolygon(geometry, feature); } const textStyle = style.getText(); - if (textStyle) { + if (textStyle && textStyle.getText()) { const textReplay = (opt_declutterBuilderGroup || builderGroup).getBuilder( style.getZIndex(), BuilderType.TEXT @@ -349,7 +349,8 @@ function renderPointGeometry( let sharedData; if (opt_declutterBuilderGroup) { builderGroup = opt_declutterBuilderGroup; - sharedData = imageStyle && textStyle ? {} : undefined; + sharedData = + imageStyle && textStyle && textStyle.getText() ? {} : undefined; } if (imageStyle) { if (imageStyle.getImageState() != ImageState.LOADED) { @@ -362,7 +363,7 @@ function renderPointGeometry( imageReplay.setImageStyle(imageStyle, sharedData); imageReplay.drawPoint(geometry, feature); } - if (textStyle) { + if (textStyle && textStyle.getText()) { const textReplay = builderGroup.getBuilder( style.getZIndex(), BuilderType.TEXT @@ -391,7 +392,8 @@ function renderMultiPointGeometry( let sharedData; if (opt_declutterBuilderGroup) { builderGroup = opt_declutterBuilderGroup; - sharedData = imageStyle && textStyle ? {} : undefined; + sharedData = + imageStyle && textStyle && textStyle.getText() ? {} : undefined; } if (imageStyle) { if (imageStyle.getImageState() != ImageState.LOADED) { @@ -404,7 +406,7 @@ function renderMultiPointGeometry( imageReplay.setImageStyle(imageStyle, sharedData); imageReplay.drawMultiPoint(geometry, feature); } - if (textStyle) { + if (textStyle && textStyle.getText()) { const textReplay = (opt_declutterBuilderGroup || builderGroup).getBuilder( style.getZIndex(), BuilderType.TEXT @@ -439,7 +441,7 @@ function renderPolygonGeometry( polygonReplay.drawPolygon(geometry, feature); } const textStyle = style.getText(); - if (textStyle) { + if (textStyle && textStyle.getText()) { const textReplay = (opt_declutterBuilderGroup || builderGroup).getBuilder( style.getZIndex(), BuilderType.TEXT From c9ebf79df5380ab7ee9559e93b6517a53ac00702 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Mon, 5 Oct 2020 15:21:40 +0200 Subject: [PATCH 6/6] Add types and comments to make combined image+text decluttering clearer --- src/ol/render/VectorContext.js | 8 ++++---- src/ol/render/canvas.js | 4 ++++ src/ol/render/canvas/Executor.js | 27 ++++++++++++++++++--------- src/ol/render/canvas/ImageBuilder.js | 14 +++++++------- src/ol/render/canvas/TextBuilder.js | 10 +++++----- src/ol/renderer/vector.js | 18 ++++++++++-------- 6 files changed, 48 insertions(+), 33 deletions(-) diff --git a/src/ol/render/VectorContext.js b/src/ol/render/VectorContext.js index bad1facc35..c7fcca15dd 100644 --- a/src/ol/render/VectorContext.js +++ b/src/ol/render/VectorContext.js @@ -100,15 +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. + * @param {import("../render/canvas.js").DeclutterImageWithText=} opt_declutterImageWithText Shared data for combined decluttering with a text style. */ - setImageStyle(imageStyle, opt_sharedData) {} + setImageStyle(imageStyle, opt_declutterImageWithText) {} /** * @param {import("../style/Text.js").default} textStyle Text style. - * @param {Object=} opt_sharedData Shared data for combined decluttering with an image style. + * @param {import("../render/canvas.js").DeclutterImageWithText=} opt_declutterImageWithText Shared data for combined decluttering with an image style. */ - setTextStyle(textStyle, opt_sharedData) {} + setTextStyle(textStyle, opt_declutterImageWithText) {} } export default VectorContext; diff --git a/src/ol/render/canvas.js b/src/ol/render/canvas.js index 27435dcd4c..783b8aa05f 100644 --- a/src/ol/render/canvas.js +++ b/src/ol/render/canvas.js @@ -77,6 +77,10 @@ import {toString} from '../transform.js'; * @property {!Object} [strokeStates] The stroke states (decluttering). */ +/** + * @typedef {Object} DeclutterImageWithText + */ + /** * @const * @type {string} diff --git a/src/ol/render/canvas/Executor.js b/src/ol/render/canvas/Executor.js index 8bef5052dd..d7a049c00b 100644 --- a/src/ol/render/canvas/Executor.js +++ b/src/ol/render/canvas/Executor.js @@ -49,6 +49,10 @@ import {transform2D} from '../../geom/flat/transform.js'; * @property {import("../../transform.js").Transform} canvasTransform */ +/** + * @typedef {{0: CanvasRenderingContext2D, 1: number, 2: import("../canvas.js").Label|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement, 3: ImageOrLabelDimensions, 4: number, 5: Array<*>, 6: Array<*>}} ReplayImageOrLabelArgs + */ + /** * @type {import("../../extent.js").Extent} */ @@ -64,12 +68,11 @@ const p3 = []; const p4 = []; /** - * @param {Array<*>} replayImageOrLabelArgs Arguments to replayImageOrLabel + * @param {ReplayImageOrLabelArgs} replayImageOrLabelArgs Arguments to replayImageOrLabel * @return {BBox} Declutter bbox. */ function getDeclutterBox(replayImageOrLabelArgs) { - return /** @type {ImageOrLabelDimensions} */ (replayImageOrLabelArgs[3]) - .declutterBox; + return replayImageOrLabelArgs[3].declutterBox; } class Executor { @@ -724,7 +727,7 @@ 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]; + const declutterImageWithText = /** @type {import("../canvas.js").DeclutterImageWithText} */ (instruction[14]); if (!image && instruction.length >= 19) { // create label images @@ -801,6 +804,7 @@ class Executor { backgroundFill || backgroundStroke, feature ); + /** @type {ReplayImageOrLabelArgs} */ const args = [ context, contextScale, @@ -816,13 +820,15 @@ class Executor { ]; let imageArgs; let imageDeclutterBox; - if (opt_declutterTree && sharedData) { - if (!sharedData[d]) { - sharedData[d] = args; + if (opt_declutterTree && declutterImageWithText) { + if (!declutterImageWithText[d]) { + // We now have the image for an image+text combination. + declutterImageWithText[d] = args; + // Don't render anything for now, wait for the text. continue; } - imageArgs = sharedData[d]; - delete sharedData[d]; + imageArgs = declutterImageWithText[d]; + delete declutterImageWithText[d]; imageDeclutterBox = getDeclutterBox(imageArgs); if (opt_declutterTree.collides(imageDeclutterBox)) { continue; @@ -835,9 +841,11 @@ class Executor { continue; } if (imageArgs) { + // We now have image and text for an image+text combination. if (opt_declutterTree) { opt_declutterTree.insert(imageDeclutterBox); } + // Render the image before we render the text. this.replayImageOrLabel_.apply(this, imageArgs); } if (opt_declutterTree) { @@ -902,6 +910,7 @@ class Executor { viewRotationFromTransform ? 0 : this.viewRotation_ ); drawChars: if (parts) { + /** @type {Array} */ const replayImageOrLabelArgs = []; let c, cc, chars, label, part; if (strokeKey) { diff --git a/src/ol/render/canvas/ImageBuilder.js b/src/ol/render/canvas/ImageBuilder.js index 855d80e886..2a2819b104 100644 --- a/src/ol/render/canvas/ImageBuilder.js +++ b/src/ol/render/canvas/ImageBuilder.js @@ -95,9 +95,9 @@ class CanvasImageBuilder extends CanvasBuilder { /** * Data shared with a text builder for combined decluttering. * @private - * @type {Object} + * @type {import("../canvas.js").DeclutterImageWithText} */ - this.sharedData_ = undefined; + this.declutterImageWithText_ = undefined; } /** @@ -132,7 +132,7 @@ class CanvasImageBuilder extends CanvasBuilder { (this.scale_[1] * this.pixelRatio) / this.imagePixelRatio_, ], Math.ceil(this.width_ * this.imagePixelRatio_), - this.sharedData_, + this.declutterImageWithText_, ]); this.hitDetectionInstructions.push([ CanvasInstruction.DRAW_IMAGE, @@ -150,7 +150,7 @@ class CanvasImageBuilder extends CanvasBuilder { this.rotation_, this.scale_, this.width_, - this.sharedData_, + this.declutterImageWithText_, ]); this.endGeometry(feature); } @@ -187,7 +187,7 @@ class CanvasImageBuilder extends CanvasBuilder { (this.scale_[1] * this.pixelRatio) / this.imagePixelRatio_, ], Math.ceil(this.width_ * this.imagePixelRatio_), - this.sharedData_, + this.declutterImageWithText_, ]); this.hitDetectionInstructions.push([ CanvasInstruction.DRAW_IMAGE, @@ -205,7 +205,7 @@ class CanvasImageBuilder extends CanvasBuilder { this.rotation_, this.scale_, this.width_, - this.sharedData_, + this.declutterImageWithText_, ]); this.endGeometry(feature); } @@ -255,7 +255,7 @@ class CanvasImageBuilder extends CanvasBuilder { this.rotation_ = imageStyle.getRotation(); this.scale_ = imageStyle.getScaleArray(); this.width_ = size[0]; - this.sharedData_ = opt_sharedData; + this.declutterImageWithText_ = opt_sharedData; } } diff --git a/src/ol/render/canvas/TextBuilder.js b/src/ol/render/canvas/TextBuilder.js index 55a602c74a..e124a496b6 100644 --- a/src/ol/render/canvas/TextBuilder.js +++ b/src/ol/render/canvas/TextBuilder.js @@ -142,9 +142,9 @@ class CanvasTextBuilder extends CanvasBuilder { /** * Data shared with an image builder for combined decluttering. * @private - * @type {Object} + * @type {import("../canvas.js").DeclutterImageWithText} */ - this.sharedData_ = undefined; + this.declutterImageWithText_ = undefined; } /** @@ -335,7 +335,7 @@ class CanvasTextBuilder extends CanvasBuilder { this.textRotation_, [1, 1], NaN, - this.sharedData_, + this.declutterImageWithText_, padding == defaultPadding ? defaultPadding : padding.map(function (p) { @@ -367,7 +367,7 @@ class CanvasTextBuilder extends CanvasBuilder { this.textRotation_, [scale, scale], NaN, - this.sharedData_, + this.declutterImageWithText_, padding, !!textState.backgroundFill, !!textState.backgroundStroke, @@ -586,7 +586,7 @@ class CanvasTextBuilder extends CanvasBuilder { : '|' + getUid(fillState.fillStyle) : ''; } - this.sharedData_ = opt_sharedData; + this.declutterImageWithText_ = opt_sharedData; } } diff --git a/src/ol/renderer/vector.js b/src/ol/renderer/vector.js index 7452b56b9e..2c626a9eb2 100644 --- a/src/ol/renderer/vector.js +++ b/src/ol/renderer/vector.js @@ -346,10 +346,11 @@ function renderPointGeometry( ) { const imageStyle = style.getImage(); const textStyle = style.getText(); - let sharedData; + /** @type {import("../render/canvas.js").DeclutterImageWithText} */ + let declutterImageWithText; if (opt_declutterBuilderGroup) { builderGroup = opt_declutterBuilderGroup; - sharedData = + declutterImageWithText = imageStyle && textStyle && textStyle.getText() ? {} : undefined; } if (imageStyle) { @@ -360,7 +361,7 @@ function renderPointGeometry( style.getZIndex(), BuilderType.IMAGE ); - imageReplay.setImageStyle(imageStyle, sharedData); + imageReplay.setImageStyle(imageStyle, declutterImageWithText); imageReplay.drawPoint(geometry, feature); } if (textStyle && textStyle.getText()) { @@ -368,7 +369,7 @@ function renderPointGeometry( style.getZIndex(), BuilderType.TEXT ); - textReplay.setTextStyle(textStyle, sharedData); + textReplay.setTextStyle(textStyle, declutterImageWithText); textReplay.drawText(geometry, feature); } } @@ -389,10 +390,11 @@ function renderMultiPointGeometry( ) { const imageStyle = style.getImage(); const textStyle = style.getText(); - let sharedData; + /** @type {import("../render/canvas.js").DeclutterImageWithText} */ + let declutterImageWithText; if (opt_declutterBuilderGroup) { builderGroup = opt_declutterBuilderGroup; - sharedData = + declutterImageWithText = imageStyle && textStyle && textStyle.getText() ? {} : undefined; } if (imageStyle) { @@ -403,7 +405,7 @@ function renderMultiPointGeometry( style.getZIndex(), BuilderType.IMAGE ); - imageReplay.setImageStyle(imageStyle, sharedData); + imageReplay.setImageStyle(imageStyle, declutterImageWithText); imageReplay.drawMultiPoint(geometry, feature); } if (textStyle && textStyle.getText()) { @@ -411,7 +413,7 @@ function renderMultiPointGeometry( style.getZIndex(), BuilderType.TEXT ); - textReplay.setTextStyle(textStyle, sharedData); + textReplay.setTextStyle(textStyle, declutterImageWithText); textReplay.drawText(geometry, feature); } }