From 821001b5d967e6b3eda886d9ee9cfe662eaacd79 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Tue, 5 Mar 2024 23:25:48 +0100 Subject: [PATCH] Fix decluttering opacity, zIndex and circles --- src/ol/render/canvas/ExecutorGroup.js | 5 +- src/ol/renderer/canvas/VectorLayer.js | 54 ++++---- src/ol/renderer/vector.js | 5 +- .../cases/layer-vector-declutter/expected.png | Bin 0 -> 5744 bytes .../cases/layer-vector-declutter/main.js | 131 ++++++++++++++++++ 5 files changed, 167 insertions(+), 28 deletions(-) create mode 100644 test/rendering/cases/layer-vector-declutter/expected.png create mode 100644 test/rendering/cases/layer-vector-declutter/main.js diff --git a/src/ol/render/canvas/ExecutorGroup.js b/src/ol/render/canvas/ExecutorGroup.js index 74f7949601b..bf6ad4795dd 100644 --- a/src/ol/render/canvas/ExecutorGroup.js +++ b/src/ol/render/canvas/ExecutorGroup.js @@ -426,8 +426,9 @@ class ExecutorGroup { renderDeferred() { const deferredZIndexContexts = this.deferredZIndexContexts_; - for (const key in deferredZIndexContexts) { - deferredZIndexContexts[key].forEach((zIndexContext) => { + const zs = Object.keys(deferredZIndexContexts).sort(ascending); + for (let i = 0, ii = zs.length; i < ii; ++i) { + deferredZIndexContexts[zs[i]].forEach((zIndexContext) => { zIndexContext.draw(this.renderedContext_); // FIXME Pass clip to replay for temporarily enabling clip zIndexContext.clear(); }); diff --git a/src/ol/renderer/canvas/VectorLayer.js b/src/ol/renderer/canvas/VectorLayer.js index 169bf87786c..eec08ef9ef2 100644 --- a/src/ol/renderer/canvas/VectorLayer.js +++ b/src/ol/renderer/canvas/VectorLayer.js @@ -153,7 +153,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { * @private * @type {CanvasRenderingContext2D} */ - this.compositionContext_ = null; + this.targetContext_ = null; /** * @private @@ -183,7 +183,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { const snapToPixel = !( viewHints[ViewHint.ANIMATING] || viewHints[ViewHint.INTERACTING] ); - const context = this.compositionContext_; + const context = this.context; const width = Math.round(frameState.size[0] * pixelRatio); const height = Math.round(frameState.size[1] * pixelRatio); @@ -223,28 +223,33 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { } while (++world < endWorld); } - setupCompositionContext_() { + /** + * @private + */ + setDrawContext_() { if (this.opacity_ !== 1) { - const compositionContext = createCanvasContext2D( + this.targetContext_ = this.context; + this.context = createCanvasContext2D( this.context.canvas.width, this.context.canvas.height, canvasPool, ); - this.compositionContext_ = compositionContext; - } else { - this.compositionContext_ = this.context; } } - releaseCompositionContext_() { + /** + * @private + */ + resetDrawContext_() { if (this.opacity_ !== 1) { - const alpha = this.context.globalAlpha; - this.context.globalAlpha = this.opacity_; - this.context.drawImage(this.compositionContext_.canvas, 0, 0); - this.context.globalAlpha = alpha; - releaseCanvas(this.compositionContext_); - canvasPool.push(this.compositionContext_.canvas); - this.compositionContext_ = null; + const alpha = this.targetContext_.globalAlpha; + this.targetContext_.globalAlpha = this.opacity_; + this.targetContext_.drawImage(this.context.canvas, 0, 0); + this.targetContext_.globalAlpha = alpha; + releaseCanvas(this.context); + canvasPool.push(this.context.canvas); + this.context = this.targetContext_; + this.targetContext_ = null; } } @@ -257,9 +262,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { if (!declutter) { return; } - this.setupCompositionContext_(); //FIXME Check if this works, or if we need to defer something. this.renderWorlds(this.replayGroup_, frameState, true); - this.releaseCompositionContext_(); } /** @@ -268,6 +271,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { */ renderDeferredInternal(frameState) { this.replayGroup_.renderDeferred(); + this.resetDrawContext_(); } /** @@ -279,6 +283,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { renderFrame(frameState, target) { const pixelRatio = frameState.pixelRatio; const layerState = frameState.layerStatesArray[frameState.layerIndex]; + this.opacity_ = layerState.opacity; // set forward and inverse pixel transforms makeScale(this.pixelTransform, 1 / pixelRatio, 1 / pixelRatio); @@ -287,6 +292,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { const canvasTransform = transformToString(this.pixelTransform); this.useContainer(target, canvasTransform, this.getBackground(frameState)); + const context = this.context; const canvas = context.canvas; @@ -314,14 +320,13 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { context.clearRect(0, 0, width, height); } + this.setDrawContext_(); + this.preRender(context, frameState); const viewState = frameState.viewState; const projection = viewState.projection; - this.opacity_ = layerState.opacity; - this.setupCompositionContext_(); - // clipped rendering if layer extent is set let clipped = false; if (render && layerState.extent && this.clipping) { @@ -329,7 +334,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { render = intersectsExtent(layerExtent, frameState.extent); clipped = render && !containsExtent(layerExtent, frameState.extent); if (clipped) { - this.clipUnrotated(this.compositionContext_, frameState, layerExtent); + this.clipUnrotated(context, frameState, layerExtent); } } @@ -342,17 +347,18 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { } if (clipped) { - this.compositionContext_.restore(); + context.restore(); } - this.releaseCompositionContext_(); - this.postRender(context, frameState); if (this.renderedRotation_ !== viewState.rotation) { this.renderedRotation_ = viewState.rotation; this.hitDetectionImageData_ = null; } + if (!frameState.declutter) { + this.resetDrawContext_(); + } return this.container; } diff --git a/src/ol/renderer/vector.js b/src/ol/renderer/vector.js index af3d8e3b7ff..8dde58edbba 100644 --- a/src/ol/renderer/vector.js +++ b/src/ol/renderer/vector.js @@ -70,14 +70,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 {number} [index] Render order index. */ -function renderCircleGeometry(builderGroup, geometry, style, feature) { +function renderCircleGeometry(builderGroup, geometry, style, feature, index) { const fillStyle = style.getFill(); const strokeStyle = style.getStroke(); if (fillStyle || strokeStyle) { const circleReplay = builderGroup.getBuilder(style.getZIndex(), 'Circle'); circleReplay.setFillStrokeStyle(fillStyle, strokeStyle); - circleReplay.drawCircle(geometry, feature); + circleReplay.drawCircle(geometry, feature, index); } const textStyle = style.getText(); if (textStyle && textStyle.getText()) { diff --git a/test/rendering/cases/layer-vector-declutter/expected.png b/test/rendering/cases/layer-vector-declutter/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..b52772f294df2568c1efb9e9f29bf56cad35952d GIT binary patch literal 5744 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