370 lines
12 KiB
JavaScript
370 lines
12 KiB
JavaScript
/**
|
|
* @module ol/renderer/canvas/TileLayer
|
|
*/
|
|
import {getUid} from '../../util.js';
|
|
import TileRange from '../../TileRange.js';
|
|
import TileState from '../../TileState.js';
|
|
import {createEmpty, getIntersection, getTopLeft} from '../../extent.js';
|
|
import CanvasLayerRenderer from './Layer.js';
|
|
import {create as createTransform, compose as composeTransform, toString as transformToString} from '../../transform.js';
|
|
|
|
/**
|
|
* @classdesc
|
|
* Canvas renderer for tile layers.
|
|
* @api
|
|
*/
|
|
class CanvasTileLayerRenderer extends CanvasLayerRenderer {
|
|
|
|
/**
|
|
* @param {import("../../layer/Tile.js").default|import("../../layer/VectorTile.js").default} tileLayer Tile layer.
|
|
*/
|
|
constructor(tileLayer) {
|
|
super(tileLayer);
|
|
|
|
/**
|
|
* @protected
|
|
* @type {import("../../transform.js").Transform}
|
|
*/
|
|
this.coordinateToCanvasPixelTransform = createTransform();
|
|
|
|
/**
|
|
* @private
|
|
* @type {number}
|
|
*/
|
|
this.oversampling_;
|
|
|
|
/**
|
|
* @private
|
|
* @type {import("../../extent.js").Extent}
|
|
*/
|
|
this.renderedExtent_ = null;
|
|
|
|
/**
|
|
* @protected
|
|
* @type {number}
|
|
*/
|
|
this.renderedRevision;
|
|
|
|
/**
|
|
* @protected
|
|
* @type {!Array<import("../../Tile.js").default>}
|
|
*/
|
|
this.renderedTiles = [];
|
|
|
|
/**
|
|
* @private
|
|
* @type {boolean}
|
|
*/
|
|
this.newTiles_ = false;
|
|
|
|
/**
|
|
* @protected
|
|
* @type {import("../../extent.js").Extent}
|
|
*/
|
|
this.tmpExtent = createEmpty();
|
|
|
|
/**
|
|
* @private
|
|
* @type {import("../../TileRange.js").default}
|
|
*/
|
|
this.tmpTileRange_ = new TileRange(0, 0, 0, 0);
|
|
|
|
/**
|
|
* @protected
|
|
* @type {number}
|
|
*/
|
|
this.zDirection = 0;
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @param {import("../../Tile.js").default} tile Tile.
|
|
* @return {boolean} Tile is drawable.
|
|
*/
|
|
isDrawableTile_(tile) {
|
|
const tileLayer = /** @type {import("../../layer/Tile.js").default} */ (this.getLayer());
|
|
const tileState = tile.getState();
|
|
const useInterimTilesOnError = tileLayer.getUseInterimTilesOnError();
|
|
return tileState == TileState.LOADED ||
|
|
tileState == TileState.EMPTY ||
|
|
tileState == TileState.ERROR && !useInterimTilesOnError;
|
|
}
|
|
|
|
/**
|
|
* @param {number} z Tile coordinate z.
|
|
* @param {number} x Tile coordinate x.
|
|
* @param {number} y Tile coordinate y.
|
|
* @param {number} pixelRatio Pixel ratio.
|
|
* @param {import("../../proj/Projection.js").default} projection Projection.
|
|
* @return {!import("../../Tile.js").default} Tile.
|
|
*/
|
|
getTile(z, x, y, pixelRatio, projection) {
|
|
const tileLayer = /** @type {import("../../layer/Tile.js").default} */ (this.getLayer());
|
|
const tileSource = /** @type {import("../../source/Tile.js").default} */ (tileLayer.getSource());
|
|
let tile = tileSource.getTile(z, x, y, pixelRatio, projection);
|
|
if (tile.getState() == TileState.ERROR) {
|
|
if (!tileLayer.getUseInterimTilesOnError()) {
|
|
// When useInterimTilesOnError is false, we consider the error tile as loaded.
|
|
tile.setState(TileState.LOADED);
|
|
} else if (tileLayer.getPreload() > 0) {
|
|
// Preloaded tiles for lower resolutions might have finished loading.
|
|
this.newTiles_ = true;
|
|
}
|
|
}
|
|
if (!this.isDrawableTile_(tile)) {
|
|
tile = tile.getInterimTile();
|
|
}
|
|
return tile;
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
prepareFrame(frameState, layerState) {
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
renderFrame(frameState, layerState) {
|
|
const context = this.context;
|
|
const viewState = frameState.viewState;
|
|
const projection = viewState.projection;
|
|
const viewResolution = viewState.resolution;
|
|
const viewCenter = viewState.center;
|
|
const rotation = viewState.rotation;
|
|
const pixelRatio = frameState.pixelRatio;
|
|
|
|
const tileLayer = /** @type {import("../../layer/Tile.js").default} */ (this.getLayer());
|
|
const tileSource = /** @type {import("../../source/Tile.js").default} */ (tileLayer.getSource());
|
|
const sourceRevision = tileSource.getRevision();
|
|
const tileGrid = tileSource.getTileGridForProjection(projection);
|
|
const z = tileGrid.getZForResolution(viewResolution, this.zDirection);
|
|
const tileResolution = tileGrid.getResolution(z);
|
|
let extent = frameState.extent;
|
|
|
|
if (layerState.extent !== undefined) {
|
|
extent = getIntersection(extent, layerState.extent);
|
|
}
|
|
|
|
// TODO: clip by layer extent
|
|
|
|
const tilePixelRatio = tileSource.getTilePixelRatio(pixelRatio);
|
|
|
|
// desired dimensions of the canvas in pixels
|
|
let width = Math.round(frameState.size[0] * tilePixelRatio);
|
|
let height = Math.round(frameState.size[1] * tilePixelRatio);
|
|
if (tileResolution < viewResolution) {
|
|
// scale canvas so it covers the viewport until new tiles come it
|
|
width *= 1.5;
|
|
height *= 1.5;
|
|
}
|
|
|
|
if (rotation) {
|
|
const size = Math.round(Math.sqrt(width * width + height * height));
|
|
width = height = size;
|
|
}
|
|
|
|
const dx = tileResolution * width / 2 / tilePixelRatio;
|
|
const dy = tileResolution * height / 2 / tilePixelRatio;
|
|
const canvasExtent = [
|
|
viewCenter[0] - dx,
|
|
viewCenter[1] - dy,
|
|
viewCenter[0] + dx,
|
|
viewCenter[1] + dy
|
|
];
|
|
|
|
const tileRange = tileGrid.getTileRangeForExtentAndZ(canvasExtent, z);
|
|
|
|
/**
|
|
* @type {Object<number, Object<string, import("../../Tile.js").default>>}
|
|
*/
|
|
const tilesToDrawByZ = {};
|
|
tilesToDrawByZ[z] = {};
|
|
|
|
const findLoadedTiles = this.createLoadedTileFinder(tileSource, projection, tilesToDrawByZ);
|
|
|
|
const tmpExtent = this.tmpExtent;
|
|
const tmpTileRange = this.tmpTileRange_;
|
|
this.newTiles_ = false;
|
|
for (let x = tileRange.minX; x <= tileRange.maxX; ++x) {
|
|
for (let y = tileRange.minY; y <= tileRange.maxY; ++y) {
|
|
const tile = this.getTile(z, x, y, pixelRatio, projection);
|
|
if (this.isDrawableTile_(tile)) {
|
|
const uid = getUid(this);
|
|
if (tile.getState() == TileState.LOADED) {
|
|
tilesToDrawByZ[z][tile.tileCoord.toString()] = tile;
|
|
const inTransition = tile.inTransition(uid);
|
|
if (!this.newTiles_ && (inTransition || this.renderedTiles.indexOf(tile) === -1)) {
|
|
this.newTiles_ = true;
|
|
}
|
|
}
|
|
if (tile.getAlpha(uid, frameState.time) === 1) {
|
|
// don't look for alt tiles if alpha is 1
|
|
continue;
|
|
}
|
|
}
|
|
|
|
const childTileRange = tileGrid.getTileCoordChildTileRange(tile.tileCoord, tmpTileRange, tmpExtent);
|
|
|
|
let covered = false;
|
|
if (childTileRange) {
|
|
covered = findLoadedTiles(z + 1, childTileRange);
|
|
}
|
|
if (!covered) {
|
|
tileGrid.forEachTileCoordParentTileRange(tile.tileCoord, findLoadedTiles, null, tmpTileRange, tmpExtent);
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
const canvas = context.canvas;
|
|
const canvasScale = tileResolution / frameState.viewState.resolution / tilePixelRatio;
|
|
const pixelTransform = composeTransform(this.transform_,
|
|
0, 0,
|
|
canvasScale, canvasScale,
|
|
rotation,
|
|
0, 0
|
|
);
|
|
const canvasTransform = transformToString(pixelTransform);
|
|
|
|
if (canvas.width != width || canvas.height != height) {
|
|
canvas.width = width;
|
|
canvas.height = height;
|
|
} else {
|
|
context.clearRect(0, 0, width, height);
|
|
}
|
|
|
|
this.preRender(context, frameState, pixelTransform);
|
|
|
|
this.renderedTiles.length = 0;
|
|
/** @type {Array<number>} */
|
|
const zs = Object.keys(tilesToDrawByZ).map(Number);
|
|
zs.sort(function(a, b) {
|
|
if (a === z) {
|
|
return 1;
|
|
} else if (b === z) {
|
|
return -1;
|
|
} else {
|
|
return a > b ? 1 : a < b ? -1 : 0;
|
|
}
|
|
});
|
|
|
|
for (let i = 0, ii = zs.length; i < ii; ++i) {
|
|
const currentZ = zs[i];
|
|
const currentTilePixelSize = tileSource.getTilePixelSize(currentZ, pixelRatio, projection);
|
|
const currentResolution = tileGrid.getResolution(currentZ);
|
|
const currentScale = currentResolution / tileResolution;
|
|
const w = currentTilePixelSize[0] * currentScale;
|
|
const h = currentTilePixelSize[1] * currentScale;
|
|
const originTileCoord = tileGrid.getTileCoordForCoordAndZ(getTopLeft(canvasExtent), currentZ);
|
|
const originTileExtent = tileGrid.getTileCoordExtent(originTileCoord);
|
|
const originX = Math.round(tilePixelRatio * (originTileExtent[0] - canvasExtent[0]) / tileResolution);
|
|
const originY = Math.round(tilePixelRatio * (canvasExtent[3] - originTileExtent[3]) / tileResolution);
|
|
const tileGutter = tilePixelRatio * tileSource.getGutterForProjection(projection);
|
|
const tilesToDraw = tilesToDrawByZ[currentZ];
|
|
for (const tileCoordKey in tilesToDraw) {
|
|
const tile = tilesToDraw[tileCoordKey];
|
|
const tileCoord = tile.tileCoord;
|
|
const x = originX - (originTileCoord[1] - tileCoord[1]) * w;
|
|
const y = originY + (originTileCoord[2] - tileCoord[2]) * h;
|
|
this.drawTileImage(tile, frameState, layerState, x, y, w, h, tileGutter, z === currentZ);
|
|
this.renderedTiles.push(tile);
|
|
}
|
|
}
|
|
|
|
|
|
this.renderedRevision = sourceRevision;
|
|
this.renderedResolution = tileResolution;
|
|
this.renderedExtent_ = canvasExtent;
|
|
|
|
this.updateUsedTiles(frameState.usedTiles, tileSource, z, tileRange);
|
|
this.manageTilePyramid(frameState, tileSource, tileGrid, pixelRatio,
|
|
projection, extent, z, tileLayer.getPreload());
|
|
this.scheduleExpireCache(frameState, tileSource);
|
|
|
|
this.postRender(context, frameState, pixelTransform);
|
|
|
|
const opacity = layerState.opacity;
|
|
if (opacity !== canvas.style.opacity) {
|
|
canvas.style.opacity = opacity;
|
|
}
|
|
|
|
if (canvasTransform !== canvas.style.transform) {
|
|
canvas.style.transform = canvasTransform;
|
|
}
|
|
|
|
return canvas;
|
|
}
|
|
|
|
/**
|
|
* @param {import("../../Tile.js").default} tile Tile.
|
|
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
|
|
* @param {import("../../layer/Layer.js").State} layerState Layer state.
|
|
* @param {number} x Left of the tile.
|
|
* @param {number} y Top of the tile.
|
|
* @param {number} w Width of the tile.
|
|
* @param {number} h Height of the tile.
|
|
* @param {number} gutter Tile gutter.
|
|
* @param {boolean} transition Apply an alpha transition.
|
|
*/
|
|
drawTileImage(tile, frameState, layerState, x, y, w, h, gutter, transition) {
|
|
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 = /** @type {import("../../source/Tile.js").default} */ (tileLayer.getSource());
|
|
if (alpha === 1 && !tileSource.getOpaque(frameState.viewState.projection)) {
|
|
this.context.clearRect(x, y, w, h);
|
|
}
|
|
const alphaChanged = alpha !== this.context.globalAlpha;
|
|
if (alphaChanged) {
|
|
this.context.save();
|
|
this.context.globalAlpha = alpha;
|
|
}
|
|
this.context.drawImage(image, gutter, gutter,
|
|
image.width - 2 * gutter, image.height - 2 * gutter, x, y, w, h);
|
|
|
|
if (alphaChanged) {
|
|
this.context.restore();
|
|
}
|
|
if (alpha !== 1) {
|
|
frameState.animate = true;
|
|
} else if (transition) {
|
|
tile.endTransition(uid);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
getImage() {
|
|
const context = this.context;
|
|
return context ? context.canvas : null;
|
|
}
|
|
|
|
/**
|
|
* Get the image from a tile.
|
|
* @param {import("../../Tile.js").default} tile Tile.
|
|
* @return {HTMLCanvasElement|HTMLImageElement|HTMLVideoElement} Image.
|
|
* @protected
|
|
*/
|
|
getTileImage(tile) {
|
|
return /** @type {import("../../ImageTile.js").default} */ (tile).getImage();
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @function
|
|
* @return {import("../../layer/Tile.js").default|import("../../layer/VectorTile.js").default}
|
|
*/
|
|
CanvasTileLayerRenderer.prototype.getLayer;
|
|
|
|
|
|
export default CanvasTileLayerRenderer;
|