diff --git a/examples/side-by-side.html b/examples/side-by-side.html
index 6aa2307ef5..ffc817397b 100644
--- a/examples/side-by-side.html
+++ b/examples/side-by-side.html
@@ -16,19 +16,22 @@
Side-by-side DOM and WebGL sync'ed maps.
+ Side-by-side DOM, WebGL and Canvas sync'ed maps.
diff --git a/examples/side-by-side.js b/examples/side-by-side.js
index 540a277e76..1c32f475e5 100644
--- a/examples/side-by-side.js
+++ b/examples/side-by-side.js
@@ -62,6 +62,22 @@ webglMap.getControls().push(new ol.control.MousePosition({
undefinedHTML: ' '
}));
+var canvasMap = new ol.Map({
+ renderer: ol.RendererHint.CANVAS,
+ target: 'canvasMap'
+});
+if (canvasMap !== null) {
+ canvasMap.bindTo('layers', domMap);
+ canvasMap.bindTo('view', domMap);
+}
+
+canvasMap.getControls().push(new ol.control.MousePosition({
+ coordinateFormat: ol.Coordinate.toStringHDMS,
+ projection: ol.Projection.getFromCode('EPSG:4326'),
+ target: document.getElementById('canvasMousePosition'),
+ undefinedHtml: ' '
+}));
+
var keyboardInteraction = new ol.interaction.Keyboard();
keyboardInteraction.addCallback('0', function() {
layer.setBrightness(0);
@@ -94,11 +110,13 @@ keyboardInteraction.addCallback('j', function() {
var bounce = ol.animation.createBounce(2 * view.getResolution());
domMap.addPreRenderFunction(bounce);
webglMap.addPreRenderFunction(bounce);
+ canvasMap.addPreRenderFunction(bounce);
});
keyboardInteraction.addCallback('l', function() {
var panFrom = ol.animation.createPanFrom(view.getCenter());
domMap.addPreRenderFunction(panFrom);
webglMap.addPreRenderFunction(panFrom);
+ canvasMap.addPreRenderFunction(panFrom);
view.setCenter(LONDON);
});
keyboardInteraction.addCallback('L', function() {
@@ -111,12 +129,14 @@ keyboardInteraction.addCallback('L', function() {
var preRenderFunctions = [bounce, panFrom, spin];
domMap.addPreRenderFunctions(preRenderFunctions);
webglMap.addPreRenderFunctions(preRenderFunctions);
+ canvasMap.addPreRenderFunctions(preRenderFunctions);
view.setCenter(LONDON);
});
keyboardInteraction.addCallback('m', function() {
var panFrom = ol.animation.createPanFrom(view.getCenter(), 1000);
domMap.addPreRenderFunction(panFrom);
webglMap.addPreRenderFunction(panFrom);
+ canvasMap.addPreRenderFunction(panFrom);
view.setCenter(MOSCOW);
});
keyboardInteraction.addCallback('M', function() {
@@ -129,6 +149,7 @@ keyboardInteraction.addCallback('M', function() {
var preRenderFunctions = [bounce, panFrom, spin];
domMap.addPreRenderFunctions(preRenderFunctions);
webglMap.addPreRenderFunctions(preRenderFunctions);
+ canvasMap.addPreRenderFunctions(preRenderFunctions);
view.setCenter(MOSCOW);
});
keyboardInteraction.addCallback('o', function() {
@@ -154,10 +175,12 @@ keyboardInteraction.addCallback('x', function() {
var spin = ol.animation.createSpin(2000, 2);
domMap.addPreRenderFunction(spin);
webglMap.addPreRenderFunction(spin);
+ canvasMap.addPreRenderFunction(spin);
});
keyboardInteraction.addCallback('X', function() {
var spin = ol.animation.createSpin(2000, -2);
domMap.addPreRenderFunction(spin);
webglMap.addPreRenderFunction(spin);
+ canvasMap.addPreRenderFunction(spin);
});
domMap.getInteractions().push(keyboardInteraction);
diff --git a/examples/two-layers.html b/examples/two-layers.html
index cc71de2683..4d7ac617ce 100644
--- a/examples/two-layers.html
+++ b/examples/two-layers.html
@@ -16,15 +16,17 @@
Two-layer example
- Sync'ed DOM and WebGL maps with two layers.
+ Sync'ed DOM, WebGL and Canvas maps with two layers.
diff --git a/examples/two-layers.js b/examples/two-layers.js
index 8ab0550a7d..4817a59a15 100644
--- a/examples/two-layers.js
+++ b/examples/two-layers.js
@@ -40,3 +40,11 @@ var domMap = new ol.Map({
});
domMap.bindTo('layers', webglMap);
domMap.bindTo('view', webglMap);
+
+
+var canvasMap = new ol.Map({
+ renderer: ol.RendererHint.CANVAS,
+ target: 'canvasMap'
+});
+canvasMap.bindTo('layers', webglMap);
+canvasMap.bindTo('view', webglMap);
diff --git a/src/ol/canvas/canvas.js b/src/ol/canvas/canvas.js
new file mode 100644
index 0000000000..4b62511cf7
--- /dev/null
+++ b/src/ol/canvas/canvas.js
@@ -0,0 +1,21 @@
+goog.provide('ol.canvas');
+
+goog.require('goog.dom');
+goog.require('goog.dom.TagName');
+
+
+/**
+ * @return {boolean} Is supported.
+ */
+ol.canvas.isSupported = function() {
+ if (!('HTMLCanvasElement' in goog.global)) {
+ return false;
+ }
+ try {
+ var canvas = /** @type {HTMLCanvasElement} */
+ (goog.dom.createElement(goog.dom.TagName.CANVAS));
+ return !goog.isNull(canvas.getContext('2d'));
+ } catch (e) {
+ return false;
+ }
+};
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);
+
+};