diff --git a/src/ol/renderer/canvas/VectorTileLayer.js b/src/ol/renderer/canvas/VectorTileLayer.js index 62c777038f..2e1a4afed8 100644 --- a/src/ol/renderer/canvas/VectorTileLayer.js +++ b/src/ol/renderer/canvas/VectorTileLayer.js @@ -2,6 +2,7 @@ * @module ol/renderer/canvas/VectorTileLayer */ import {getUid} from '../../util.js'; +import {createCanvasContext2D} from '../../dom.js'; import TileState from '../../TileState.js'; import ViewHint from '../../ViewHint.js'; import {listen, unlisten} from '../../events.js'; @@ -12,7 +13,7 @@ import VectorTileRenderType from '../../layer/VectorTileRenderType.js'; import {equivalent as equivalentProjection} from '../../proj.js'; import Units from '../../proj/Units.js'; import ReplayType from '../../render/ReplayType.js'; -import {labelCache, rotateAtOffset} from '../../render/canvas.js'; +import {labelCache} from '../../render/canvas.js'; import CanvasBuilderGroup from '../../render/canvas/BuilderGroup.js'; import {ORDER} from '../../render/replay.js'; import CanvasTileLayerRenderer from './TileLayer.js'; @@ -60,6 +61,33 @@ 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'; + + const container = document.createElement('div'); + const style = container.style; + style.position = 'absolute'; + style.display = 'flex'; + style.alignItems = 'center'; + style.justifyContent = 'center'; + + container.appendChild(baseCanvas); + container.appendChild(overlayCanvas); + + /** + * @private + * @type {HTMLElement} + */ + this.container_ = container; + /** * Declutter tree. * @private @@ -224,9 +252,9 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { render.call(this, feature); } } - const replayGroupInstructions = builderGroup.finish(); + const executorGroupInstructions = builderGroup.finish(); const renderingReplayGroup = new CanvasExecutorGroup(0, sharedExtent, resolution, - pixelRatio, source.getOverlaps(), this.declutterTree_, replayGroupInstructions, layer.getRenderBuffer()); + pixelRatio, source.getOverlaps(), this.declutterTree_, executorGroupInstructions, layer.getRenderBuffer()); sourceTile.setExecutorGroup(layer, tile.tileCoord.toString(), renderingReplayGroup); } builderState.renderedRevision = revision; @@ -329,101 +357,125 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { /** * @inheritDoc */ - postCompose(context, frameState, layerState) { + renderFrame(frameState, layerState) { + super.renderFrame(frameState, layerState); + const layer = /** @type {import("../../layer/VectorTile.js").default} */ (this.getLayer()); const renderMode = layer.getRenderMode(); - if (renderMode != VectorTileRenderType.IMAGE) { - const declutterReplays = layer.getDeclutter() ? {} : null; - const source = /** @type {import("../../source/VectorTile.js").default} */ (layer.getSource()); - const replayTypes = VECTOR_REPLAYS[renderMode]; - const pixelRatio = frameState.pixelRatio; - const rotation = frameState.viewState.rotation; - const size = frameState.size; - let offsetX, offsetY; - if (rotation) { - offsetX = Math.round(pixelRatio * size[0] / 2); - offsetY = Math.round(pixelRatio * size[1] / 2); - rotateAtOffset(context, -rotation, offsetX, offsetY); + if (renderMode === VectorTileRenderType.IMAGE) { + return this.container_; + } + + const context = this.overlayContext_; + const declutterReplays = layer.getDeclutter() ? {} : null; + const source = /** @type {import("../../source/VectorTile.js").default} */ (layer.getSource()); + const replayTypes = VECTOR_REPLAYS[renderMode]; + const pixelRatio = frameState.pixelRatio; + const rotation = frameState.viewState.rotation; + const size = frameState.size; + + // resize and clear + const canvas = context.canvas; + let width = Math.round(size[0] * pixelRatio); + let height = Math.round(size[1] * pixelRatio); + if (rotation) { + const size = Math.round(Math.sqrt(width * width + height * height)); + width = height = size; + } + if (canvas.width != width || canvas.height != height) { + canvas.width = width; + canvas.height = height; + canvas.style.width = (width / pixelRatio) + 'px'; + canvas.style.height = (height / pixelRatio) + 'px'; + } else { + context.clearRect(0, 0, width, height); + } + + if (declutterReplays) { + this.declutterTree_.clear(); + } + const viewHints = frameState.viewHints; + const snapToPixel = !(viewHints[ViewHint.ANIMATING] || viewHints[ViewHint.INTERACTING]); + const tiles = this.renderedTiles; + const tileGrid = source.getTileGridForProjection(frameState.viewState.projection); + const clips = []; + const zs = []; + for (let i = tiles.length - 1; i >= 0; --i) { + const tile = /** @type {import("../../VectorImageTile.js").default} */ (tiles[i]); + if (tile.getState() == TileState.ABORT) { + continue; } - if (declutterReplays) { - this.declutterTree_.clear(); - } - const viewHints = frameState.viewHints; - const snapToPixel = !(viewHints[ViewHint.ANIMATING] || viewHints[ViewHint.INTERACTING]); - const tiles = this.renderedTiles; - const tileGrid = source.getTileGridForProjection(frameState.viewState.projection); - const clips = []; - const zs = []; - for (let i = tiles.length - 1; i >= 0; --i) { - const tile = /** @type {import("../../VectorImageTile.js").default} */ (tiles[i]); - if (tile.getState() == TileState.ABORT) { + const tileCoord = tile.tileCoord; + const worldOffset = tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent)[0] - tile.extent[0]; + let transform = undefined; + for (let t = 0, tt = tile.tileKeys.length; t < tt; ++t) { + const sourceTile = tile.getTile(tile.tileKeys[t]); + if (sourceTile.getState() != TileState.LOADED) { continue; } - const tileCoord = tile.tileCoord; - const worldOffset = tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent)[0] - tile.extent[0]; - let transform = undefined; - for (let t = 0, tt = tile.tileKeys.length; t < tt; ++t) { - const sourceTile = tile.getTile(tile.tileKeys[t]); - if (sourceTile.getState() != TileState.LOADED) { - continue; - } - const executorGroup = /** @type {CanvasExecutorGroup} */ (sourceTile.getExecutorGroup(layer, tileCoord.toString())); - if (!executorGroup || !executorGroup.hasExecutors(replayTypes)) { - // sourceTile was not yet loaded when this.createReplayGroup_() was - // called, or it has no replays of the types we want to render - continue; - } - if (!transform) { - transform = this.getTransform(frameState, worldOffset); - } - const currentZ = sourceTile.tileCoord[0]; - const currentClip = executorGroup.getClipCoords(transform); - context.save(); - context.globalAlpha = layerState.opacity; - // 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 < zs[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(); - } - } - executorGroup.execute(context, transform, rotation, {}, snapToPixel, replayTypes, declutterReplays); - context.restore(); - clips.push(currentClip); - zs.push(currentZ); + const executorGroup = /** @type {CanvasExecutorGroup} */ (sourceTile.getExecutorGroup(layer, tileCoord.toString())); + if (!executorGroup || !executorGroup.hasExecutors(replayTypes)) { + // sourceTile was not yet loaded when this.createReplayGroup_() was + // called, or it has no replays of the types we want to render + continue; } - } - if (declutterReplays) { - replayDeclutter(declutterReplays, context, rotation, snapToPixel); - } - if (rotation) { - rotateAtOffset(context, rotation, - /** @type {number} */ (offsetX), /** @type {number} */ (offsetY)); + if (!transform) { + transform = this.getTransform(frameState, worldOffset); + } + const currentZ = sourceTile.tileCoord[0]; + const currentClip = executorGroup.getClipCoords(transform); + 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 < zs[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(); + } + } + executorGroup.execute(context, transform, rotation, {}, snapToPixel, replayTypes, declutterReplays); + context.restore(); + clips.push(currentClip); + zs.push(currentZ); } } - super.postCompose(context, frameState, layerState); + if (declutterReplays) { + replayDeclutter(declutterReplays, context, rotation, snapToPixel); + } + + const opacity = layerState.opacity; + if (opacity !== canvas.style.opacity) { + canvas.style.opacity = opacity; + } + + const canvasTransform = 'rotate(' + rotation + 'rad)'; + if (canvasTransform !== canvas.style.transform) { + canvas.style.transform = canvasTransform; + } + + return this.container_; } /** * @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} replayGroup Replay group. + * @param {import("../../render/canvas/BuilderGroup.js").default} executorGroup Replay group. * @return {boolean} `true` if an image is loading. */ - renderFeature(feature, squaredTolerance, styles, replayGroup) { + renderFeature(feature, squaredTolerance, styles, executorGroup) { if (!styles) { return false; } @@ -431,12 +483,12 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { if (Array.isArray(styles)) { for (let i = 0, ii = styles.length; i < ii; ++i) { loading = renderFeature( - replayGroup, feature, styles[i], squaredTolerance, + executorGroup, feature, styles[i], squaredTolerance, this.handleStyleImageChange_, this) || loading; } } else { loading = renderFeature( - replayGroup, feature, styles, squaredTolerance, + executorGroup, feature, styles, squaredTolerance, this.handleStyleImageChange_, this); } return loading;