diff --git a/src/ol/ol.js b/src/ol/ol.js index 23c754f49a..67f3cc5ddc 100644 --- a/src/ol/ol.js +++ b/src/ol/ol.js @@ -124,6 +124,14 @@ ol.ENABLE_TILE = true; ol.ENABLE_VECTOR = true; +/** + * @define {boolean} Enable rendering of ol.layer.VectorTile based layers. + * Default is `true`. Setting this to false at compile time in advanced mode + * removes all code supporting VectorTile layers from the build. + */ +ol.ENABLE_VECTOR_TILE = true; + + /** * @define {boolean} Enable the WebGL renderer. Default is `true`. Setting * this to false at compile time in advanced mode removes all code diff --git a/src/ol/render/canvas/canvasreplay.js b/src/ol/render/canvas/canvasreplay.js index 3c3f091b85..0e2d7189a9 100644 --- a/src/ol/render/canvas/canvasreplay.js +++ b/src/ol/render/canvas/canvasreplay.js @@ -1158,6 +1158,8 @@ ol.render.canvas.PolygonReplay = function(tolerance, maxExtent, resolution) { miterLimit: undefined }; + this.rightHanded_ = false; + }; goog.inherits(ol.render.canvas.PolygonReplay, ol.render.canvas.Replay); @@ -1290,7 +1292,9 @@ ol.render.canvas.PolygonReplay.prototype.drawPolygonGeometry = state.miterLimit, state.lineDash]); } var ends = polygonGeometry.getEnds(); - var flatCoordinates = polygonGeometry.getOrientedFlatCoordinates(); + var flatCoordinates = this.rightHanded_ ? + polygonGeometry.getFlatCoordinates() : + polygonGeometry.getOrientedFlatCoordinates(); var stride = polygonGeometry.getStride(); this.drawFlatCoordinatess_(flatCoordinates, 0, ends, stride); this.endGeometry(polygonGeometry, feature); @@ -1375,6 +1379,16 @@ ol.render.canvas.PolygonReplay.prototype.getBufferedMaxExtent = function() { }; +/** + * @param {boolean} rightHanded Whether polygons are assumed to follow + * the right-hand rule. + */ +ol.render.canvas.PolygonReplay.prototype.setRightHanded = + function(rightHanded) { + this.rightHanded_ = rightHanded; +}; + + /** * @inheritDoc */ @@ -1793,10 +1807,13 @@ ol.render.canvas.TextReplay.prototype.setTextStyle = function(textStyle) { * @param {ol.Extent} maxExtent Max extent. * @param {number} resolution Resolution. * @param {number=} opt_renderBuffer Optional rendering buffer. + * @param {boolean=} opt_rightHandedPolygons Assume that polygons follow the + * right-hand rule. * @struct */ ol.render.canvas.ReplayGroup = function( - tolerance, maxExtent, resolution, opt_renderBuffer) { + tolerance, maxExtent, resolution, opt_renderBuffer, + opt_rightHandedPolygons) { /** * @private @@ -1841,6 +1858,13 @@ ol.render.canvas.ReplayGroup = function( */ this.hitDetectionTransform_ = goog.vec.Mat4.createNumber(); + /** + * @private + * @type {boolean} + */ + this.rightHandedPolygons_ = goog.isDef(opt_rightHandedPolygons) ? + opt_rightHandedPolygons : false; + }; @@ -1928,6 +1952,10 @@ ol.render.canvas.ReplayGroup.prototype.getReplay = ' constructor missing from ol.render.canvas.BATCH_CONSTRUCTORS_'); replay = new Constructor(this.tolerance_, this.maxExtent_, this.resolution_); + if (replayType == ol.render.ReplayType.POLYGON) { + goog.asserts.assertInstanceof(replay, ol.render.canvas.PolygonReplay); + replay.setRightHanded(this.rightHandedPolygons_); + } replays[replayType] = replay; } return replay; diff --git a/src/ol/render/vector.js b/src/ol/render/vector.js index 30b4d4c32e..5c91a013d9 100644 --- a/src/ol/render/vector.js +++ b/src/ol/render/vector.js @@ -271,7 +271,8 @@ ol.renderer.vector.renderPointGeometry_ = var textReplay = replayGroup.getReplay( style.getZIndex(), ol.render.ReplayType.TEXT); textReplay.setTextStyle(textStyle); - textReplay.drawText(geometry.getCoordinates(), 0, 2, 2, geometry, feature); + textReplay.drawText(geometry.getFlatCoordinates(), 0, 2, 2, geometry, + feature); } }; diff --git a/src/ol/renderer/canvas/canvasmaprenderer.js b/src/ol/renderer/canvas/canvasmaprenderer.js index 21c9f88285..447bb44bd8 100644 --- a/src/ol/renderer/canvas/canvasmaprenderer.js +++ b/src/ol/renderer/canvas/canvasmaprenderer.js @@ -15,6 +15,7 @@ goog.require('ol.layer.Image'); goog.require('ol.layer.Layer'); goog.require('ol.layer.Tile'); goog.require('ol.layer.Vector'); +goog.require('ol.layer.VectorTile'); goog.require('ol.render.Event'); goog.require('ol.render.EventType'); goog.require('ol.render.canvas.Immediate'); @@ -23,6 +24,7 @@ goog.require('ol.renderer.canvas.ImageLayer'); goog.require('ol.renderer.canvas.Layer'); goog.require('ol.renderer.canvas.TileLayer'); goog.require('ol.renderer.canvas.VectorLayer'); +goog.require('ol.renderer.canvas.VectorTileLayer'); goog.require('ol.source.State'); goog.require('ol.vec.Mat4'); @@ -79,6 +81,8 @@ ol.renderer.canvas.Map.prototype.createLayerRenderer = function(layer) { return new ol.renderer.canvas.ImageLayer(layer); } else if (ol.ENABLE_TILE && layer instanceof ol.layer.Tile) { return new ol.renderer.canvas.TileLayer(layer); + } else if (ol.ENABLE_VECTOR_TILE && layer instanceof ol.layer.VectorTile) { + return new ol.renderer.canvas.VectorTileLayer(layer); } else if (ol.ENABLE_VECTOR && layer instanceof ol.layer.Vector) { return new ol.renderer.canvas.VectorLayer(layer); } else { diff --git a/src/ol/renderer/canvas/canvasvectortilelayerrenderer.js b/src/ol/renderer/canvas/canvasvectortilelayerrenderer.js new file mode 100644 index 0000000000..169641285b --- /dev/null +++ b/src/ol/renderer/canvas/canvasvectortilelayerrenderer.js @@ -0,0 +1,445 @@ +goog.provide('ol.renderer.canvas.VectorTileLayer'); + +goog.require('goog.array'); +goog.require('goog.asserts'); +goog.require('goog.events'); +goog.require('goog.object'); +goog.require('goog.vec.Mat4'); +goog.require('ol.TileRange'); +goog.require('ol.TileState'); +goog.require('ol.ViewHint'); +goog.require('ol.dom'); +goog.require('ol.extent'); +goog.require('ol.layer.VectorTile'); +goog.require('ol.proj.Units'); +goog.require('ol.render.EventType'); +goog.require('ol.render.canvas.ReplayGroup'); +goog.require('ol.renderer.canvas.Layer'); +goog.require('ol.renderer.vector'); +goog.require('ol.size'); +goog.require('ol.source.VectorTile'); +goog.require('ol.tilecoord'); +goog.require('ol.vec.Mat4'); + + + +/** + * @constructor + * @extends {ol.renderer.canvas.Layer} + * @param {ol.layer.VectorTile} layer VectorTile layer. + */ +ol.renderer.canvas.VectorTileLayer = function(layer) { + + goog.base(this, layer); + + /** + * @private + * @type {CanvasRenderingContext2D} + */ + this.context_ = ol.dom.createCanvasContext2D(); + + /** + * @private + * @type {boolean} + */ + this.dirty_ = false; + + /** + * @private + * @type {Array.} + */ + this.renderedTiles_ = []; + + /** + * @private + * @type {ol.Extent} + */ + this.tmpExtent_ = ol.extent.createEmpty(); + + /** + * @private + * @type {ol.Size} + */ + this.tmpSize_ = [NaN, NaN]; + + /** + * @private + * @type {!goog.vec.Mat4.Number} + */ + this.tmpTransform_ = goog.vec.Mat4.createNumber(); + +}; +goog.inherits(ol.renderer.canvas.VectorTileLayer, ol.renderer.canvas.Layer); + + +/** + * @inheritDoc + */ +ol.renderer.canvas.VectorTileLayer.prototype.composeFrame = + function(frameState, layerState, context) { + + var pixelRatio = frameState.pixelRatio; + var skippedFeatureUids = layerState.managed ? + frameState.skippedFeatureUids : {}; + var viewState = frameState.viewState; + var center = viewState.center; + var projection = viewState.projection; + var resolution = viewState.resolution; + var rotation = viewState.rotation; + var source = this.getLayer().getSource(); + goog.asserts.assertInstanceof(source, ol.source.VectorTile, + 'Source is an ol.source.VectorTile'); + + var transform = this.getTransform(frameState, 0); + + this.dispatchPreComposeEvent(context, frameState, transform); + + var layer = this.getLayer(); + var replayContext; + if (layer.hasListener(ol.render.EventType.RENDER)) { + // resize and clear + this.context_.canvas.width = context.canvas.width; + this.context_.canvas.height = context.canvas.height; + replayContext = this.context_; + } else { + replayContext = context; + } + // for performance reasons, context.save / context.restore is not used + // to save and restore the transformation matrix and the opacity. + // see http://jsperf.com/context-save-restore-versus-variable + var alpha = replayContext.globalAlpha; + replayContext.globalAlpha = layerState.opacity; + + var tilesToDraw = this.renderedTiles_; + var tileGrid = source.getTileGrid(); + + var currentZ, i, ii, origin, scale, tile, tileExtent, tileSize; + var tilePixelRatio, tilePixelResolution, tilePixelSize, tileResolution; + for (i = 0, ii = tilesToDraw.length; i < ii; ++i) { + tile = tilesToDraw[i]; + currentZ = tile.getTileCoord()[0]; + tileSize = tileGrid.getTileSize(currentZ); + tilePixelSize = source.getTilePixelSize(currentZ, pixelRatio, projection); + tilePixelRatio = tilePixelSize[0] / + ol.size.toSize(tileGrid.getTileSize(currentZ), this.tmpSize_)[0]; + 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); + } + tile.getReplayState().replayGroup.replay(replayContext, pixelRatio, + transform, rotation, skippedFeatureUids); + } + + transform = this.getTransform(frameState, 0); + + if (replayContext != context) { + this.dispatchRenderEvent(replayContext, frameState, transform); + context.drawImage(replayContext.canvas, 0, 0); + } + replayContext.globalAlpha = alpha; + + this.dispatchPostComposeEvent(context, frameState, transform); +}; + + +/** + * @param {ol.VectorTile} tile Tile. + * @param {ol.layer.VectorTile} layer Vector tile layer. + * @param {number} resolution Resolution. + * @param {number} pixelRatio Pixel ratio. + * @return {boolean} Success. + */ +ol.renderer.canvas.VectorTileLayer.prototype.createReplayGroup = function(tile, + layer, resolution, pixelRatio) { + var revision = layer.getRevision(); + var renderOrder = layer.getRenderOrder(); + if (!goog.isDef(renderOrder)) { + renderOrder = null; + } + + var replayState = tile.getReplayState(); + if (!replayState.dirty && + replayState.renderedResolution == resolution && + replayState.renderedRevision == revision && + replayState.renderedRenderOrder == renderOrder) { + return false; + } + + // FIXME dispose of old replayGroup in post render + goog.dispose(replayState.replayGroup); + replayState.replayGroup = null; + replayState.dirty = false; + + var source = layer.getSource(); + goog.asserts.assertInstanceof(source, ol.source.VectorTile, + 'Source is an ol.source.VectorTile'); + var tileGrid = source.getTileGrid(); + var pixelSpace = tile.getProjection().getUnits() == ol.proj.Units.TILE_PIXELS; + var extent = pixelSpace ? + [0, 0].concat(source.getTilePixelSize( + tileGrid.getZForResolution(resolution), pixelRatio, + tile.getProjection())) : + tileGrid.getTileCoordExtent(tile.getTileCoord()); + var tileResolution = pixelSpace ? source.getTilePixelRatio() : resolution; + var replayGroup = new ol.render.canvas.ReplayGroup(pixelSpace ? 0 : + ol.renderer.vector.getTolerance(tileResolution, pixelRatio), extent, + tileResolution, layer.getRenderBuffer(), + source.getRightHandedPolygons()); + + /** + * @param {ol.Feature} feature Feature. + * @this {ol.renderer.canvas.VectorTileLayer} + */ + function renderFeature(feature) { + var styles; + if (goog.isDef(feature.getStyleFunction())) { + styles = feature.getStyleFunction().call(feature, resolution); + } else if (goog.isDef(layer.getStyleFunction())) { + styles = layer.getStyleFunction()(feature, resolution); + } + var squaredTolerance = pixelSpace ? -1 : + ol.renderer.vector.getSquaredTolerance(resolution, pixelRatio); + if (goog.isDefAndNotNull(styles)) { + var dirty = this.renderFeature(feature, squaredTolerance, styles, + replayGroup); + replayState.dirty = replayState.dirty || dirty; + this.dirty_ = this.dirty_ || replayState.dirty; + } + } + + var features = tile.getFeatures(); + if (!goog.isNull(renderOrder) && + renderOrder !== replayState.renderedRenderOrder) { + goog.array.sort(features, renderOrder); + } + goog.array.forEach(features, renderFeature, this); + + replayGroup.finish(); + + replayState.renderedResolution = resolution; + replayState.renderedRevision = revision; + replayState.renderedRenderOrder = renderOrder; + replayState.replayGroup = replayGroup; + + return true; +}; + + +/** + * @inheritDoc + */ +ol.renderer.canvas.VectorTileLayer.prototype.forEachFeatureAtCoordinate = + function(coordinate, frameState, callback, thisArg) { + var resolution = frameState.viewState.resolution; + var rotation = frameState.viewState.rotation; + var layer = this.getLayer(); + var layerState = frameState.layerStates[goog.getUid(layer)]; + /** @type {Object.} */ + var features = {}; + + var replayables = this.renderedTiles_; + var found, tileSpaceCoordinate; + var i, ii, origin, replayGroup, source; + var tile, tileCoord, tileExtent, tilePixelRatio, tileResolution, tileSize; + for (i = 0, ii = replayables.length; i < ii; ++i) { + tile = replayables[i]; + tileCoord = tile.getTileCoord(); + source = layer.getSource(); + goog.asserts.assertInstanceof(source, ol.source.VectorTile, + 'Source is an ol.source.VectorTile'); + tileExtent = source.getTileGrid().getTileCoordExtent(tileCoord); + if (!ol.extent.containsCoordinate(tileExtent, coordinate)) { + continue; + } + if (tile.getProjection().getUnits() === ol.proj.Units.TILE_PIXELS) { + origin = ol.extent.getTopLeft( + source.getTileGrid().getTileCoordExtent(tileCoord)); + tilePixelRatio = source.getTilePixelRatio(); + tileResolution = resolution / tilePixelRatio; + tileSize = ol.size.toSize( + source.getTileGrid().getTileSize(tileCoord[0])); + tileSpaceCoordinate = [ + (coordinate[0] - origin[0]) / tileResolution, + (origin[1] - coordinate[1]) / tileResolution + ]; + resolution = tilePixelRatio; + } else { + tileSpaceCoordinate = coordinate; + } + replayGroup = tile.getReplayState().replayGroup; + found = found || replayGroup.forEachFeatureAtCoordinate( + tileSpaceCoordinate, resolution, rotation, + layerState.managed ? frameState.skippedFeatureUids : {}, + /** + * @param {ol.Feature} feature Feature. + * @return {?} Callback result. + */ + function(feature) { + goog.asserts.assert(goog.isDef(feature), 'received a feature'); + var key = goog.getUid(feature).toString(); + if (!(key in features)) { + features[key] = true; + return callback.call(thisArg, feature, layer); + } + }); + } + return found; +}; + + +/** + * Handle changes in image style state. + * @param {goog.events.Event} event Image style change event. + * @private + */ +ol.renderer.canvas.VectorTileLayer.prototype.handleStyleImageChange_ = + function(event) { + this.renderIfReadyAndVisible(); +}; + + +/** + * @inheritDoc + */ +ol.renderer.canvas.VectorTileLayer.prototype.prepareFrame = + function(frameState, layerState) { + var layer = /** @type {ol.layer.Vector} */ (this.getLayer()); + goog.asserts.assertInstanceof(layer, ol.layer.VectorTile, + 'layer is an instance of ol.layer.VectorTile'); + var source = layer.getSource(); + goog.asserts.assertInstanceof(source, ol.source.VectorTile, + 'Source is an ol.source.VectorTile'); + + this.updateAttributions( + frameState.attributions, source.getAttributions()); + this.updateLogos(frameState, source); + + var animating = frameState.viewHints[ol.ViewHint.ANIMATING]; + var interacting = frameState.viewHints[ol.ViewHint.INTERACTING]; + var updateWhileAnimating = layer.getUpdateWhileAnimating(); + var updateWhileInteracting = layer.getUpdateWhileInteracting(); + + if (!this.dirty_ && (!updateWhileAnimating && animating) || + (!updateWhileInteracting && interacting)) { + return true; + } + + var extent = frameState.extent; + if (goog.isDef(layerState.extent)) { + extent = ol.extent.getIntersection(extent, layerState.extent); + } + if (ol.extent.isEmpty(extent)) { + // Return false to prevent the rendering of the layer. + return false; + } + + var viewState = frameState.viewState; + var projection = viewState.projection; + var resolution = viewState.resolution; + var pixelRatio = frameState.pixelRatio; + + var tileGrid = source.getTileGrid(); + var z = tileGrid.getZForResolution(resolution); + var tileRange = tileGrid.getTileRangeForExtentAndResolution( + extent, resolution); + this.updateUsedTiles(frameState.usedTiles, source, z, tileRange); + this.manageTilePyramid(frameState, source, tileGrid, pixelRatio, + projection, extent, z, layer.getPreload()); + this.scheduleExpireCache(frameState, source); + + /** + * @type {Object.>} + */ + var tilesToDrawByZ = {}; + tilesToDrawByZ[z] = {}; + + var findLoadedTiles = this.createLoadedTileFinder(source, tilesToDrawByZ); + + var useInterimTilesOnError = layer.getUseInterimTilesOnError(); + + var tmpExtent = this.tmpExtent_; + var tmpTileRange = new ol.TileRange(0, 0, 0, 0); + var childTileRange, fullyLoaded, tile, tileState, x, y; + for (x = tileRange.minX; x <= tileRange.maxX; ++x) { + for (y = tileRange.minY; y <= tileRange.maxY; ++y) { + + tile = source.getTile(z, x, y, pixelRatio, projection); + tileState = tile.getState(); + if (tileState == ol.TileState.LOADED || + tileState == ol.TileState.EMPTY || + (tileState == ol.TileState.ERROR && !useInterimTilesOnError)) { + tilesToDrawByZ[z][ol.tilecoord.toString(tile.tileCoord)] = tile; + continue; + } + + fullyLoaded = tileGrid.forEachTileCoordParentTileRange( + tile.tileCoord, findLoadedTiles, null, tmpTileRange, tmpExtent); + if (!fullyLoaded) { + childTileRange = tileGrid.getTileCoordChildTileRange( + tile.tileCoord, tmpTileRange, tmpExtent); + if (!goog.isNull(childTileRange)) { + findLoadedTiles(z + 1, childTileRange); + } + } + + } + } + + /** @type {Array.} */ + var zs = goog.array.map(goog.object.getKeys(tilesToDrawByZ), Number); + goog.array.sort(zs); + var replayables = []; + var i, ii, currentZ, tileCoordKey, tilesToDraw; + for (i = 0, ii = zs.length; i < ii; ++i) { + currentZ = zs[i]; + tilesToDraw = tilesToDrawByZ[currentZ]; + for (tileCoordKey in tilesToDraw) { + tile = tilesToDraw[tileCoordKey]; + if (tile.getState() == ol.TileState.LOADED) { + replayables.push(tile); + } + } + } + + for (i = 0, ii = replayables.length; i < ii; ++i) { + tile = replayables[i]; + this.dirty_ = this.createReplayGroup(tile, layer, resolution, pixelRatio) || + this.dirty_; + } + + this.renderedTiles_ = replayables; + + return true; +}; + + +/** + * @param {ol.Feature} feature Feature. + * @param {number} squaredTolerance Squared tolerance. + * @param {Array.} styles Array of styles + * @param {ol.render.canvas.ReplayGroup} replayGroup Replay group. + * @return {boolean} `true` if an image is loading. + */ +ol.renderer.canvas.VectorTileLayer.prototype.renderFeature = + function(feature, squaredTolerance, styles, replayGroup) { + if (!goog.isDefAndNotNull(styles)) { + return false; + } + var i, ii, loading = false; + for (i = 0, ii = styles.length; i < ii; ++i) { + loading = ol.renderer.vector.renderFeature( + replayGroup, feature, styles[i], squaredTolerance, + this.handleStyleImageChange_, this) || loading; + } + return loading; +};