Merge pull request #9584 from ahocevar/reuse-render-target

Reuse render target
This commit is contained in:
Andreas Hocevar
2019-05-24 10:33:36 +02:00
committed by GitHub
18 changed files with 290 additions and 163 deletions

View File

@@ -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>} declutterItems
* @property {import("./coordinate.js").Coordinate} focus
* @property {number} index
* @property {Array<import("./layer/Layer.js").State>} 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
*/

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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;
}
}
}

View File

@@ -430,10 +430,11 @@ export function getCircleArray(radius) {
* @param {!Object<string, Array<*>>} 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<Array<*>>} declutterItems Declutter items.
* @param {Array<import("../../PluggableMap.js").DeclutterItems>} 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);

View File

@@ -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);

View File

@@ -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();
}

View File

@@ -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;
}

View File

@@ -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;
}
}
/**

View File

@@ -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<number>} */
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();

View File

@@ -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();

View File

@@ -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;
}
/**

View File

@@ -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);

View File

@@ -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);
}