diff --git a/rendering/cases/layer-mapbox-vector/expected.png b/rendering/cases/layer-mapbox-vector/expected.png index a4c2011370..301b578e8b 100644 Binary files a/rendering/cases/layer-mapbox-vector/expected.png and b/rendering/cases/layer-mapbox-vector/expected.png differ diff --git a/src/ol/render/canvas/BuilderGroup.js b/src/ol/render/canvas/BuilderGroup.js index 0bc6e1d533..d588727215 100644 --- a/src/ol/render/canvas/BuilderGroup.js +++ b/src/ol/render/canvas/BuilderGroup.js @@ -7,7 +7,6 @@ import ImageBuilder from './ImageBuilder.js'; import LineStringBuilder from './LineStringBuilder.js'; import PolygonBuilder from './PolygonBuilder.js'; import TextBuilder from './TextBuilder.js'; -import {createEmpty} from '../../extent.js'; /** * @type {Object} @@ -78,15 +77,15 @@ class BuilderGroup { * @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][4])++; + /** @type {number} */ (declutter[0][0])++; } else { - declutter = [createEmpty()]; + declutter = [[1]]; this.declutterGroups_ = declutter; - declutter[0].push(1); } } return declutter; diff --git a/src/ol/render/canvas/Executor.js b/src/ol/render/canvas/Executor.js index 26943c576e..d1c82d2625 100644 --- a/src/ol/render/canvas/Executor.js +++ b/src/ol/render/canvas/Executor.js @@ -14,8 +14,8 @@ import { import { createEmpty, createOrUpdate, - createOrUpdateEmpty, - extend, + getHeight, + getWidth, intersects, } from '../../extent.js'; import { @@ -69,8 +69,9 @@ class Executor { * @param {number} pixelRatio Pixel ratio. * @param {boolean} overlaps The replay can have overlapping geometries. * @param {SerializableInstructions} instructions The serializable instructions + * @param {import("../../size.js").Size} renderBuffer Render buffer (width/height) in pixels. */ - constructor(resolution, pixelRatio, overlaps, instructions) { + constructor(resolution, pixelRatio, overlaps, instructions, renderBuffer) { /** * @protected * @type {boolean} @@ -119,6 +120,12 @@ class Executor { */ this.coordinateCache_ = {}; + /** + * @private + * @type {import("../../size.js").Size} + */ + this.renderBuffer_ = renderBuffer; + /** * @private * @type {!import("../../transform.js").Transform} @@ -316,6 +323,7 @@ class Executor { * @param {Array} padding Padding. * @param {Array<*>} fillInstruction Fill instruction. * @param {Array<*>} strokeInstruction Stroke instruction. + * @return {boolean} The image or label was rendered. */ replayImageOrLabel_( context, @@ -397,15 +405,26 @@ class Executor { } else { createOrUpdate(boxX, boxY, boxX + boxW, boxY + boxH, tmpExtent); } + this.renderBuffer_[0] = Math.max( + this.renderBuffer_[0], + getWidth(tmpExtent) + ); + this.renderBuffer_[1] = Math.max( + this.renderBuffer_[1], + getHeight(tmpExtent) + ); const canvas = context.canvas; const strokePadding = strokeInstruction ? (strokeInstruction[2] * scale) / 2 : 0; + const renderBuffer = this.renderBuffer_; const intersects = - tmpExtent[0] - strokePadding <= canvas.width / contextScale && - tmpExtent[2] + strokePadding >= 0 && - tmpExtent[1] - strokePadding <= canvas.height / contextScale && - tmpExtent[3] + strokePadding >= 0; + tmpExtent[0] - strokePadding <= + (canvas.width + renderBuffer[0]) / contextScale && + tmpExtent[2] + strokePadding >= -renderBuffer[0] / contextScale && + tmpExtent[1] - strokePadding <= + (canvas.height + renderBuffer[1]) / contextScale && + tmpExtent[3] + strokePadding >= -renderBuffer[1] / contextScale; if (snapToPixel) { x = Math.round(x); @@ -413,10 +432,9 @@ class Executor { } if (declutterGroup) { - if (!intersects && declutterGroup[4] == 1) { - return; + if (!intersects && declutterGroup[0] == 1) { + return false; } - extend(declutterGroup, tmpExtent); const declutterArgs = intersects ? [ context, @@ -430,6 +448,7 @@ class Executor { x, y, scale, + tmpExtent.slice(), ] : null; if (declutterArgs) { @@ -472,6 +491,7 @@ class Executor { scale ); } + return true; } /** @@ -517,51 +537,57 @@ class Executor { * @return {?} Declutter tree. */ renderDeclutter(declutterGroup, feature, opacity, declutterTree) { - if (declutterGroup && declutterGroup.length > 5) { - const groupCount = declutterGroup[4]; - if (groupCount == 1 || groupCount == declutterGroup.length - 5) { - /** @type {import("../../structs/RBush.js").Entry} */ - const box = { - minX: /** @type {number} */ (declutterGroup[0]), - minY: /** @type {number} */ (declutterGroup[1]), - maxX: /** @type {number} */ (declutterGroup[2]), - maxY: /** @type {number} */ (declutterGroup[3]), - value: feature, - }; - if (!declutterTree) { - declutterTree = new RBush(9); - } - if (!declutterTree.collides(box)) { - declutterTree.insert(box); - for (let j = 5, 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 > 11) { - this.replayTextBackground_( - declutterData[0], - declutterData[13], - declutterData[14], - declutterData[15], - declutterData[16], - declutterData[11], - declutterData[12], - true - ); - } - drawImageOrLabel.apply(undefined, declutterData); - if (currentAlpha !== opacity) { - context.globalAlpha = currentAlpha; - } - } - } - declutterGroup.length = 5; - createOrUpdateEmpty(declutterGroup); + /** @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; } @@ -828,14 +854,12 @@ class Executor { } if (declutterGroups) { const index = Math.floor(declutterGroupIndex); - if (declutterGroups.length < index + 1) { - declutterGroup = createEmpty(); - declutterGroup.push(declutterGroups[0][4]); - declutterGroups.push(declutterGroup); - } - declutterGroup = declutterGroups[index]; + declutterGroup = + declutterGroups.length < index + 1 + ? [declutterGroups[0][0]] + : declutterGroups[index]; } - this.replayImageOrLabel_( + const rendered = this.replayImageOrLabel_( context, contextScale, pixelCoordinates[d], @@ -860,11 +884,18 @@ class Executor { ? /** @type {Array<*>} */ (lastStrokeInstruction) : null ); + if ( + rendered && + declutterGroup && + declutterGroups[declutterGroups.length - 1] !== declutterGroup + ) { + declutterGroups.push(declutterGroup); + } if (declutterGroup) { - if (declutterGroupIndex === Math.floor(declutterGroupIndex)) { + if (declutterGroup.length - 1 === declutterGroup[0]) { this.declutterItems.push(this, declutterGroup, feature); } - declutterGroupIndex += 1 / declutterGroup[4]; + declutterGroupIndex += 1 / declutterGroup[0]; } } ++i; @@ -917,6 +948,7 @@ class Executor { cachedWidths ); if (parts) { + let rendered = false; let c, cc, chars, label, part; if (strokeKey) { for (c = 0, cc = parts.length; c < cc; ++c) { @@ -928,27 +960,28 @@ class Executor { baseline * label.height + (0.5 - baseline) * 2 * strokeWidth - offsetY; - this.replayImageOrLabel_( - context, - contextScale, - /** @type {number} */ (part[0]), - /** @type {number} */ (part[1]), - label, - anchorX, - anchorY, - declutterGroup, - label.height, - 1, - 0, - 0, - /** @type {number} */ (part[3]), - pixelRatioScale, - false, - label.width, - defaultPadding, - null, - null - ); + rendered = + this.replayImageOrLabel_( + context, + contextScale, + /** @type {number} */ (part[0]), + /** @type {number} */ (part[1]), + label, + anchorX, + anchorY, + declutterGroup, + label.height, + 1, + 0, + 0, + /** @type {number} */ (part[3]), + pixelRatioScale, + false, + label.width, + defaultPadding, + null, + null + ) || rendered; } } if (fillKey) { @@ -958,32 +991,35 @@ class Executor { label = this.createLabel(chars, textKey, fillKey, ''); anchorX = /** @type {number} */ (part[2]); anchorY = baseline * label.height - offsetY; - this.replayImageOrLabel_( - context, - contextScale, - /** @type {number} */ (part[0]), - /** @type {number} */ (part[1]), - label, - anchorX, - anchorY, - declutterGroup, - label.height, - 1, - 0, - 0, - /** @type {number} */ (part[3]), - pixelRatioScale, - false, - label.width, - defaultPadding, - null, - null - ); + rendered = + this.replayImageOrLabel_( + context, + contextScale, + /** @type {number} */ (part[0]), + /** @type {number} */ (part[1]), + label, + anchorX, + anchorY, + declutterGroup, + label.height, + 1, + 0, + 0, + /** @type {number} */ (part[3]), + pixelRatioScale, + false, + label.width, + defaultPadding, + null, + null + ) || rendered; } } + if (rendered) { + this.declutterItems.push(this, declutterGroup, feature); + } } } - this.declutterItems.push(this, declutterGroup, feature); ++i; break; case CanvasInstruction.END_GEOMETRY: diff --git a/src/ol/render/canvas/ExecutorGroup.js b/src/ol/render/canvas/ExecutorGroup.js index ccf95e6bd2..a14d6bc8a7 100644 --- a/src/ol/render/canvas/ExecutorGroup.js +++ b/src/ol/render/canvas/ExecutorGroup.js @@ -126,13 +126,15 @@ class ExecutorGroup { this.executorsByZIndex_[zIndex] = executors; } const instructionByZindex = allInstructions[zIndex]; + const renderBuffer = [this.renderBuffer_ || 0, this.renderBuffer_ || 0]; for (const builderType in instructionByZindex) { const instructions = instructionByZindex[builderType]; executors[builderType] = new Executor( this.resolution_, this.pixelRatio_, this.overlaps_, - instructions + instructions, + renderBuffer ); } } @@ -476,15 +478,15 @@ export function replayDeclutter( let currentExecutor; for (let i = 0, ii = executorData.length; i < ii; ) { const executor = executorData[i++]; - if (executor !== currentExecutor) { + 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, }); } - const transform = executorData[i++]; - executor.execute(context, 1, transform, rotation, snapToPixel); } } }