diff --git a/rendering/cases/multiple-layers/main.js b/rendering/cases/multiple-layers/main.js index c5dd78ae69..0d2df97582 100644 --- a/rendering/cases/multiple-layers/main.js +++ b/rendering/cases/multiple-layers/main.js @@ -9,12 +9,6 @@ import Point from '../../../src/ol/geom/Point.js'; const map = new Map({ layers: [ - new TileLayer({ - source: new XYZ({ - url: '/data/tiles/satellite/{z}/{x}/{y}.jpg', - maxZoom: 3 - }) - }), new VectorLayer({ zIndex: 1, style: new Style({ @@ -27,6 +21,12 @@ const map = new Map({ url: '/data/countries.json', format: new GeoJSON() }) + }), + new TileLayer({ + source: new XYZ({ + url: '/data/tiles/satellite/{z}/{x}/{y}.jpg', + maxZoom: 3 + }) }) ], target: 'map', diff --git a/src/ol/PluggableMap.js b/src/ol/PluggableMap.js index b73e6489ba..46e3233525 100644 --- a/src/ol/PluggableMap.js +++ b/src/ol/PluggableMap.js @@ -39,7 +39,7 @@ import {create as createTransform, apply as applyTransform} from './transform.js * @property {boolean} animate * @property {import("./transform.js").Transform} coordinateToPixelTransform * @property {null|import("./extent.js").Extent} extent - * @property {Array<*>} declutterItems + * @property {Array} declutterItems * @property {import("./coordinate.js").Coordinate} focus * @property {number} index * @property {Array} layerStatesArray @@ -54,6 +54,13 @@ import {create as createTransform, apply as applyTransform} from './transform.js */ +/** + * @typedef {Object} DeclutterItems + * @property {Array<*>} items Declutter items of an executor. + * @property {number} opacity Layer opacity. + */ + + /** * @typedef {function(PluggableMap, ?FrameState): any} PostRenderFunction */ diff --git a/src/ol/layer/Layer.js b/src/ol/layer/Layer.js index 23a4324a63..424429cc26 100644 --- a/src/ol/layer/Layer.js +++ b/src/ol/layer/Layer.js @@ -189,13 +189,15 @@ class Layer extends BaseLayer { * In charge to manage the rendering of the layer. One layer type is * bounded with one layer renderer. * @param {?import("../PluggableMap.js").FrameState} frameState Frame state. + * @param {HTMLElement} target Target which the renderer may (but need not) use + * for rendering its content. * @return {HTMLElement} The rendered element. */ - render(frameState) { + render(frameState, target) { const layerRenderer = this.getRenderer(); const layerState = this.getLayerState(); if (layerRenderer.prepareFrame(frameState, layerState)) { - return layerRenderer.renderFrame(frameState, layerState); + return layerRenderer.renderFrame(frameState, layerState, target); } } diff --git a/src/ol/render.js b/src/ol/render.js index c413506596..fb48e303bd 100644 --- a/src/ol/render.js +++ b/src/ol/render.js @@ -122,9 +122,10 @@ export function renderDeclutterItems(frameState, declutterTree) { } const items = frameState.declutterItems; for (let z = items.length - 1; z >= 0; --z) { - const zIndexItems = items[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], declutterTree); + declutterTree = zIndexItems[i].renderDeclutter(zIndexItems[i + 1], zIndexItems[i + 2], item.opacity, declutterTree); } } items.length = 0; diff --git a/src/ol/render/canvas.js b/src/ol/render/canvas.js index 3e55f4216f..0dc981a605 100644 --- a/src/ol/render/canvas.js +++ b/src/ol/render/canvas.js @@ -403,7 +403,7 @@ export function drawImage(context, context.drawImage(image, originX, originY, w, h, x, y, w * scale, h * scale); - if (alpha) { + if (opacity != 1) { context.globalAlpha = alpha; } if (transform) { diff --git a/src/ol/render/canvas/Executor.js b/src/ol/render/canvas/Executor.js index 0f2b0a75f1..afb0198a8c 100644 --- a/src/ol/render/canvas/Executor.js +++ b/src/ol/render/canvas/Executor.js @@ -362,10 +362,12 @@ class Executor extends Disposable { const declutterArgs = intersects ? [context, transform ? transform.slice(0) : null, opacity, image, originX, originY, w, h, x, y, scale] : null; - if (declutterArgs && fillStroke) { - declutterArgs.push(fillInstruction, strokeInstruction, p1, p2, p3, p4); + if (declutterArgs) { + if (fillStroke) { + declutterArgs.push(fillInstruction, strokeInstruction, p1, p2, p3, p4); + } + declutterGroup.push(declutterArgs); } - declutterGroup.push(declutterArgs); } else if (intersects) { if (fillStroke) { this.replayTextBackground_(context, p1, p2, p3, p4, @@ -414,10 +416,11 @@ class Executor extends Disposable { /** * @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, declutterTree) { + renderDeclutter(declutterGroup, feature, opacity, declutterTree) { if (declutterGroup && declutterGroup.length > 5) { const groupCount = declutterGroup[4]; if (groupCount == 1 || groupCount == declutterGroup.length - 5) { @@ -436,13 +439,19 @@ class Executor extends Disposable { declutterTree.insert(box); for (let j = 5, jj = declutterGroup.length; j < jj; ++j) { const declutterData = /** @type {Array} */ (declutterGroup[j]); - if (declutterData) { - if (declutterData.length > 11) { - this.replayTextBackground_(declutterData[0], - declutterData[13], declutterData[14], declutterData[15], declutterData[16], - declutterData[11], declutterData[12]); - } - drawImage.apply(undefined, declutterData); + const context = declutterData[0]; + const currentAlpha = context.globalAlpha; + if (currentAlpha !== opacity) { + context.globalAlpha = opacity; + } + if (declutterData.length > 11) { + this.replayTextBackground_(declutterData[0], + declutterData[13], declutterData[14], declutterData[15], declutterData[16], + declutterData[11], declutterData[12]); + } + drawImage.apply(undefined, declutterData); + if (currentAlpha !== opacity) { + context.globalAlpha = currentAlpha; } } } diff --git a/src/ol/render/canvas/ExecutorGroup.js b/src/ol/render/canvas/ExecutorGroup.js index ea28923ee6..1d74dd0eb1 100644 --- a/src/ol/render/canvas/ExecutorGroup.js +++ b/src/ol/render/canvas/ExecutorGroup.js @@ -430,10 +430,11 @@ export function getCircleArray(radius) { * @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. + * @param {Array} declutterItems Declutter items. */ -export function replayDeclutter(declutterReplays, context, rotation, snapToPixel, declutterItems) { +export function replayDeclutter(declutterReplays, context, rotation, opacity, snapToPixel, declutterItems) { const zs = Object.keys(declutterReplays).map(Number).sort(numberSafeCompareFunction); const skippedFeatureUids = {}; for (let z = 0, zz = zs.length; z < zz; ++z) { @@ -443,7 +444,10 @@ export function replayDeclutter(declutterReplays, context, rotation, snapToPixel const executor = executorData[i++]; if (executor !== currentExecutor) { currentExecutor = executor; - declutterItems.push(executor.declutterItems); + declutterItems.push({ + items: executor.declutterItems, + opacity: opacity + }); } const transform = executorData[i++]; executor.execute(context, transform, rotation, skippedFeatureUids, snapToPixel); diff --git a/src/ol/renderer/Composite.js b/src/ol/renderer/Composite.js index e1e68f70cd..3934b5b14d 100644 --- a/src/ol/renderer/Composite.js +++ b/src/ol/renderer/Composite.js @@ -81,10 +81,13 @@ class CompositeMapRenderer extends MapRenderer { this.calculateMatrices2D(frameState); this.dispatchRenderEvent(RenderEventType.PRECOMPOSE, frameState); - const layerStatesArray = frameState.layerStatesArray; + const layerStatesArray = frameState.layerStatesArray.sort(function(a, b) { + return a.zIndex - b.zIndex; + }); const viewResolution = frameState.viewState.resolution; this.children_.length = 0; + let previousElement = null; for (let i = 0, ii = layerStatesArray.length; i < ii; ++i) { const layerState = layerStatesArray[i]; if (!visibleAtResolution(layerState, viewResolution) || @@ -93,13 +96,10 @@ class CompositeMapRenderer extends MapRenderer { } const layer = layerState.layer; - const element = layer.render(frameState); - if (element) { - const zIndex = layerState.zIndex; - if (zIndex !== element.style.zIndex) { - element.style.zIndex = zIndex === Infinity ? Number.MAX_SAFE_INTEGER : zIndex; - } + const element = layer.render(frameState, previousElement); + if (element !== previousElement) { this.children_.push(element); + previousElement = element; } } super.renderFrame(frameState); diff --git a/src/ol/renderer/Layer.js b/src/ol/renderer/Layer.js index 0b11f0d8e5..31c052354b 100644 --- a/src/ol/renderer/Layer.js +++ b/src/ol/renderer/Layer.js @@ -41,9 +41,10 @@ class LayerRenderer extends Observable { * @abstract * @param {import("../PluggableMap.js").FrameState} frameState Frame state. * @param {import("../layer/Layer.js").State} layerState Layer state. + * @param {HTMLElement} target Target that may be used to render content to. * @return {HTMLElement} The rendered element. */ - renderFrame(frameState, layerState) { + renderFrame(frameState, layerState, target) { return abstract(); } diff --git a/src/ol/renderer/canvas/ImageLayer.js b/src/ol/renderer/canvas/ImageLayer.js index 26793e7a1d..bd40b16878 100644 --- a/src/ol/renderer/canvas/ImageLayer.js +++ b/src/ol/renderer/canvas/ImageLayer.js @@ -72,7 +72,7 @@ class CanvasImageLayerRenderer extends CanvasLayerRenderer { /** * @inheritDoc */ - renderFrame(frameState, layerState) { + renderFrame(frameState, layerState, target) { const image = this.image_; const imageExtent = image.getExtent(); const imageResolution = image.getResolution(); @@ -101,13 +101,15 @@ class CanvasImageLayerRenderer extends CanvasLayerRenderer { ); makeInverse(this.inversePixelTransform_, this.pixelTransform_); + this.useContainer(target, this.pixelTransform_, layerState.opacity); + const context = this.context; const canvas = context.canvas; if (canvas.width != width || canvas.height != height) { canvas.width = width; canvas.height = height; - } else { + } else if (!this.containerReused) { context.clearRect(0, 0, width, height); } @@ -138,8 +140,17 @@ class CanvasImageLayerRenderer extends CanvasLayerRenderer { this.preRender(context, frameState); if (dw >= 0.5 && dh >= 0.5) { + const opacity = layerState.opacity; + let previousAlpha; + if (opacity !== 1) { + previousAlpha = this.context.globalAlpha; + this.context.globalAlpha = opacity; + } this.context.drawImage(img, 0, 0, +img.width, +img.height, Math.round(dx), Math.round(dy), Math.round(dw), Math.round(dh)); + if (opacity !== 1) { + this.context.globalAlpha = previousAlpha; + } } this.postRender(context, frameState); @@ -147,17 +158,12 @@ class CanvasImageLayerRenderer extends CanvasLayerRenderer { context.restore(); } - const opacity = layerState.opacity; - if (opacity !== parseFloat(canvas.style.opacity)) { - canvas.style.opacity = opacity; - } - const canvasTransform = transformToString(this.pixelTransform_); if (canvasTransform !== canvas.style.transform) { canvas.style.transform = canvasTransform; } - return canvas; + return this.container; } diff --git a/src/ol/renderer/canvas/Layer.js b/src/ol/renderer/canvas/Layer.js index aa7531c0f0..8fd413a508 100644 --- a/src/ol/renderer/canvas/Layer.js +++ b/src/ol/renderer/canvas/Layer.js @@ -7,7 +7,7 @@ import RenderEvent from '../../render/Event.js'; import RenderEventType from '../../render/EventType.js'; import {rotateAtOffset} from '../../render/canvas.js'; import LayerRenderer from '../Layer.js'; -import {create as createTransform, apply as applyTransform, compose as composeTransform} from '../../transform.js'; +import {create as createTransform, apply as applyTransform, compose as composeTransform, toString as transformToString} from '../../transform.js'; /** * @abstract @@ -21,6 +21,12 @@ class CanvasLayerRenderer extends LayerRenderer { super(layer); + /** + * @protected + * @type {HTMLElement} + */ + this.container = null; + /** * @protected * @type {number} @@ -55,20 +61,57 @@ class CanvasLayerRenderer extends LayerRenderer { * @protected * @type {CanvasRenderingContext2D} */ - this.context = createCanvasContext2D(); + this.context = null; + + /** + * @type {boolean} + */ + this.containerReused = false; - const canvas = this.context.canvas; - canvas.style.position = 'absolute'; - canvas.style.transformOrigin = 'top left'; - canvas.className = this.getLayer().getClassName(); } /** - * @inheritDoc + * Get a rendering container from an existing target, if compatible. + * @param {HTMLElement} target Potential render target. + * @param {import("../../transform").Transform} transform Transform. + * @param {number} opacity Opacity. */ - disposeInternal() { - this.context.canvas.width = this.context.canvas.height = 0; - super.disposeInternal(); + useContainer(target, transform, opacity) { + const layerClassName = this.getLayer().getClassName(); + let container, context; + if (target && target.style.opacity === '' && target.className === layerClassName) { + const canvas = target.firstElementChild; + if (canvas instanceof HTMLCanvasElement) { + context = canvas.getContext('2d'); + } + } + if (context && context.canvas.style.transform === transformToString(transform)) { + // Container of the previous layer renderer can be used. + this.container = target; + this.context = context; + this.containerReused = true; + } else if (this.containerReused) { + // Previously reused container cannot be used any more. + this.container = null; + this.context = null; + this.containerReused = false; + } + if (!this.container) { + container = document.createElement('div'); + container.className = layerClassName; + let style = container.style; + style.position = 'absolute'; + style.width = '100%'; + style.height = '100%'; + context = createCanvasContext2D(); + const canvas = context.canvas; + container.appendChild(canvas); + style = canvas.style; + style.position = 'absolute'; + style.transformOrigin = 'top left'; + this.container = container; + this.context = context; + } } /** diff --git a/src/ol/renderer/canvas/TileLayer.js b/src/ol/renderer/canvas/TileLayer.js index 512fb58a4b..dd65150e9a 100644 --- a/src/ol/renderer/canvas/TileLayer.js +++ b/src/ol/renderer/canvas/TileLayer.js @@ -137,8 +137,7 @@ class CanvasTileLayerRenderer extends CanvasLayerRenderer { * @inheritDoc * @returns {HTMLElement} The rendered element. */ - renderFrame(frameState, layerState) { - const context = this.context; + renderFrame(frameState, layerState, target) { const viewState = frameState.viewState; const projection = viewState.projection; const viewResolution = viewState.resolution; @@ -224,7 +223,6 @@ class CanvasTileLayerRenderer extends CanvasLayerRenderer { } - const canvas = context.canvas; const canvasScale = tileResolution / viewResolution; // set forward and inverse pixel transforms @@ -234,6 +232,11 @@ class CanvasTileLayerRenderer extends CanvasLayerRenderer { rotation, -width / 2, -height / 2 ); + + this.useContainer(target, this.pixelTransform_, layerState.opacity); + const context = this.context; + const canvas = context.canvas; + makeInverse(this.inversePixelTransform_, this.pixelTransform_); // set scale transform for calculating tile positions on the canvas @@ -247,7 +250,7 @@ class CanvasTileLayerRenderer extends CanvasLayerRenderer { if (canvas.width != width || canvas.height != height) { canvas.width = width; canvas.height = height; - } else { + } else if (!this.containerReused) { context.clearRect(0, 0, width, height); } @@ -259,7 +262,7 @@ class CanvasTileLayerRenderer extends CanvasLayerRenderer { this.renderedTiles.length = 0; /** @type {Array} */ - const zs = Object.keys(tilesToDrawByZ).map(Number); + let zs = Object.keys(tilesToDrawByZ).map(Number); zs.sort(function(a, b) { if (a === z) { return 1; @@ -270,7 +273,14 @@ class CanvasTileLayerRenderer extends CanvasLayerRenderer { } }); - for (let i = 0, ii = zs.length; i < ii; ++i) { + let clips, clipZs, currentClip; + if (layerState.opacity === 1 && (!this.containerReused || tileSource.getOpaque(frameState.viewState.projection))) { + zs = zs.reverse(); + } else { + clips = []; + clipZs = []; + } + for (let i = zs.length - 1; i >= 0; --i) { const currentZ = zs[i]; const currentTilePixelSize = tileSource.getTilePixelSize(currentZ, pixelRatio, projection); const currentResolution = tileGrid.getResolution(currentZ); @@ -298,8 +308,36 @@ class CanvasTileLayerRenderer extends CanvasLayerRenderer { const y = Math.round(floatY); const w = nextX - x; const h = nextY - y; + const transition = z === currentZ; - this.drawTileImage(tile, frameState, x, y, w, h, tileGutter, z === currentZ); + if (clips && (!transition || tile.getAlpha(getUid(this), frameState.time) === 1)) { + // Clip mask for regions in this tile that already filled by a higher z tile + context.save(); + currentClip = [x, y, x + w, y, x + w, y + h, x, y + h]; + for (let i = 0, ii = clips.length; i < ii; ++i) { + if (z !== currentZ && currentZ < clipZs[i]) { + const clip = clips[i]; + 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 z 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(); + } + } + clips.push(currentClip); + clipZs.push(currentZ); + } + this.drawTileImage(tile, frameState, x, y, w, h, tileGutter, transition, layerState.opacity); + if (clips) { + context.restore(); + } this.renderedTiles.push(tile); this.updateUsedTiles(frameState.usedTiles, tileSource, tile); } @@ -322,17 +360,12 @@ class CanvasTileLayerRenderer extends CanvasLayerRenderer { context.restore(); } - const opacity = layerState.opacity; - if (opacity !== parseFloat(canvas.style.opacity)) { - canvas.style.opacity = opacity; - } - const canvasTransform = transformToString(this.pixelTransform_); if (canvasTransform !== canvas.style.transform) { canvas.style.transform = canvasTransform; } - return canvas; + return this.container; } /** @@ -344,19 +377,15 @@ class CanvasTileLayerRenderer extends CanvasLayerRenderer { * @param {number} h Height of the tile. * @param {number} gutter Tile gutter. * @param {boolean} transition Apply an alpha transition. + * @param {number} opacity Opacity. */ - drawTileImage(tile, frameState, x, y, w, h, gutter, transition) { + drawTileImage(tile, frameState, x, y, w, h, gutter, transition, opacity) { const image = this.getTileImage(tile); if (!image) { return; } const uid = getUid(this); - const alpha = transition ? tile.getAlpha(uid, frameState.time) : 1; - const tileLayer = /** @type {import("../../layer/Tile.js").default} */ (this.getLayer()); - const tileSource = tileLayer.getSource(); - if (alpha === 1 && !tileSource.getOpaque(frameState.viewState.projection)) { - this.context.clearRect(x, y, w, h); - } + const alpha = opacity * (transition ? tile.getAlpha(uid, frameState.time) : 1); const alphaChanged = alpha !== this.context.globalAlpha; if (alphaChanged) { this.context.save(); diff --git a/src/ol/renderer/canvas/VectorImageLayer.js b/src/ol/renderer/canvas/VectorImageLayer.js index 13a3e14c14..c4b6d98c27 100644 --- a/src/ol/renderer/canvas/VectorImageLayer.js +++ b/src/ol/renderer/canvas/VectorImageLayer.js @@ -78,6 +78,7 @@ class CanvasVectorImageLayerRenderer extends CanvasImageLayerRenderer { if (!hints[ViewHint.ANIMATING] && !hints[ViewHint.INTERACTING] && !isEmpty(renderedExtent)) { let skippedFeatures = this.skippedFeatures_; + vectorRenderer.useContainer(null, null, 1); const context = vectorRenderer.context; const imageFrameState = /** @type {import("../../PluggableMap.js").FrameState} */ (assign({}, frameState, { declutterItems: [], @@ -94,7 +95,7 @@ class CanvasVectorImageLayerRenderer extends CanvasImageLayerRenderer { if (vectorRenderer.prepareFrame(imageFrameState, layerState) && (vectorRenderer.replayGroupChanged || !equals(skippedFeatures, newSkippedFeatures))) { - vectorRenderer.renderFrame(imageFrameState, layerState); + vectorRenderer.renderFrame(imageFrameState, layerState, null); renderDeclutterItems(imageFrameState, null); skippedFeatures = newSkippedFeatures; callback(); diff --git a/src/ol/renderer/canvas/VectorLayer.js b/src/ol/renderer/canvas/VectorLayer.js index 3c481c9b62..9246bf3c6c 100644 --- a/src/ol/renderer/canvas/VectorLayer.js +++ b/src/ol/renderer/canvas/VectorLayer.js @@ -70,17 +70,17 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { /** * @inheritDoc */ - renderFrame(frameState, layerState) { - const context = this.context; - const canvas = context.canvas; - - const replayGroup = this.replayGroup_; - if (!replayGroup || replayGroup.isEmpty()) { - if (canvas.width > 0) { - canvas.width = 0; - } - return canvas; + useContainer(target, transform, opacity) { + if (opacity < 1) { + target = null; } + super.useContainer(target, transform, opacity); + } + + /** + * @inheritDoc + */ + renderFrame(frameState, layerState, target) { const pixelRatio = frameState.pixelRatio; @@ -88,6 +88,18 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { makeScale(this.pixelTransform_, 1 / pixelRatio, 1 / pixelRatio); makeInverse(this.inversePixelTransform_, this.pixelTransform_); + this.useContainer(target, this.pixelTransform_, layerState.opacity); + const context = this.context; + const canvas = context.canvas; + + const replayGroup = this.replayGroup_; + if (!replayGroup || replayGroup.isEmpty()) { + if (!this.containerReused && canvas.width > 0) { + canvas.width = 0; + } + return this.container; + } + // resize and clear const width = Math.round(frameState.size[0] * pixelRatio); const height = Math.round(frameState.size[1] * pixelRatio); @@ -98,7 +110,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { if (canvas.style.transform !== canvasTransform) { canvas.style.transform = canvasTransform; } - } else { + } else if (!this.containerReused) { context.clearRect(0, 0, width, height); } @@ -152,7 +164,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { if (declutterReplays) { const viewHints = frameState.viewHints; const hifi = !(viewHints[ViewHint.ANIMATING] || viewHints[ViewHint.INTERACTING]); - replayDeclutter(declutterReplays, context, rotation, hifi, frameState.declutterItems); + replayDeclutter(declutterReplays, context, rotation, 1, hifi, frameState.declutterItems); } if (clipped) { @@ -162,11 +174,12 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { this.postRender(context, frameState); const opacity = layerState.opacity; - if (opacity !== parseFloat(canvas.style.opacity)) { - canvas.style.opacity = opacity; + const container = this.container; + if (opacity !== parseFloat(container.style.opacity)) { + container.style.opacity = opacity === 1 ? '' : opacity; } - return canvas; + return this.container; } /** diff --git a/src/ol/renderer/canvas/VectorTileLayer.js b/src/ol/renderer/canvas/VectorTileLayer.js index 66f3645452..e59bcbafa5 100644 --- a/src/ol/renderer/canvas/VectorTileLayer.js +++ b/src/ol/renderer/canvas/VectorTileLayer.js @@ -24,7 +24,6 @@ import { makeInverse } from '../../transform.js'; import CanvasExecutorGroup, {replayDeclutter} from '../../render/canvas/ExecutorGroup.js'; -import {clear, isEmpty} from '../../obj.js'; /** @@ -59,32 +58,11 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { constructor(layer) { super(layer); - const baseCanvas = this.context.canvas; - /** * @private * @type {CanvasRenderingContext2D} */ - this.overlayContext_ = createCanvasContext2D(); - - const overlayCanvas = this.overlayContext_.canvas; - overlayCanvas.style.position = 'absolute'; - overlayCanvas.style.transformOrigin = 'top left'; - - const container = document.createElement('div'); - const style = container.style; - style.position = 'absolute'; - style.width = '100%'; - style.height = '100%'; - - container.appendChild(baseCanvas); - container.appendChild(overlayCanvas); - - /** - * @private - * @type {HTMLElement} - */ - this.container_ = container; + this.overlayContext_ = null; /** * The transform for rendered pixels to viewport CSS pixels for the overlay canvas. @@ -136,17 +114,43 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { /** * @inheritDoc */ - disposeInternal() { - this.overlayContext_.canvas.width = this.overlayContext_.canvas.height = 0; - super.disposeInternal(); + useContainer(target, transform, opacity) { + let overlayContext; + if (target && target.childElementCount === 2) { + overlayContext = target.lastElementChild.getContext('2d'); + if (!overlayContext) { + target = null; + } + } + const containerReused = this.containerReused; + super.useContainer(target, transform, opacity); + if (containerReused && !this.containerReused && !overlayContext) { + this.overlayContext_ = null; + } + if (this.containerReused && overlayContext) { + this.overlayContext_ = overlayContext; + } + if (!this.overlayContext_) { + const overlayContext = createCanvasContext2D(); + const style = overlayContext.canvas.style; + style.position = 'absolute'; + style.transformOrigin = 'top left'; + this.overlayContext_ = overlayContext; + } + if (this.container.childElementCount === 1) { + this.container.appendChild(this.overlayContext_.canvas); + } } /** * @param {import("../../VectorRenderTile.js").default} tile Tile. * @param {number} pixelRatio Pixel ratio. * @param {import("../../proj/Projection").default} projection Projection. + * @param {boolean} queue Queue tile for rendering. + * @return {boolean} Tile needs to be rendered. */ - prepareTile(tile, pixelRatio, projection) { + prepareTile(tile, pixelRatio, projection, queue) { + let render = false; const tileUid = getUid(tile); const state = tile.getState(); if (((state === TileState.LOADED && tile.hifi) || @@ -158,9 +162,13 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { if (state === TileState.LOADED || state === TileState.ERROR) { this.updateExecutorGroup_(tile, pixelRatio, projection); if (this.tileImageNeedsRender_(tile, pixelRatio, projection)) { - this.renderTileImageQueue_[tileUid] = tile; + render = true; + if (queue) { + this.renderTileImageQueue_[tileUid] = tile; + } } } + return render; } /** @@ -176,7 +184,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { tile.wantedResolution = resolution; const tileUid = getUid(tile); if (!(tileUid in this.tileListenerKeys_)) { - const listenerKey = listen(tile, EventType.CHANGE, this.prepareTile.bind(this, tile, pixelRatio, projection)); + const listenerKey = listen(tile, EventType.CHANGE, this.prepareTile.bind(this, tile, pixelRatio, projection, true)); this.tileListenerKeys_[tileUid] = listenerKey; } } else { @@ -185,7 +193,10 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { if (hifi || !tile.wantedResolution) { tile.wantedResolution = resolution; } - this.prepareTile(tile, pixelRatio, projection); + const render = this.prepareTile(tile, pixelRatio, projection, false); + if (render) { + this.renderTileImage_(tile, frameState); + } } return tile; } @@ -379,26 +390,30 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { /** * @inheritDoc */ - renderFrame(frameState, layerState) { - super.renderFrame(frameState, layerState); - - const layer = /** @type {import("../../layer/VectorTile.js").default} */ (this.getLayer()); + renderFrame(frameState, layerState, target) { const viewHints = frameState.viewHints; const hifi = !(viewHints[ViewHint.ANIMATING] || viewHints[ViewHint.INTERACTING]); + this.renderQueuedTileImages_(hifi, frameState); + + super.renderFrame(frameState, layerState, target); + + const layer = /** @type {import("../../layer/VectorTile.js").default} */ (this.getLayer()); const renderMode = layer.getRenderMode(); if (renderMode === VectorTileRenderType.IMAGE) { - this.renderTileImages_(hifi, frameState); - return this.container_; + return this.container; } - if (!isEmpty(this.renderTileImageQueue_) && !this.extentChanged) { - this.renderTileImages_(hifi, frameState); - return this.container_; + const source = layer.getSource(); + // Unqueue tiles from the image queue when we don't need any more + const usedTiles = frameState.usedTiles[getUid(source)]; + for (const tileUid in this.renderTileImageQueue_) { + if (!(tileUid in usedTiles)) { + delete this.renderTileImageQueue_[tileUid]; + } } const context = this.overlayContext_; const declutterReplays = layer.getDeclutter() ? {} : null; - const source = layer.getSource(); const replayTypes = VECTOR_REPLAYS[renderMode]; const pixelRatio = frameState.pixelRatio; const rotation = frameState.viewState.rotation; @@ -419,13 +434,14 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { if (canvas.style.transform !== canvasTransform) { canvas.style.transform = canvasTransform; } - } else { + } else if (!this.containerReused) { context.clearRect(0, 0, width, height); } 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]); if (tile.getState() == TileState.ABORT) { @@ -436,6 +452,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { const worldOffset = tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent)[0] - tileExtent[0]; const transform = this.getRenderTransform(frameState, width, height, worldOffset); const executorGroups = tile.executorGroups[getUid(layer)]; + let clipped = false; for (let t = 0, tt = executorGroups.length; t < tt; ++t) { const executorGroup = executorGroups[t]; if (!executorGroup.hasExecutors(replayTypes)) { @@ -443,9 +460,8 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { continue; } const currentZ = tile.tileCoord[0]; - let zs, currentClip; - if (!declutterReplays) { - zs = []; + let currentClip; + if (!declutterReplays && !clipped) { currentClip = executorGroup.getClipCoords(transform); context.save(); @@ -453,7 +469,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { // already filled by a higher resolution tile for (let j = 0, jj = clips.length; j < jj; ++j) { const clip = clips[j]; - if (currentZ < zs[j]) { + if (currentZ < clipZs[j]) { context.beginPath(); // counter-clockwise (outer ring) for current tile context.moveTo(currentClip[0], currentClip[1]); @@ -470,51 +486,37 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { } } executorGroup.execute(context, transform, rotation, {}, hifi, replayTypes, declutterReplays); - if (!declutterReplays) { + if (!declutterReplays && !clipped) { context.restore(); clips.push(currentClip); - zs.push(currentZ); + clipZs.push(currentZ); + clipped = true; } } } if (declutterReplays) { - replayDeclutter(declutterReplays, context, rotation, hifi, frameState.declutterItems); + replayDeclutter(declutterReplays, context, rotation, layerState.opacity, hifi, frameState.declutterItems); } - const opacity = layerState.opacity; - if (opacity !== parseFloat(canvas.style.opacity)) { - canvas.style.opacity = opacity; - } - - // Now that we have rendered the tiles we have already, let's prepare new tile images - // for the next frame - this.renderTileImages_(hifi, frameState); - - return this.container_; + return this.container; } /** * @param {boolean} hifi We have time to render a high fidelity map image. * @param {import('../../PluggableMap.js').FrameState} frameState Frame state. */ - renderTileImages_(hifi, frameState) { + renderQueuedTileImages_(hifi, frameState) { // When we don't have time to render hifi, only render tiles until we have used up // half of the frame budget of 16 ms for (const uid in this.renderTileImageQueue_) { if (!hifi && Date.now() - frameState.time > 8) { + frameState.animate = true; break; } const tile = this.renderTileImageQueue_[uid]; - frameState.animate = true; delete this.renderTileImageQueue_[uid]; - const layer = /** @type {import("../../layer/VectorTile.js").default} */ (this.getLayer()); - const viewState = frameState.viewState; - const tileGrid = layer.getSource().getTileGridForProjection(viewState.projection); - const tileResolution = tileGrid.getResolution(tile.tileCoord[0]); - const renderPixelRatio = frameState.pixelRatio / tile.wantedResolution * tileResolution; - this.renderTileImage_(tile, frameState.pixelRatio, renderPixelRatio, viewState.projection); + this.renderTileImage_(tile, frameState); } - clear(this.renderTileImageQueue_); } /** @@ -561,24 +563,29 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { /** * @param {import("../../VectorRenderTile.js").default} tile Tile. - * @param {number} pixelRatio Pixel ratio. - * @param {number} renderPixelRatio Render pixel ratio. - * @param {import("../../proj/Projection.js").default} projection Projection. + * @param {import("../../PluggableMap").FrameState} frameState Frame state. * @private */ - renderTileImage_(tile, pixelRatio, renderPixelRatio, projection) { + renderTileImage_(tile, frameState) { const layer = /** @type {import("../../layer/VectorTile.js").default} */ (this.getLayer()); const replayState = tile.getReplayState(layer); const revision = layer.getRevision(); const executorGroups = tile.executorGroups[getUid(layer)]; replayState.renderedTileRevision = revision; replayState.renderedTileZ = tile.sourceZ; + const tileCoord = tile.wrappedTileCoord; const z = tileCoord[0]; const source = layer.getSource(); + let pixelRatio = frameState.pixelRatio; + const viewState = frameState.viewState; + const projection = viewState.projection; const tileGrid = source.getTileGridForProjection(projection); + const tileResolution = tileGrid.getResolution(tile.tileCoord[0]); + const renderPixelRatio = frameState.pixelRatio / tile.wantedResolution * tileResolution; const resolution = tileGrid.getResolution(z); const context = tile.getContext(layer); + // Increase tile size when overzooming for low pixel ratio, to avoid blurry tiles pixelRatio = Math.max(pixelRatio, renderPixelRatio / pixelRatio); const size = source.getTilePixelSize(z, pixelRatio, projection); diff --git a/src/ol/source/Raster.js b/src/ol/source/Raster.js index ed44849ff8..9759b66dbb 100644 --- a/src/ol/source/Raster.js +++ b/src/ol/source/Raster.js @@ -444,7 +444,11 @@ function getImageData(layer, frameState, layerState) { } const width = frameState.size[0]; const height = frameState.size[1]; - const element = renderer.renderFrame(frameState, layerState); + const container = renderer.renderFrame(frameState, layerState, null); + let element; + if (container) { + element = container.firstElementChild; + } if (!(element instanceof HTMLCanvasElement)) { throw new Error('Unsupported rendered element: ' + element); } diff --git a/test/spec/ol/renderer/canvas/vectorlayer.test.js b/test/spec/ol/renderer/canvas/vectorlayer.test.js index ea591ddc9c..b2ea559139 100644 --- a/test/spec/ol/renderer/canvas/vectorlayer.test.js +++ b/test/spec/ol/renderer/canvas/vectorlayer.test.js @@ -307,7 +307,7 @@ describe('ol.renderer.canvas.VectorLayer', function() { let rendered = false; if (renderer.prepareFrame(frameState, {})) { rendered = true; - renderer.renderFrame(frameState, layer.getLayerState()); + renderer.renderFrame(frameState, layer.getLayerState(), null); } expect(rendered).to.be(true); }); diff --git a/test/spec/ol/renderer/canvas/vectortilelayer.test.js b/test/spec/ol/renderer/canvas/vectortilelayer.test.js index af8d06bc17..9efe8fe0d1 100644 --- a/test/spec/ol/renderer/canvas/vectortilelayer.test.js +++ b/test/spec/ol/renderer/canvas/vectortilelayer.test.js @@ -250,10 +250,10 @@ describe('ol.renderer.canvas.VectorTileLayer', function() { renderer.renderFrame(frameState, {}); const replayState = renderer.renderedTiles[0].getReplayState(layer); const revision = replayState.renderedTileRevision; - renderer.renderFrame(frameState, {}); + renderer.renderFrame(frameState, {}, null); expect(replayState.renderedTileRevision).to.be(revision); layer.changed(); - renderer.renderFrame(frameState, {}); + renderer.renderFrame(frameState, {}, null); expect(replayState.renderedTileRevision).to.be(revision + 1); expect(Object.keys(renderer.tileListenerKeys_).length).to.be(0); });