Merge pull request #9584 from ahocevar/reuse-render-target
Reuse render target
This commit is contained in:
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user