Add canvas renderer

This commit is contained in:
Tom Payne
2013-01-14 21:47:51 +01:00
parent 7e7356dfd6
commit 54327fb750
6 changed files with 477 additions and 1 deletions

View File

@@ -3,6 +3,7 @@
@exportProperty ol.Map.prototype.getInteractions
@exportSymbol ol.RendererHint
@exportProperty ol.RendererHint.CANVAS
@exportProperty ol.RendererHint.DOM
@exportProperty ol.RendererHint.WEBGL

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
};

View File

@@ -0,0 +1,9 @@
goog.provide('ol.renderer.canvas');
goog.require('ol.canvas');
/**
* @return {boolean} Is supported.
*/
ol.renderer.canvas.isSupported = ol.canvas.isSupported;

View File

@@ -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.<number, Object.<string, ol.Tile>>}
*/
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.<number>} */
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);
};