diff --git a/examples/offscreen-canvas.worker.js b/examples/offscreen-canvas.worker.js index ce33a656c5..5adec394e5 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,7 @@ worker.addEventListener('message', (event) => { renderer.renderFrame(frameState, canvas); } }); - renderDeclutterItems(frameState, null); + layers.forEach((layer) => layer.renderDeclutter(frameState)); if (tileQueue.getTilesLoading() < maxTotalLoading) { tileQueue.reprioritize(); tileQueue.loadMoreTiles(maxTotalLoading, maxNewLoads); 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 0000000000..f650963374 Binary files /dev/null and b/rendering/cases/layer-vector-multipoint-decluttering/expected.png differ diff --git a/rendering/cases/layer-vector-multipoint-decluttering/main.js b/rendering/cases/layer-vector-multipoint-decluttering/main.js new file mode 100644 index 0000000000..7b17e84ecc --- /dev/null +++ b/rendering/cases/layer-vector-multipoint-decluttering/main.js @@ -0,0 +1,53 @@ +import Feature from '../../../src/ol/Feature.js'; +import Map from '../../../src/ol/Map.js'; +import MultiPoint from '../../../src/ol/geom/MultiPoint.js'; +import VectorSource from '../../../src/ol/source/Vector.js'; +import View from '../../../src/ol/View.js'; +import {Icon, Stroke, Style, Text} from '../../../src/ol/style.js'; +import {Vector as VectorLayer} from '../../../src/ol/layer.js'; + +const vectorLayer = new VectorLayer({ + declutter: true, + renderBuffer: 0, + source: new VectorSource({ + features: [ + new Feature( + new MultiPoint([ + [0, 0], + [0, 1], + [0.5, 0.5], + [0.9, 0.85], + [1, 0], + [0.3, 0.5], + ]) + ), + ], + }), + style: new Style({ + image: new Icon({ + anchor: [0.5, 46], + anchorXUnits: 'fraction', + anchorYUnits: 'pixels', + src: '/data/icon.png', + }), + text: new Text({ + text: 'Test', + font: 'italic 700 20px Ubuntu', + stroke: new Stroke({ + color: 'red', + width: 20, + }), + }), + }), +}); + +new Map({ + layers: [vectorLayer], + target: document.getElementById('map'), + view: new View({ + center: [0.5, 0.5], + resolution: 0.006679631467570084, + }), +}); + +render({tolerance: 0.007}); diff --git a/src/ol/PluggableMap.js b/src/ol/PluggableMap.js index 3e57bdb561..4d8128a0ff 100644 --- a/src/ol/PluggableMap.js +++ b/src/ol/PluggableMap.js @@ -50,8 +50,8 @@ 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 {Array} declutterItems * @property {number} index * @property {Array} layerStatesArray * @property {number} layerIndex @@ -64,12 +64,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 */ @@ -1387,9 +1381,7 @@ class PluggableMap extends BaseObject { frameState = { animate: false, coordinateToPixelTransform: this.coordinateToPixelTransform_, - declutterItems: previousFrameState - ? previousFrameState.declutterItems - : [], + declutterTree: null, extent: getForViewAndSize( viewState.center, viewState.resolution, diff --git a/src/ol/VectorRenderTile.js b/src/ol/VectorRenderTile.js index efac251307..0902b92af5 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.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..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 {import("./canvas.js").DeclutterGroup=} opt_declutterGroup Declutter. + * @param {import("../render/canvas.js").DeclutterImageWithText=} opt_declutterImageWithText Shared data for combined decluttering with a text style. */ - setImageStyle(imageStyle, opt_declutterGroup) {} + setImageStyle(imageStyle, opt_declutterImageWithText) {} /** * @param {import("../style/Text.js").default} textStyle Text style. - * @param {import("./canvas.js").DeclutterGroups=} opt_declutterGroups Declutter. + * @param {import("../render/canvas.js").DeclutterImageWithText=} opt_declutterImageWithText Shared data for combined decluttering with an image style. */ - setTextStyle(textStyle, opt_declutterGroups) {} + setTextStyle(textStyle, opt_declutterImageWithText) {} } export default VectorContext; diff --git a/src/ol/render/canvas.js b/src/ol/render/canvas.js index 97cf9df9f5..783b8aa05f 100644 --- a/src/ol/render/canvas.js +++ b/src/ol/render/canvas.js @@ -68,20 +68,17 @@ import {toString} from '../transform.js'; */ /** - * 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 + * @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). */ /** - * Declutter groups for support of multi geometries. - * @typedef {Array} DeclutterGroups + * @typedef {Object} DeclutterImageWithText */ /** @@ -293,9 +290,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); @@ -303,7 +299,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 { @@ -318,9 +314,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/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..d7a049c00b 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, @@ -35,13 +28,29 @@ 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 + */ + +/** + * @typedef {{0: CanvasRenderingContext2D, 1: number, 2: import("../canvas.js").Label|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement, 3: ImageOrLabelDimensions, 4: number, 5: Array<*>, 6: Array<*>}} ReplayImageOrLabelArgs */ /** @@ -49,11 +58,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} */ @@ -63,12 +67,20 @@ const p3 = []; /** @type {import("../../coordinate.js").Coordinate} */ const p4 = []; +/** + * @param {ReplayImageOrLabelArgs} replayImageOrLabelArgs Arguments to replayImageOrLabel + * @return {BBox} Declutter bbox. + */ +function getDeclutterBox(replayImageOrLabelArgs) { + return 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) { @@ -97,11 +109,6 @@ class Executor { */ this.alignFill_; - /** - * @type {Array<*>} - */ - this.declutterItems = []; - /** * @protected * @type {Array<*>} @@ -274,7 +281,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 +289,7 @@ class Executor { p3, p4, fillInstruction, - strokeInstruction, - declutter + strokeInstruction ) { context.beginPath(); context.moveTo.apply(context, p1); @@ -294,9 +299,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) { @@ -310,62 +312,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 {import("../canvas.js").DeclutterGroup} declutterGroup Declutter group. - * @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, - declutterGroup, - 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]; @@ -382,12 +371,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, @@ -397,10 +384,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]), @@ -417,66 +404,63 @@ 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 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, + }; + } - 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) { + /** + * @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) { this.replayTextBackground_( context, @@ -485,22 +469,21 @@ class Executor { p3, p4, /** @type {Array<*>} */ (fillInstruction), - /** @type {Array<*>} */ (strokeInstruction), - false + /** @type {Array<*>} */ (strokeInstruction) ); } 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; @@ -530,7 +513,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]); @@ -541,68 +526,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. @@ -647,6 +570,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 */ @@ -657,9 +581,9 @@ class Executor { instructions, snapToPixel, featureCallback, - opt_hitExtent + opt_hitExtent, + opt_declutterTree ) { - this.declutterItems.length = 0; /** @type {Array} */ let pixelCoordinates; if (this.pixelCoordinates_ && equals(transform, this.renderedTransform_)) { @@ -688,12 +612,11 @@ class Executor { prevY, roundX, roundY, - declutterGroup, - declutterGroups, image, text, - textKey; - let strokeKey, fillKey; + textKey, + strokeKey, + fillKey; let pendingFill = 0; let pendingStroke = 0; let lastFillInstruction = null; @@ -796,15 +719,15 @@ 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]); + const declutterImageWithText = /** @type {import("../canvas.js").DeclutterImageWithText} */ (instruction[14]); if (!image && instruction.length >= 19) { // create label images @@ -827,9 +750,9 @@ class Executor { 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; @@ -856,7 +779,6 @@ class Executor { rotation -= viewRotation; } let widthIndex = 0; - let declutterGroupIndex = 0; for (; d < dd; d += 2) { if ( geometryWidths && @@ -864,51 +786,72 @@ class Executor { ) { continue; } - if (declutterGroups) { - const index = Math.floor(declutterGroupIndex); - declutterGroup = - declutterGroups.length < index + 1 - ? [declutterGroups[0][0]] - : declutterGroups[index]; - } - const rendered = this.replayImageOrLabel_( - context, - contextScale, + const dimensions = this.calculateImageOrLabelDimensions_( + image.width, + image.height, pixelCoordinates[d], pixelCoordinates[d + 1], - image, + width, + height, anchorX, anchorY, - declutterGroup, - height, - opacity, originX, originY, rotation, scale, snapToPixel, - width, padding, + backgroundFill || backgroundStroke, + feature + ); + /** @type {ReplayImageOrLabelArgs} */ + const args = [ + context, + contextScale, + image, + dimensions, + opacity, backgroundFill ? /** @type {Array<*>} */ (lastFillInstruction) : null, backgroundStroke ? /** @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); + : null, + ]; + let imageArgs; + let imageDeclutterBox; + 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 = declutterImageWithText[d]; + delete declutterImageWithText[d]; + imageDeclutterBox = getDeclutterBox(imageArgs); + if (opt_declutterTree.collides(imageDeclutterBox)) { + continue; } - declutterGroupIndex += 1 / declutterGroup[0]; } + if ( + opt_declutterTree && + opt_declutterTree.collides(dimensions.declutterBox) + ) { + 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) { + opt_declutterTree.insert(dimensions.declutterBox); + } + this.replayImageOrLabel_.apply(this, args); } ++i; break; @@ -916,19 +859,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]; @@ -967,8 +909,9 @@ class Executor { cachedWidths, viewRotationFromTransform ? 0 : this.viewRotation_ ); - if (parts) { - let rendered = false; + drawChars: if (parts) { + /** @type {Array} */ + const replayImageOrLabelArgs = []; let c, cc, chars, label, part; if (strokeKey) { for (c = 0, cc = parts.length; c < cc; ++c) { @@ -981,28 +924,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, - declutterGroup, - 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) { @@ -1012,32 +966,48 @@ 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, - declutterGroup, - 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 (rendered) { - this.declutterItems.push(this, declutterGroup, feature); + 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]); } } } @@ -1139,8 +1109,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, @@ -1149,7 +1127,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 a14d6bc8a7..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) { @@ -318,7 +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 {Object=} opt_declutterReplays Declutter replays. + * @param {import("rbush").default=} opt_declutterTree Declutter tree. */ execute( context, @@ -327,7 +327,7 @@ class ExecutorGroup { viewRotation, snapToPixel, opt_builderTypes, - opt_declutterReplays + opt_declutterTree ) { /** @type {Array} */ const zs = Object.keys(this.executorsByZIndex_).map(Number); @@ -342,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]; @@ -349,26 +352,14 @@ 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, + opt_declutterTree + ); } } } @@ -454,41 +445,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..2a2819b104 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} @@ -97,6 +91,13 @@ class CanvasImageBuilder extends CanvasBuilder { * @type {number|undefined} */ this.width_ = undefined; + + /** + * Data shared with a text builder for combined decluttering. + * @private + * @type {import("../canvas.js").DeclutterImageWithText} + */ + this.declutterImageWithText_ = undefined; } /** @@ -120,7 +121,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_, @@ -132,6 +132,7 @@ class CanvasImageBuilder extends CanvasBuilder { (this.scale_[1] * this.pixelRatio) / this.imagePixelRatio_, ], Math.ceil(this.width_ * this.imagePixelRatio_), + this.declutterImageWithText_, ]); this.hitDetectionInstructions.push([ CanvasInstruction.DRAW_IMAGE, @@ -141,7 +142,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_, @@ -150,6 +150,7 @@ class CanvasImageBuilder extends CanvasBuilder { this.rotation_, this.scale_, this.width_, + this.declutterImageWithText_, ]); this.endGeometry(feature); } @@ -175,7 +176,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_, @@ -187,6 +187,7 @@ class CanvasImageBuilder extends CanvasBuilder { (this.scale_[1] * this.pixelRatio) / this.imagePixelRatio_, ], Math.ceil(this.width_ * this.imagePixelRatio_), + this.declutterImageWithText_, ]); this.hitDetectionInstructions.push([ CanvasInstruction.DRAW_IMAGE, @@ -196,7 +197,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_, @@ -205,12 +205,13 @@ class CanvasImageBuilder extends CanvasBuilder { this.rotation_, this.scale_, this.width_, + this.declutterImageWithText_, ]); this.endGeometry(feature); } /** - * @return {import("./Builder.js").SerializableInstructions} the serializable instructions. + * @return {import("../canvas.js").SerializableInstructions} the serializable instructions. */ finish() { this.reverseHitDetectionInstructions(); @@ -233,9 +234,9 @@ class CanvasImageBuilder extends CanvasBuilder { /** * @param {import("../../style/Image.js").default} imageStyle Image style. - * @param {import("../canvas.js").DeclutterGroup} declutterGroups Declutter. + * @param {Object=} opt_sharedData Shared data. */ - setImageStyle(imageStyle, declutterGroups) { + setImageStyle(imageStyle, opt_sharedData) { const anchor = imageStyle.getAnchor(); const size = imageStyle.getSize(); const hitDetectionImage = imageStyle.getHitDetectionImage(); @@ -244,7 +245,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]; @@ -255,6 +255,7 @@ class CanvasImageBuilder extends CanvasBuilder { this.rotation_ = imageStyle.getRotation(); this.scale_ = imageStyle.getScaleArray(); this.width_ = size[0]; + this.declutterImageWithText_ = 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 a5235a2a61..e124a496b6 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} @@ -144,10 +138,17 @@ class CanvasTextBuilder extends CanvasBuilder { * @type {string} */ this.strokeKey_ = ''; + + /** + * Data shared with an image builder for combined decluttering. + * @private + * @type {import("../canvas.js").DeclutterImageWithText} + */ + this.declutterImageWithText_ = undefined; } /** - * @return {import("./Builder.js").SerializableInstructions} the serializable instructions. + * @return {import("../canvas.js").SerializableInstructions} the serializable instructions. */ finish() { const instructions = super.finish(); @@ -226,12 +227,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 +327,6 @@ class CanvasTextBuilder extends CanvasBuilder { null, NaN, NaN, - this.declutterGroups_, NaN, 1, 0, @@ -340,6 +335,7 @@ class CanvasTextBuilder extends CanvasBuilder { this.textRotation_, [1, 1], NaN, + this.declutterImageWithText_, padding == defaultPadding ? defaultPadding : padding.map(function (p) { @@ -363,7 +359,6 @@ class CanvasTextBuilder extends CanvasBuilder { null, NaN, NaN, - this.declutterGroups_, NaN, 1, 0, @@ -372,6 +367,7 @@ class CanvasTextBuilder extends CanvasBuilder { this.textRotation_, [scale, scale], NaN, + this.declutterImageWithText_, padding, !!textState.backgroundFill, !!textState.backgroundStroke, @@ -433,9 +429,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 +453,6 @@ class CanvasTextBuilder extends CanvasBuilder { begin, end, baseline, - declutterGroup, textState.overflow, fillKey, textState.maxAngle, @@ -475,7 +469,6 @@ class CanvasTextBuilder extends CanvasBuilder { begin, end, baseline, - declutterGroup, textState.overflow, fillKey, textState.maxAngle, @@ -491,15 +484,13 @@ class CanvasTextBuilder extends CanvasBuilder { /** * @param {import("../../style/Text.js").default} textStyle Text style. - * @param {import("../canvas.js").DeclutterGroups} declutterGroups Declutter. + * @param {Object=} opt_sharedData Shared data. */ - setTextStyle(textStyle, declutterGroups) { + setTextStyle(textStyle, opt_sharedData) { let textState, fillState, strokeState; if (!textStyle) { this.text_ = ''; } else { - this.declutterGroups_ = declutterGroups; - const textFillStyle = textStyle.getFill(); if (!textFillStyle) { fillState = null; @@ -595,6 +586,7 @@ class CanvasTextBuilder extends CanvasBuilder { : '|' + getUid(fillState.fillStyle) : ''; } + this.declutterImageWithText_ = 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 b814c60b64..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; } /** @@ -101,17 +106,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..28b676b6f4 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) { @@ -224,10 +211,11 @@ class MapRenderer extends Disposable { /** * Render. + * @abstract * @param {?import("../PluggableMap.js").FrameState} frameState Frame state. */ renderFrame(frameState) { - this.declutterTree_ = renderDeclutterItems(frameState, this.declutterTree_); + abstract(); } /** diff --git a/src/ol/renderer/canvas/VectorImageLayer.js b/src/ol/renderer/canvas/VectorImageLayer.js index 76ec2f8e0f..bcc4ce4d85 100644 --- a/src/ol/renderer/canvas/VectorImageLayer.js +++ b/src/ol/renderer/canvas/VectorImageLayer.js @@ -6,11 +6,11 @@ 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'; import {getHeight, getWidth, isEmpty, scaleFromCenter} from '../../extent.js'; -import {renderDeclutterItems} from '../../render.js'; /** * @classdesc @@ -115,7 +115,7 @@ class CanvasVectorImageLayerRenderer extends CanvasImageLayerRenderer { {}, frameState, { - declutterItems: [], + declutterTree: new RBush(9), extent: renderedExtent, size: [width, height], viewState: /** @type {import("../../View.js").State} */ (assign( @@ -139,7 +139,7 @@ class CanvasVectorImageLayerRenderer extends CanvasImageLayerRenderer { ) { vectorRenderer.clipping = false; vectorRenderer.renderFrame(imageFrameState, null); - renderDeclutterItems(imageFrameState, null); + vectorRenderer.renderDeclutter(imageFrameState); callback(); } } @@ -186,37 +186,32 @@ class CanvasVectorImageLayerRenderer extends CanvasImageLayerRenderer { */ postRender() {} + /** + */ + renderDeclutter() {} + /** * @param {import("../../coordinate.js").Coordinate} coordinate Coordinate. * @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..0939d7bd3e 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, @@ -130,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} @@ -150,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. @@ -171,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; } @@ -193,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; @@ -214,56 +282,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { } } - const viewHints = frameState.viewHints; - const snapToPixel = !( - 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 - ? 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, - declutterReplays - ); - } 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 - ); - } + this.renderWorlds(replayGroup, frameState); if (clipped) { context.restore(); @@ -388,44 +407,51 @@ 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 { 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); - } - }, - layer.getDeclutter() ? declutteredFeatures : null - ); + /** + * @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; } @@ -556,10 +582,19 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { getRenderTolerance(resolution, pixelRatio), extent, resolution, - pixelRatio, - vectorLayer.getDeclutter() + pixelRatio ); + let declutterBuilderGroup; + if (this.getLayer().getDeclutter()) { + declutterBuilderGroup = new CanvasBuilderGroup( + getRenderTolerance(resolution, pixelRatio), + extent, + resolution, + pixelRatio + ); + } + const userProjection = getUserProjection(); let userTransform; if (userProjection) { @@ -597,7 +632,8 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { squaredTolerance, styles, replayGroup, - userTransform + userTransform, + declutterBuilderGroup ); this.dirty_ = this.dirty_ || dirty; } @@ -624,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; @@ -643,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( @@ -650,7 +698,8 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { squaredTolerance, styles, builderGroup, - opt_transform + opt_transform, + opt_declutterBuilderGroup ) { if (!styles) { return false; @@ -665,7 +714,8 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { styles[i], squaredTolerance, this.boundHandleStyleImageChange_, - opt_transform + opt_transform, + opt_declutterBuilderGroup ) || loading; } } else { @@ -675,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 efb3763063..71d2a9a6f9 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'; @@ -262,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); @@ -270,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,9 +294,11 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { 0, sharedExtent, resolution, - pixelRatio, - layer.getDeclutter() + pixelRatio ); + const declutterBuilderGroup = declutter + ? new CanvasBuilderGroup(0, sharedExtent, resolution, pixelRatio) + : undefined; const squaredTolerance = getSquaredRenderTolerance( resolution, pixelRatio @@ -316,7 +320,8 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { feature, squaredTolerance, styles, - builderGroup + builderGroup, + declutterBuilderGroup ); this.dirty_ = this.dirty_ || dirty; builderState.dirty = builderState.dirty || dirty; @@ -340,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; @@ -353,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; @@ -365,17 +381,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; @@ -405,39 +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 || - (declutteredFeatures && - declutteredFeatures.indexOf(feature) !== -1) - ) { - let key = feature.getId(); - if (key === undefined) { - key = getUid(feature); - } - if (!(key in features)) { - features[key] = true; - return callback(feature, layer); - } - } - }, - layer.getDeclutter() ? declutteredFeatures : null - ); + 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; } @@ -554,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. @@ -587,49 +665,18 @@ 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; - 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) { @@ -640,27 +687,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 +719,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 +729,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; } @@ -718,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; } @@ -730,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 579f15960f..2c626a9eb2 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) { @@ -75,12 +82,12 @@ function renderCircleGeometry(builderGroup, geometry, style, feature) { circleReplay.drawCircle(geometry, feature); } const textStyle = style.getText(); - if (textStyle) { - const textReplay = builderGroup.getBuilder( + if (textStyle && textStyle.getText()) { + const textReplay = (opt_declutterBuilderGroup || builderGroup).getBuilder( style.getZIndex(), BuilderType.TEXT ); - textReplay.setTextStyle(textStyle, builderGroup.addDeclutter(false)); + textReplay.setTextStyle(textStyle); textReplay.drawText(geometry, feature); } } @@ -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( @@ -219,12 +251,12 @@ function renderLineStringGeometry(builderGroup, geometry, style, feature) { lineStringReplay.drawLineString(geometry, feature); } const textStyle = style.getText(); - if (textStyle) { - const textReplay = builderGroup.getBuilder( + if (textStyle && textStyle.getText()) { + const textReplay = (opt_declutterBuilderGroup || builderGroup).getBuilder( style.getZIndex(), BuilderType.TEXT ); - textReplay.setTextStyle(textStyle, builderGroup.addDeclutter(false)); + textReplay.setTextStyle(textStyle); textReplay.drawText(geometry, feature); } } @@ -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( @@ -246,12 +285,12 @@ function renderMultiLineStringGeometry(builderGroup, geometry, style, feature) { lineStringReplay.drawMultiLineString(geometry, feature); } const textStyle = style.getText(); - if (textStyle) { - const textReplay = builderGroup.getBuilder( + if (textStyle && textStyle.getText()) { + const textReplay = (opt_declutterBuilderGroup || builderGroup).getBuilder( style.getZIndex(), BuilderType.TEXT ); - textReplay.setTextStyle(textStyle, builderGroup.addDeclutter(false)); + textReplay.setTextStyle(textStyle); textReplay.drawText(geometry, feature); } } @@ -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) { @@ -274,12 +320,12 @@ function renderMultiPolygonGeometry(builderGroup, geometry, style, feature) { polygonReplay.drawMultiPolygon(geometry, feature); } const textStyle = style.getText(); - if (textStyle) { - const textReplay = builderGroup.getBuilder( + if (textStyle && textStyle.getText()) { + const textReplay = (opt_declutterBuilderGroup || builderGroup).getBuilder( style.getZIndex(), BuilderType.TEXT ); - textReplay.setTextStyle(textStyle, builderGroup.addDeclutter(false)); + textReplay.setTextStyle(textStyle); textReplay.drawText(geometry, feature); } } @@ -289,9 +335,24 @@ 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(); + /** @type {import("../render/canvas.js").DeclutterImageWithText} */ + let declutterImageWithText; + if (opt_declutterBuilderGroup) { + builderGroup = opt_declutterBuilderGroup; + declutterImageWithText = + imageStyle && textStyle && textStyle.getText() ? {} : undefined; + } if (imageStyle) { if (imageStyle.getImageState() != ImageState.LOADED) { return; @@ -300,16 +361,15 @@ function renderPointGeometry(builderGroup, geometry, style, feature) { style.getZIndex(), BuilderType.IMAGE ); - imageReplay.setImageStyle(imageStyle, builderGroup.addDeclutter(false)); + imageReplay.setImageStyle(imageStyle, declutterImageWithText); imageReplay.drawPoint(geometry, feature); } - const textStyle = style.getText(); - if (textStyle) { + if (textStyle && textStyle.getText()) { const textReplay = builderGroup.getBuilder( style.getZIndex(), BuilderType.TEXT ); - textReplay.setTextStyle(textStyle, builderGroup.addDeclutter(!!imageStyle)); + textReplay.setTextStyle(textStyle, declutterImageWithText); textReplay.drawText(geometry, feature); } } @@ -319,9 +379,24 @@ 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(); + /** @type {import("../render/canvas.js").DeclutterImageWithText} */ + let declutterImageWithText; + if (opt_declutterBuilderGroup) { + builderGroup = opt_declutterBuilderGroup; + declutterImageWithText = + imageStyle && textStyle && textStyle.getText() ? {} : undefined; + } if (imageStyle) { if (imageStyle.getImageState() != ImageState.LOADED) { return; @@ -330,16 +405,15 @@ function renderMultiPointGeometry(builderGroup, geometry, style, feature) { style.getZIndex(), BuilderType.IMAGE ); - imageReplay.setImageStyle(imageStyle, builderGroup.addDeclutter(false)); + imageReplay.setImageStyle(imageStyle, declutterImageWithText); imageReplay.drawMultiPoint(geometry, feature); } - const textStyle = style.getText(); - if (textStyle) { - const textReplay = builderGroup.getBuilder( + if (textStyle && textStyle.getText()) { + const textReplay = (opt_declutterBuilderGroup || builderGroup).getBuilder( style.getZIndex(), BuilderType.TEXT ); - textReplay.setTextStyle(textStyle, builderGroup.addDeclutter(!!imageStyle)); + textReplay.setTextStyle(textStyle, declutterImageWithText); textReplay.drawText(geometry, feature); } } @@ -349,8 +423,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) { @@ -362,12 +443,12 @@ function renderPolygonGeometry(builderGroup, geometry, style, feature) { polygonReplay.drawPolygon(geometry, feature); } const textStyle = style.getText(); - if (textStyle) { - const textReplay = builderGroup.getBuilder( + if (textStyle && textStyle.getText()) { + const textReplay = (opt_declutterBuilderGroup || builderGroup).getBuilder( 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 0a85511d3b..9ca0d9e7ba 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, @@ -565,7 +566,6 @@ class RasterSource extends ImageSource { }), viewHints: [], wantedTiles: {}, - declutterItems: [], }; this.setAttributions(function (frameState) {