Skip to content

Commit

Permalink
Conditionally render tiles to a separate tile canvas
Browse files Browse the repository at this point in the history
Because clip geometries are anti-aliased in most browsers, there will be tiny
gaps between tiles. If tiles are rendered to a tile canvas which is then drawn
to the map canvas upon composition, these gaps can be avoided. For rotated
views, it is stil necessary to clip the tile, but in this case a 1-pixel
buffer is used.

This change also brings a huge performance improvement for panning, because
the fully rendered tiles can be reused.

Because of the added cost of using drawImage in addition to replaying the tile
replay group, we fall back to directly drawing to the map canvas when the tile
canvas would be too large, or during interaction/animation when resolution or
rotation change.
  • Loading branch information
ahocevar committed Dec 16, 2015
1 parent 6bc6fd9 commit c1b1621
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 44 deletions.
43 changes: 23 additions & 20 deletions src/ol/render/canvas/canvasreplay.js
Expand Up @@ -1995,32 +1995,35 @@ ol.render.canvas.ReplayGroup.prototype.isEmpty = function() {
* @param {number} viewRotation View rotation.
* @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
* to skip.
* @param {boolean=} opt_clip Clip at `maxExtent`. Default is true.
*/
ol.render.canvas.ReplayGroup.prototype.replay = function(
context, pixelRatio, transform, viewRotation, skippedFeaturesHash) {
ol.render.canvas.ReplayGroup.prototype.replay = function(context, pixelRatio,
transform, viewRotation, skippedFeaturesHash, opt_clip) {

/** @type {Array.<number>} */
var zs = Object.keys(this.replaysByZIndex_).map(Number);
zs.sort(ol.array.numberSafeCompareFunction);

// setup clipping so that the parts of over-simplified geometries are not
// visible outside the current extent when panning
var maxExtent = this.maxExtent_;
var minX = maxExtent[0];
var minY = maxExtent[1];
var maxX = maxExtent[2];
var maxY = maxExtent[3];
var flatClipCoords = [minX, minY, minX, maxY, maxX, maxY, maxX, minY];
ol.geom.flat.transform.transform2D(
flatClipCoords, 0, 8, 2, transform, flatClipCoords);
context.save();
context.beginPath();
context.moveTo(flatClipCoords[0], flatClipCoords[1]);
context.lineTo(flatClipCoords[2], flatClipCoords[3]);
context.lineTo(flatClipCoords[4], flatClipCoords[5]);
context.lineTo(flatClipCoords[6], flatClipCoords[7]);
context.closePath();
context.clip();
if (opt_clip !== false) {
// setup clipping so that the parts of over-simplified geometries are not
// visible outside the current extent when panning
var maxExtent = this.maxExtent_;
var minX = maxExtent[0];
var minY = maxExtent[1];
var maxX = maxExtent[2];
var maxY = maxExtent[3];
var flatClipCoords = [minX, minY, minX, maxY, maxX, maxY, maxX, minY];
ol.geom.flat.transform.transform2D(
flatClipCoords, 0, 8, 2, transform, flatClipCoords);
context.save();
context.beginPath();
context.moveTo(flatClipCoords[0], flatClipCoords[1]);
context.lineTo(flatClipCoords[2], flatClipCoords[3]);
context.lineTo(flatClipCoords[4], flatClipCoords[5]);
context.lineTo(flatClipCoords[6], flatClipCoords[7]);
context.closePath();
context.clip();
}

var i, ii, j, jj, replays, replay;
for (i = 0, ii = zs.length; i < ii; ++i) {
Expand Down
122 changes: 98 additions & 24 deletions src/ol/renderer/canvas/canvasvectortilelayerrenderer.js
Expand Up @@ -11,6 +11,7 @@ goog.require('ol.ViewHint');
goog.require('ol.array');
goog.require('ol.dom');
goog.require('ol.extent');
goog.require('ol.geom.flat.transform');
goog.require('ol.layer.VectorTile');
goog.require('ol.proj.Units');
goog.require('ol.render.EventType');
Expand Down Expand Up @@ -51,6 +52,18 @@ ol.renderer.canvas.VectorTileLayer = function(layer) {
*/
this.renderedTiles_ = [];

/**
* @private
* @type {number}
*/
this.resolution_ = NaN;

/**
* @private
* @type {number}
*/
this.rotation_ = NaN;

/**
* @private
* @type {ol.Extent}
Expand Down Expand Up @@ -110,44 +123,101 @@ ol.renderer.canvas.VectorTileLayer.prototype.composeFrame =
// see http://jsperf.com/context-save-restore-versus-variable
var alpha = replayContext.globalAlpha;
replayContext.globalAlpha = layerState.opacity;
var imageSmoothingEnabled = replayContext.imageSmoothingEnabled;
replayContext.imageSmoothingEnabled = false;


var tilesToDraw = this.renderedTiles_;
var tileGrid = source.getTileGrid();

var currentZ, i, ii, origin, tile, tileSize;
var tilePixelRatio, tilePixelResolution, tilePixelSize, tileResolution;
var currentZ, height, i, ii, insertPoint, insertTransform, origin, pixelScale;
var pixelSpace, replayState, rotatedTileExtent, rotatedTileSize, size, tile;
var tileCenter, tileContext, tileExtent, tilePixelResolution, tilePixelSize;
var tileResolution, tileSize, tileTransform, width;
for (i = 0, ii = tilesToDraw.length; i < ii; ++i) {
tile = tilesToDraw[i];
replayState = tile.getReplayState();
tileExtent = tileGrid.getTileCoordExtent(
tile.getTileCoord(), this.tmpExtent_);
currentZ = tile.getTileCoord()[0];
tileSize = tileGrid.getTileSize(currentZ);
tilePixelSize = source.getTilePixelSize(currentZ, pixelRatio, projection);
tilePixelRatio = tilePixelSize[0] /
ol.size.toSize(tileSize, this.tmpSize_)[0];
tileSize = ol.size.toSize(tileGrid.getTileSize(currentZ), this.tmpSize_);
pixelSpace = tile.getProjection().getUnits() == ol.proj.Units.TILE_PIXELS;

This comment has been minimized.

Copy link
@schmidtk

schmidtk Sep 2, 2016

Contributor

@ahocevar - this.renderedTiles is declared as Array<ol.Tile>, causing a compiler error when checkTypes is enabled. This class should use generics, an override, or a @type {ol.VectorTile} cast to instruct the compiler of the correct type.

This comment has been minimized.

Copy link
@ahocevar

ahocevar Sep 2, 2016

Author Member

@schmidtk A @type {ol.VectorTile} cast would be in line with what we've done elsewhere. Would you be able to provide a quick pull request?

This comment has been minimized.

Copy link
@ahocevar

ahocevar Sep 2, 2016

Author Member

Nevermind @schmidtk, I'm working on the library right now and can create the pull request.

size = frameState.size;
tileResolution = tileGrid.getResolution(currentZ);
tilePixelResolution = tileResolution / tilePixelRatio;
if (tile.getProjection().getUnits() == ol.proj.Units.TILE_PIXELS) {
origin = ol.extent.getTopLeft(tileGrid.getTileCoordExtent(
tile.getTileCoord(), this.tmpExtent_));
transform = ol.vec.Mat4.makeTransform2D(this.tmpTransform_,
pixelRatio * frameState.size[0] / 2,
pixelRatio * frameState.size[1] / 2,
pixelRatio * tilePixelResolution / resolution,
pixelRatio * tilePixelResolution / resolution,
viewState.rotation,
(origin[0] - center[0]) / tilePixelResolution,
(center[1] - origin[1]) / tilePixelResolution);
tilePixelResolution = tileResolution / source.getTilePixelRatio();
pixelScale = pixelRatio / resolution;
scale = tileResolution / resolution;
offsetX = Math.round(pixelRatio * size[0] / 2);
offsetY = Math.round(pixelRatio * size[1] / 2);
width = tileSize[0] * pixelRatio * scale;
height = tileSize[1] * pixelRatio * scale;
if (width < 1 || width > size[0]) {
if (pixelSpace) {
origin = ol.extent.getTopLeft(tileExtent);
tileTransform = ol.vec.Mat4.makeTransform2D(this.tmpTransform_,
pixelRatio * size[0] / 2, pixelRatio * size[1] / 2,
pixelScale * tilePixelResolution,
pixelScale * tilePixelResolution,
rotation,
(origin[0] - center[0]) / tilePixelResolution,
(center[1] - origin[1]) / tilePixelResolution);
} else {
tileTransform = transform;
}
replayState.replayGroup.replay(replayContext, pixelRatio,
tileTransform, rotation, skippedFeatureUids);
} else {
rotatedTileExtent = ol.extent.getForViewAndSize(
ol.extent.getCenter(tileExtent), tileResolution, rotation, tileSize);
rotatedTileSize = [ol.extent.getWidth(rotatedTileExtent),
ol.extent.getHeight(rotatedTileExtent)];
tilePixelSize = source.getTilePixelSize(currentZ, pixelRatio, projection);
if (pixelSpace) {
tileTransform = ol.vec.Mat4.makeTransform2D(this.tmpTransform_,
width / 2, height / 2,
pixelScale * tilePixelResolution, pixelScale * tilePixelResolution,
rotation,
-tilePixelSize[0] / 2, -tilePixelSize[1] / 2);
} else {
tileCenter = ol.extent.getCenter(rotatedTileExtent);
tileTransform = ol.vec.Mat4.makeTransform2D(this.tmpTransform_,
width / 2, height / 2,
pixelScale, -pixelScale,
-rotation,
-tileCenter[0], -tileCenter[1]);
}
tileContext = tile.getContext();
if (replayState.resolution !== resolution ||
replayState.rotation !== rotation) {
replayState.resolution = resolution;
replayState.rotation = rotation;
tileContext.canvas.width = width + 0.5;
tileContext.canvas.height = height + 0.5;
replayState.replayGroup.replay(tileContext, pixelRatio,
tileTransform, rotation, skippedFeatureUids, rotation !== 0);
}
insertTransform = ol.vec.Mat4.makeTransform2D(this.tmpTransform_,
(pixelRatio * size[0] - width) / 2,
(pixelRatio * size[1] - height) / 2,
pixelScale, -pixelScale,
-rotation,
-center[0], -center[1]);
insertPoint = ol.geom.flat.transform.transform2D(
ol.extent.getCenter(rotatedTileExtent), 0, 1, 2, insertTransform);
replayContext.drawImage(tileContext.canvas,
insertPoint[0], insertPoint[1]);
}
tile.getReplayState().replayGroup.replay(replayContext, pixelRatio,
transform, rotation, skippedFeatureUids);
}

transform = this.getTransform(frameState, 0);
this.resolution_ = resolution;
this.rotation_ = rotation;

if (replayContext != context) {
this.dispatchRenderEvent(replayContext, frameState, transform);
context.drawImage(replayContext.canvas, 0, 0);
}
replayContext.globalAlpha = alpha;
replayContext.imageSmoothingEnabled = imageSmoothingEnabled;

this.dispatchPostComposeEvent(context, frameState, transform);
};
Expand Down Expand Up @@ -179,16 +249,19 @@ ol.renderer.canvas.VectorTileLayer.prototype.createReplayGroup = function(tile,
'Source is an ol.source.VectorTile');
var tileGrid = source.getTileGrid();
var tileCoord = tile.getTileCoord();
var buffer = 1;
var resolution = tileGrid.getResolution(tileCoord[0]);
var pixelSpace = tile.getProjection().getUnits() == ol.proj.Units.TILE_PIXELS;
var extent;
if (pixelSpace) {
var tilePixelSize = source.getTilePixelSize(tileCoord[0], pixelRatio,
tile.getProjection());
extent = [0, 0, tilePixelSize[0], tilePixelSize[1]];
extent = [-buffer, -buffer,
tilePixelSize[0] + buffer, tilePixelSize[1] + buffer];
} else {
extent = tileGrid.getTileCoordExtent(tileCoord);
extent = ol.extent.buffer(tileGrid.getTileCoordExtent(tileCoord),
buffer * resolution);
}
var resolution = tileGrid.getResolution(tileCoord[0]);
var tileResolution = pixelSpace ? source.getTilePixelRatio() : resolution;
replayState.dirty = false;
var replayGroup = new ol.render.canvas.ReplayGroup(0, extent,
Expand Down Expand Up @@ -234,6 +307,7 @@ ol.renderer.canvas.VectorTileLayer.prototype.createReplayGroup = function(tile,
replayState.renderedRevision = revision;
replayState.renderedRenderOrder = renderOrder;
replayState.replayGroup = replayGroup;
replayState.resolution = NaN;
};


Expand Down
15 changes: 15 additions & 0 deletions src/ol/vectortile.js
Expand Up @@ -4,6 +4,7 @@ goog.require('ol.Tile');
goog.require('ol.TileCoord');
goog.require('ol.TileLoadFunctionType');
goog.require('ol.TileState');
goog.require('ol.dom');
goog.require('ol.proj.Projection');


Expand Down Expand Up @@ -33,6 +34,12 @@ ol.VectorTile =

goog.base(this, tileCoord, state);

/**
* @private
* @type {CanvasRenderingContext2D}
*/
this.context_ = ol.dom.createCanvasContext2D();

/**
* @private
* @type {ol.format.Feature}
Expand Down Expand Up @@ -84,6 +91,14 @@ ol.VectorTile =
goog.inherits(ol.VectorTile, ol.Tile);


/**
* @return {CanvasRenderingContext2D}
*/
ol.VectorTile.prototype.getContext = function() {
return this.context_;
};


/**
* @inheritDoc
*/
Expand Down

0 comments on commit c1b1621

Please sign in to comment.