diff --git a/src/ol/map.exports b/src/ol/map.exports index b2f70f549b..85ac465dfd 100644 --- a/src/ol/map.exports +++ b/src/ol/map.exports @@ -3,6 +3,7 @@ @exportProperty ol.Map.prototype.getInteractions @exportSymbol ol.RendererHint +@exportProperty ol.RendererHint.CANVAS @exportProperty ol.RendererHint.DOM @exportProperty ol.RendererHint.WEBGL diff --git a/src/ol/map.js b/src/ol/map.js index 383529adeb..349a32f65b 100644 --- a/src/ol/map.js +++ b/src/ol/map.js @@ -54,12 +54,20 @@ goog.require('ol.interaction.MouseWheelZoom'); goog.require('ol.interaction.condition'); goog.require('ol.renderer.Layer'); goog.require('ol.renderer.Map'); +goog.require('ol.renderer.canvas'); +goog.require('ol.renderer.canvas.Map'); goog.require('ol.renderer.dom'); goog.require('ol.renderer.dom.Map'); goog.require('ol.renderer.webgl'); goog.require('ol.renderer.webgl.Map'); +/** + * @define {boolean} Whether to enable canvas. + */ +ol.ENABLE_CANVAS = true; + + /** * @define {boolean} Whether to enable DOM. */ @@ -76,6 +84,7 @@ ol.ENABLE_WEBGL = true; * @enum {string} */ ol.RendererHint = { + CANVAS: 'canvas', DOM: 'dom', WEBGL: 'webgl' }; @@ -86,6 +95,7 @@ ol.RendererHint = { */ ol.DEFAULT_RENDERER_HINTS = [ ol.RendererHint.WEBGL, + ol.RendererHint.CANVAS, ol.RendererHint.DOM ]; @@ -795,7 +805,12 @@ ol.Map.createOptionsInternal = function(mapOptions) { var i, rendererHint; for (i = 0; i < rendererHints.length; ++i) { rendererHint = rendererHints[i]; - if (rendererHint == ol.RendererHint.DOM) { + if (rendererHint == ol.RendererHint.CANVAS) { + if (ol.ENABLE_CANVAS && ol.renderer.canvas.isSupported()) { + rendererConstructor = ol.renderer.canvas.Map; + break; + } + } else if (rendererHint == ol.RendererHint.DOM) { if (ol.ENABLE_DOM && ol.renderer.dom.isSupported()) { rendererConstructor = ol.renderer.dom.Map; break; diff --git a/src/ol/renderer/canvas/canvaslayerrenderer.js b/src/ol/renderer/canvas/canvaslayerrenderer.js new file mode 100644 index 0000000000..78ed9d6665 --- /dev/null +++ b/src/ol/renderer/canvas/canvaslayerrenderer.js @@ -0,0 +1,37 @@ +goog.provide('ol.renderer.canvas.Layer'); + +goog.require('ol.FrameState'); +goog.require('ol.layer.LayerState'); +goog.require('ol.renderer.Layer'); + + + +/** + * @constructor + * @extends {ol.renderer.Layer} + * @param {ol.renderer.Map} mapRenderer Map renderer. + * @param {ol.layer.Layer} layer Layer. + */ +ol.renderer.canvas.Layer = function(mapRenderer, layer) { + goog.base(this, mapRenderer, layer); +}; +goog.inherits(ol.renderer.canvas.Layer, ol.renderer.Layer); + + +/** + * @return {HTMLCanvasElement|HTMLVideoElement|Image} Canvas. + */ +ol.renderer.canvas.Layer.prototype.getImage = goog.abstractMethod; + + +/** + * @return {!goog.vec.Mat4.Number} Transform. + */ +ol.renderer.canvas.Layer.prototype.getTransform = goog.abstractMethod; + + +/** + * @param {ol.FrameState} frameState Frame state. + * @param {ol.layer.LayerState} layerState Layer state. + */ +ol.renderer.canvas.Layer.prototype.renderFrame = goog.abstractMethod; diff --git a/src/ol/renderer/canvas/canvasmaprenderer.js b/src/ol/renderer/canvas/canvasmaprenderer.js new file mode 100644 index 0000000000..34d82d5722 --- /dev/null +++ b/src/ol/renderer/canvas/canvasmaprenderer.js @@ -0,0 +1,178 @@ +// FIXME offset panning + +goog.provide('ol.renderer.canvas.Map'); + +goog.require('goog.dom'); +goog.require('goog.style'); +goog.require('goog.vec.Mat4'); +goog.require('ol.Size'); +goog.require('ol.layer.TileLayer'); +goog.require('ol.renderer.Map'); +goog.require('ol.renderer.canvas.TileLayer'); + + + +/** + * @constructor + * @extends {ol.renderer.Map} + * @param {Element} container Container. + * @param {ol.Map} map Map. + */ +ol.renderer.canvas.Map = function(container, map) { + + goog.base(this, container, map); + + /** + * @private + * @type {ol.Size} + */ + this.canvasSize_ = new ol.Size(container.clientHeight, container.clientWidth); + + /** + * @private + * @type {Element} + */ + this.canvas_ = goog.dom.createElement(goog.dom.TagName.CANVAS); + this.canvas_.height = this.canvasSize_.height; + this.canvas_.width = this.canvasSize_.width; + this.canvas_.className = 'ol-unselectable'; + goog.dom.insertChildAt(container, this.canvas_, 0); + + /** + * @private + * @type {boolean} + */ + this.renderedVisible_ = true; + + /** + * @private + * @type {CanvasRenderingContext2D} + */ + this.context_ = this.canvas_.getContext('2d'); + +}; +goog.inherits(ol.renderer.canvas.Map, ol.renderer.Map); + + +/** + * @inheritDoc + */ +ol.renderer.canvas.Map.prototype.createLayerRenderer = function(layer) { + if (layer instanceof ol.layer.TileLayer) { + return new ol.renderer.canvas.TileLayer(this, layer); + } else { + goog.asserts.assert(false); + return null; + } +}; + + +/** + * @inheritDoc + */ +ol.renderer.canvas.Map.prototype.handleBackgroundColorChanged = function() { + this.getMap().render(); +}; + + +/** + * @inheritDoc + */ +ol.renderer.canvas.Map.prototype.handleViewPropertyChanged = function() { + goog.base(this, 'handleViewPropertyChanged'); + this.getMap().render(); +}; + + +/** + * @param {goog.events.Event} event Event. + * @protected + */ +ol.renderer.canvas.Map.prototype.handleLayerRendererChange = function(event) { + this.getMap().render(); +}; + + +/** + * @inheritDoc + */ +ol.renderer.canvas.Map.prototype.handleSizeChanged = function() { + goog.base(this, 'handleSizeChanged'); + this.getMap().render(); +}; + + +/** + * @inheritDoc + */ +ol.renderer.canvas.Map.prototype.handleViewChanged = function() { + goog.base(this, 'handleViewChanged'); + this.getMap().render(); +}; + + +/** + * @inheritDoc + */ +ol.renderer.canvas.Map.prototype.renderFrame = function(frameState) { + + if (goog.isNull(frameState)) { + if (this.renderedVisible_) { + goog.style.showElement(this.canvas_, false); + this.renderedVisible_ = false; + } + return; + } + + var size = frameState.size; + if (!this.canvasSize_.equals(size)) { + this.canvas_.width = size.width; + this.canvas_.height = size.height; + this.canvasSize_ = size; + } + + // FIXME filling the background doesn't seem to work + var context = this.context_; + context.setTransform(1, 0, 1, 0, 0, 0); + var backgroundColor = frameState.backgroundColor; + context.fillStyle = 'rgb(' + + backgroundColor.r.toFixed(0) + ',' + + backgroundColor.g.toFixed(0) + ',' + + backgroundColor.b.toFixed(0) + ')'; + context.globalAlpha = 1; + context.fillRect(0, 0, size.width, size.height); + + goog.array.forEach(frameState.layersArray, function(layer) { + + var layerState = frameState.layerStates[goog.getUid(layer)]; + if (!layerState.visible) { + return; + } else if (!layerState.ready) { + frameState.animate = true; + return; + } + var layerRenderer = this.getLayerRenderer(layer); + layerRenderer.renderFrame(frameState, layerState); + + var transform = layerRenderer.getTransform(); + context.setTransform( + goog.vec.Mat4.getElement(transform, 0, 0), + goog.vec.Mat4.getElement(transform, 1, 0), + goog.vec.Mat4.getElement(transform, 0, 1), + goog.vec.Mat4.getElement(transform, 1, 1), + goog.vec.Mat4.getElement(transform, 0, 3), + goog.vec.Mat4.getElement(transform, 1, 3)); + + context.globalAlpha = layerState.opacity; + context.drawImage(layerRenderer.getImage(), 0, 0); + + }, this); + + if (!this.renderedVisible_) { + goog.style.showElement(this.canvas_, true); + this.renderedVisible_ = true; + } + + this.calculateMatrices2D(frameState); + +}; diff --git a/src/ol/renderer/canvas/canvasrenderer.js b/src/ol/renderer/canvas/canvasrenderer.js new file mode 100644 index 0000000000..f28861dcdc --- /dev/null +++ b/src/ol/renderer/canvas/canvasrenderer.js @@ -0,0 +1,9 @@ +goog.provide('ol.renderer.canvas'); + +goog.require('ol.canvas'); + + +/** + * @return {boolean} Is supported. + */ +ol.renderer.canvas.isSupported = ol.canvas.isSupported; diff --git a/src/ol/renderer/canvas/canvastilelayerrenderer.js b/src/ol/renderer/canvas/canvastilelayerrenderer.js new file mode 100644 index 0000000000..4c17795648 --- /dev/null +++ b/src/ol/renderer/canvas/canvastilelayerrenderer.js @@ -0,0 +1,236 @@ +// FIXME don't redraw tiles if not needed +// FIXME find correct globalCompositeOperation +// FIXME optimize :-) + +goog.provide('ol.renderer.canvas.TileLayer'); + +goog.require('goog.dom'); +goog.require('goog.style'); +goog.require('goog.vec.Mat4'); +goog.require('ol.Size'); +goog.require('ol.TileRange'); +goog.require('ol.layer.TileLayer'); +goog.require('ol.renderer.Map'); +goog.require('ol.renderer.canvas.Layer'); + + + +/** + * @constructor + * @extends {ol.renderer.canvas.Layer} + * @param {ol.renderer.Map} mapRenderer Map renderer. + * @param {ol.layer.TileLayer} tileLayer Tile layer. + */ +ol.renderer.canvas.TileLayer = function(mapRenderer, tileLayer) { + + goog.base(this, mapRenderer, tileLayer); + + /** + * @private + * @type {HTMLCanvasElement} + */ + this.canvas_ = null; + + /** + * @private + * @type {ol.Size} + */ + this.canvasSize_ = null; + + /** + * @private + * @type {CanvasRenderingContext2D} + */ + this.context_ = null; + + /** + * @private + * @type {!goog.vec.Mat4.Number} + */ + this.transform_ = goog.vec.Mat4.createNumber(); + +}; +goog.inherits(ol.renderer.canvas.TileLayer, ol.renderer.canvas.Layer); + + +/** + * @inheritDoc + */ +ol.renderer.canvas.TileLayer.prototype.getImage = function() { + return this.canvas_; +}; + + +/** + * @return {ol.layer.TileLayer} Tile layer. + */ +ol.renderer.canvas.TileLayer.prototype.getTileLayer = function() { + return /** @type {ol.layer.TileLayer} */ (this.getLayer()); +}; + + +/** + * @inheritDoc + */ +ol.renderer.canvas.TileLayer.prototype.getTransform = function() { + return this.transform_; +}; + + +/** + * @inheritDoc + */ +ol.renderer.canvas.TileLayer.prototype.renderFrame = + function(frameState, layerState) { + + var view2DState = frameState.view2DState; + + var tileLayer = this.getTileLayer(); + var tileSource = tileLayer.getTileSource(); + var tileGrid = tileSource.getTileGrid(); + var tileSize = tileGrid.getTileSize(); + var z = tileGrid.getZForResolution(view2DState.resolution); + var tileResolution = tileGrid.getResolution(z); + var tileRange = tileGrid.getTileRangeForExtentAndResolution( + frameState.extent, tileResolution); + + var canvasSize = new ol.Size( + tileSize.width * tileRange.getWidth(), + tileSize.height * tileRange.getHeight()); + + var canvas, context; + if (goog.isNull(this.canvas_)) { + canvas = /** @type {HTMLCanvasElement} */ + (goog.dom.createElement(goog.dom.TagName.CANVAS)); + canvas.width = canvasSize.width; + canvas.height = canvasSize.height; + context = /** @type {CanvasRenderingContext2D} */ (canvas.getContext('2d')); + this.canvas_ = canvas; + this.canvasSize_ = canvasSize; + this.context_ = context; + } else { + canvas = this.canvas_; + context = this.context_; + if (!this.canvasSize_.equals(canvasSize)) { + canvas.width = canvasSize.width; + canvas.height = canvasSize.height; + this.canvasSize_ = canvasSize; + } + } + + context.clearRect(0, 0, canvasSize.width, canvasSize.height); + + /** + * @type {Object.>} + */ + var tilesToDrawByZ = {}; + tilesToDrawByZ[z] = {}; + + var findInterimTiles = function(z, tileRange) { + // FIXME this could be more efficient about filling partial holes + var fullyCovered = true; + var tile, tileCoord, tileCoordKey, x, y; + for (x = tileRange.minX; x <= tileRange.maxX; ++x) { + for (y = tileRange.minY; y <= tileRange.maxY; ++y) { + tileCoord = new ol.TileCoord(z, x, y); + tileCoordKey = tileCoord.toString(); + if (tilesToDrawByZ[z] && tilesToDrawByZ[z][tileCoordKey]) { + return; + } + tile = tileSource.getTile(tileCoord); + if (!goog.isNull(tile) && tile.getState() == ol.TileState.LOADED) { + if (!tilesToDrawByZ[z]) { + tilesToDrawByZ[z] = {}; + } + tilesToDrawByZ[z][tileCoordKey] = tile; + } else { + fullyCovered = false; + } + } + } + return fullyCovered; + }; + + var allTilesLoaded = true; + var tile, tileCenter, tileCoord, tileState, x, y; + for (x = tileRange.minX; x <= tileRange.maxX; ++x) { + for (y = tileRange.minY; y <= tileRange.maxY; ++y) { + + tileCoord = new ol.TileCoord(z, x, y); + tile = tileSource.getTile(tileCoord); + if (goog.isNull(tile)) { + continue; + } + + tileState = tile.getState(); + if (tileState == ol.TileState.IDLE) { + tileCenter = tileGrid.getTileCoordCenter(tileCoord); + frameState.tileQueue.enqueue(tile, tileCenter, tileResolution); + } else if (tileState == ol.TileState.LOADED) { + tilesToDrawByZ[z][tileCoord.toString()] = tile; + continue; + } else if (tileState == ol.TileState.ERROR) { + continue; + } + + allTilesLoaded = false; + tileGrid.forEachTileCoordParentTileRange(tileCoord, findInterimTiles); + + } + } + + /** @type {Array.} */ + var zs = goog.array.map(goog.object.getKeys(tilesToDrawByZ), Number); + goog.array.sort(zs); + var origin = tileGrid.getTileCoordExtent( + new ol.TileCoord(z, tileRange.minX, tileRange.maxY)).getTopLeft(); + var currentZ, i, scale, tileCoordKey, tileExtent, tilesToDraw; + for (i = 0; i < zs.length; ++i) { + currentZ = zs[i]; + tilesToDraw = tilesToDrawByZ[currentZ]; + if (currentZ == z) { + for (tileCoordKey in tilesToDraw) { + tile = tilesToDraw[tileCoordKey]; + context.drawImage( + tile.getImage(), + tileSize.width * (tile.tileCoord.x - tileRange.minX), + tileSize.height * (tileRange.maxY - tile.tileCoord.y)); + } + } else { + scale = tileGrid.getResolution(currentZ) / tileResolution; + for (tileCoordKey in tilesToDraw) { + tile = tilesToDraw[tileCoordKey]; + tileExtent = tileGrid.getTileCoordExtent(tile.tileCoord); + context.drawImage( + tile.getImage(), + (tileExtent.minX - origin.x) / tileResolution, + (origin.y - tileExtent.maxY) / tileResolution, + scale * tileSize.width, + scale * tileSize.height); + } + } + } + + if (!allTilesLoaded) { + frameState.animate = true; + } + + this.updateTileUsage(frameState.tileUsage, tileSource, z, tileRange); + + var transform = this.transform_; + goog.vec.Mat4.makeIdentity(transform); + goog.vec.Mat4.translate(transform, + frameState.size.width / 2, frameState.size.height / 2, 0); + goog.vec.Mat4.rotateZ(transform, view2DState.rotation); + goog.vec.Mat4.scale( + transform, + tileResolution / view2DState.resolution, + tileResolution / view2DState.resolution, + 1); + goog.vec.Mat4.translate( + transform, + (origin.x - view2DState.center.x) / tileResolution, + (view2DState.center.y - origin.y) / tileResolution, + 0); + +};