diff --git a/Makefile b/Makefile
index d380929..e36aadc 100644
--- a/Makefile
+++ b/Makefile
@@ -5,6 +5,7 @@ BROWSERIFY=./node_modules/browserify/bin/cmd.js
JS_CLIENT_FILES= lib/torque/*.js \
lib/torque/renderer/*.js \
lib/torque/gmaps/*.js \
+ lib/torque/ol/*.js \
lib/torque/leaflet/leaflet_tileloader_mixin.js \
lib/torque/leaflet/canvas_layer.js \
lib/torque/leaflet/torque.js
diff --git a/examples/navy_ol.html b/examples/navy_ol.html
new file mode 100644
index 0000000..722ff34
--- /dev/null
+++ b/examples/navy_ol.html
@@ -0,0 +1,73 @@
+
+
+
+
+ CartoDb Torque Layer Example
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/lib/torque/index.js b/lib/torque/index.js
index 2dd68bc..2f3e34b 100644
--- a/lib/torque/index.js
+++ b/lib/torque/index.js
@@ -15,3 +15,5 @@ var gmaps = require('./gmaps');
module.exports.GMapsTileLoader = gmaps.GMapsTileLoader;
module.exports.GMapsTorqueLayer = gmaps.GMapsTorqueLayer;
module.exports.GMapsTiledTorqueLayer = gmaps.GMapsTiledTorqueLayer;
+
+require('./ol');
\ No newline at end of file
diff --git a/lib/torque/leaflet/canvas_layer.js b/lib/torque/leaflet/canvas_layer.js
index 32b15f6..64420ab 100644
--- a/lib/torque/leaflet/canvas_layer.js
+++ b/lib/torque/leaflet/canvas_layer.js
@@ -128,7 +128,7 @@ L.CanvasLayer = L.Class.extend({
var origin = {
x: newCenter.x - oldCenter.x + pos.x,
- y: newCenter.y - oldCenter.y + pos.y,
+ y: newCenter.y - oldCenter.y + pos.y
};
var bg = back;
diff --git a/lib/torque/leaflet/torque.js b/lib/torque/leaflet/torque.js
index b883db6..dc27b42 100644
--- a/lib/torque/leaflet/torque.js
+++ b/lib/torque/leaflet/torque.js
@@ -138,7 +138,7 @@ L.TorqueLayer = L.CanvasLayer.extend({
onAdd: function (map) {
map.on({
'zoomend': this._clearCaches,
- 'zoomstart': this._pauseOnZoom,
+ 'zoomstart': this._pauseOnZoom
}, this);
map.on({
@@ -152,7 +152,7 @@ L.TorqueLayer = L.CanvasLayer.extend({
this._removeTileLoader();
map.off({
'zoomend': this._clearCaches,
- 'zoomstart': this._pauseOnZoom,
+ 'zoomstart': this._pauseOnZoom
}, this);
map.off({
'zoomend': this._resumeOnZoom
diff --git a/lib/torque/ol/canvas_layer.js b/lib/torque/ol/canvas_layer.js
new file mode 100644
index 0000000..aad02be
--- /dev/null
+++ b/lib/torque/ol/canvas_layer.js
@@ -0,0 +1,131 @@
+require('./ol_tileloader_mixin');
+
+ol.CanvasLayer = function(options) {
+ this.root_ = document.createElement('div');
+ this.root_.setAttribute('class', 'ol-heatmap-layer');
+
+ this.options = {
+ subdomains: 'abc',
+ errorTileUrl: '',
+ attribution: '',
+ opacity: 1,
+ tileLoader: false, // installs tile loading events
+ tileSize: 256
+ };
+
+ options = options || {};
+ torque.extend(this.options, options);
+
+ ol.TileLoader.call(this, this.options.tileSize, this.options.maxZoom);
+
+ this.render = this.render.bind(this);
+ this._canvas = this._createCanvas();
+
+ this.root_.appendChild(this._canvas);
+
+ this._ctx = this._canvas.getContext('2d');
+ this.currentAnimationFrame = -1;
+ this.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame ||
+ window.webkitRequestAnimationFrame || window.msRequestAnimationFrame || function (callback) {
+ return window.setTimeout(callback, 1000 / 60);
+ };
+ this.cancelAnimationFrame = window.cancelAnimationFrame || window.mozCancelAnimationFrame ||
+ window.webkitCancelAnimationFrame || window.msCancelAnimationFrame || function (id) {
+ clearTimeout(id);
+ };
+
+ if(options.map){
+ this.setMap(options.map);
+ }
+};
+
+ol.inherits(ol.CanvasLayer, ol.TileLoader);
+
+ol.CanvasLayer.prototype.setMap = function(map){
+ if(this._map){
+ //remove
+ this._map.unByKey(this.pointdragKey_);
+ this._map.unByKey(this.sizeChangedKey_);
+ this._map.unByKey(this.moveendKey_);
+ this._map.getView().unByKey(this.centerChanged_);
+ }
+ this._map = map;
+
+ if(map){
+ var overlayContainer = this._map.getViewport().getElementsByClassName("ol-overlaycontainer")[0];
+ overlayContainer.appendChild(this.root_);
+
+ this.pointdragKey_ = map.on('pointerdrag', this._render, this);
+ this.moveendKey_ = map.on("moveend", this._render, this);
+ this.centerChanged_ = map.getView().on("change:center", this._render, this);
+ this.sizeChangedKey_ = map.on('change:size', this._reset, this);
+
+ if(this.options.tileLoader) {
+ ol.TileLoader.prototype._initTileLoader.call(this, map);
+ }
+ this._reset();
+ }
+};
+
+ol.CanvasLayer.prototype._createCanvas = function() {
+ var canvas;
+ canvas = document.createElement('canvas');
+ canvas.style.position = 'absolute';
+ canvas.style.top = 0;
+ canvas.style.left = 0;
+ canvas.style.pointerEvents = "none";
+ canvas.style.zIndex = this.options.zIndex || 0;
+ return canvas;
+};
+
+ol.CanvasLayer.prototype._reset = function () {
+ this._resize();
+};
+
+ol.CanvasLayer.prototype._resize = function() {
+ var size = this._map.getSize();
+ var width = size[0];
+ var height = size[1];
+ var oldWidth = this._canvas.width;
+ var oldHeight = this._canvas.height;
+
+ // resizing may allocate a new back buffer, so do so conservatively
+ if (oldWidth !== width || oldHeight !== height) {
+ this._canvas.width = width;
+ this._canvas.height = height;
+ this._canvas.style.width = width + 'px';
+ this._canvas.style.height = height + 'px';
+ this.root_.style.width = width + 'px';
+ this.root_.style.height = height + 'px';
+ this._render();
+ }
+};
+
+ol.CanvasLayer.prototype._render = function() {
+ if (this.currentAnimationFrame >= 0) {
+ this.cancelAnimationFrame.call(window, this.currentAnimationFrame);
+ }
+ this.currentAnimationFrame = this.requestAnimationFrame.call(window, this.render);
+};
+
+ol.CanvasLayer.prototype.getCanvas = function() {
+ return this._canvas;
+};
+
+ol.CanvasLayer.prototype.getAttribution = function() {
+ return this.options.attribution;
+};
+
+ol.CanvasLayer.prototype.draw = function() {
+ return this._render();
+};
+
+ol.CanvasLayer.prototype.redraw = function(direct) {
+ if (direct) {
+ this.render();
+ } else {
+ this._render();
+ }
+};
+
+module.exports = ol.CanvasLayer;
\ No newline at end of file
diff --git a/lib/torque/ol/index.js b/lib/torque/ol/index.js
new file mode 100644
index 0000000..87fb091
--- /dev/null
+++ b/lib/torque/ol/index.js
@@ -0,0 +1,3 @@
+if (typeof ol !== 'undefined') {
+ require('./torque');
+}
diff --git a/lib/torque/ol/ol_tileloader_mixin.js b/lib/torque/ol/ol_tileloader_mixin.js
new file mode 100644
index 0000000..7687211
--- /dev/null
+++ b/lib/torque/ol/ol_tileloader_mixin.js
@@ -0,0 +1,169 @@
+ol.TileLoader = function(tileSize, maxZoom){
+ this._tileSize = tileSize;
+ this._tiles = {};
+ this._tilesLoading = {};
+ this._tilesToLoad = 0;
+ this._updateTiles = this._updateTiles.bind(this);
+
+ this._tileGrid = ol.tilegrid.createXYZ({
+ maxZoom: maxZoom,
+ tileSize: tileSize
+ });
+};
+
+ol.TileLoader.prototype._initTileLoader = function(map) {
+ this._map = map;
+ this._view = map.getView();
+ this._centerChangedId = this._view.on("change:center", function(e){
+ this._updateTiles();
+ }, this);
+
+ this._postcomposeKey = undefined;
+
+ this._resolutionChangedId = this._view.on("change:resolution", function(evt){
+ this._currentResolution = this._view.getResolution();
+ if(this._postcomposeKey) return;
+ this.fire("mapZoomStart");
+ this._postcomposeKey = this._map.on("postcompose", function(evt) {
+ if(evt.frameState.viewState.resolution === this._currentResolution){
+ this._updateTiles();
+ this._map.unByKey(this._postcomposeKey);
+ this._postcomposeKey = undefined;
+ this.fire("mapZoomEnd");
+ }
+ }, this);
+ }, this);
+
+ this._updateTiles();
+};
+ol.TileLoader.prototype._removeTileLoader = function() {
+ this._view.unByKey(this._centerChangedId);
+ this._view.unByKey(this._resolutionChangedId );
+
+ this._removeTiles();
+};
+
+ol.TileLoader.prototype._removeTiles = function () {
+ for (var key in this._tiles) {
+ this._removeTile(key);
+ }
+};
+
+ol.TileLoader.prototype._reloadTiles = function() {
+ this._removeTiles();
+ this._updateTiles();
+};
+
+ol.TileLoader.prototype._updateTiles = function () {
+ if (!this._map) { return; }
+
+ var zoom = this._tileGrid.getZForResolution(this._view.getResolution());
+ var extent = this._view.calculateExtent(this._map.getSize());
+
+ var tileRange = this._requestTilesForExtentAndZ(extent, zoom);
+ this._removeOtherTiles(tileRange);
+};
+
+ol.TileLoader.prototype._removeOtherTiles = function(tileRange) {
+ var kArr, x, y, z, key;
+
+ var zoom = this._tileGrid.getZForResolution(this._view.getResolution());
+
+ for (key in this._tiles) {
+ if (this._tiles.hasOwnProperty(key)) {
+ kArr = key.split(':');
+ x = parseInt(kArr[0], 10);
+ y = parseInt(kArr[1], 10);
+ z = parseInt(kArr[2], 10);
+
+ // remove tile if it's out of bounds
+ if (z !== zoom || x < tileRange.minX || x > tileRange.maxX || ((-y-1) < tileRange.minY) || (-y-1) > tileRange.maxY) {
+ this._removeTile(key);
+ }
+ }
+ }
+};
+
+ol.TileLoader.prototype._removeTile = function (key) {
+ this.fire('tileRemoved', this._tiles[key]);
+ delete this._tiles[key];
+ delete this._tilesLoading[key];
+};
+
+ol.TileLoader.prototype._tileKey = function(tilePoint) {
+ return tilePoint.x + ':' + tilePoint.y + ':' + tilePoint.zoom;
+};
+
+ol.TileLoader.prototype._tileShouldBeLoaded = function (tilePoint) {
+ var k = this._tileKey(tilePoint);
+ return !(k in this._tiles) && !(k in this._tilesLoading);
+};
+
+ol.TileLoader.prototype._removeFromTilesLoading = function(tilePoint){
+ this._tilesToLoad--;
+ var k = this._tileKey(tilePoint);
+ delete this._tilesLoading[k];
+ if(this._tilesToLoad === 0) {
+ this.fire("tilesLoaded");
+ }
+};
+
+ol.TileLoader.prototype._tileLoaded = function(tilePoint, tileData) {
+ var k = this._tileKey(tilePoint);
+ this._tiles[k] = tileData;
+};
+
+ol.TileLoader.prototype.getTilePos = function (tilePoint) {
+ var zoom = this._tileGrid.getZForResolution(this._view.getResolution());
+ var extent = this._tileGrid.getTileCoordExtent([zoom, tilePoint.x, -tilePoint.y-1]);
+ var topLeft = this._map.getPixelFromCoordinate([extent[0], extent[3]]);
+
+ return {
+ x: topLeft[0],
+ y: topLeft[1]
+ };
+};
+
+ol.TileLoader.prototype._requestTilesForExtentAndZ = function (extent, zoom) {
+ var queue = [];
+ var tileCoords = [];
+
+ this._tileGrid.forEachTileCoord(extent, zoom, function(coord){
+ tileCoords.push(coord);
+ var point = {
+ x: coord[1],
+ y: -coord[2] - 1,
+ zoom: coord[0]
+ };
+
+ if (this._tileShouldBeLoaded(point)) {
+ queue.push(point);
+ }
+ }.bind(this));
+
+ var tilesToLoad = queue.length;
+ if (tilesToLoad > 0) {
+ this._tilesToLoad += tilesToLoad;
+
+ for (var i = 0; i < tilesToLoad; i++) {
+ var t = queue[i];
+ var k = this._tileKey(t);
+ this._tilesLoading[k] = t;
+ // events
+ this.fire('tileAdded', t);
+ }
+
+ this.fire("tilesLoading");
+ }
+
+ var tileRange = {
+ minX : tileCoords[0][1],
+ maxX : tileCoords [tileCoords.length - 1][1],
+ minY : tileCoords[0][2],
+ maxY : tileCoords [tileCoords.length - 1] [2]
+ };
+
+ return tileRange;
+};
+
+module.exports = ol.TileLoader;
diff --git a/lib/torque/ol/torque.js b/lib/torque/ol/torque.js
new file mode 100644
index 0000000..c6b1105
--- /dev/null
+++ b/lib/torque/ol/torque.js
@@ -0,0 +1,443 @@
+var carto = global.carto || require('carto');
+var torque = require('../');
+require('./canvas_layer');
+
+ol.TorqueLayer = function(options){
+ var self = this;
+ if (!torque.isBrowserSupported()) {
+ throw new Error("browser is not supported by torque");
+ }
+ options.tileLoader = true;
+ this.keys = [0];
+ Object.defineProperty(this, 'key', {
+ get: function() {
+ return this.getKey();
+ }
+ });
+ this.prevRenderedKey = 0;
+ if (options.cartocss) {
+ torque.extend(options, torque.common.TorqueLayer.optionsFromCartoCSS(options.cartocss));
+ }
+
+ options.resolution = options.resolution || 2;
+ options.steps = options.steps || 100;
+ options.visible = options.visible === undefined ? true: options.visible;
+ this.hidden = !options.visible;
+
+ this.animator = new torque.Animator(function(time) {
+ var k = time | 0;
+ if(self.getKey() !== k) {
+ self.setKey(k, { direct: true });
+ }
+ }, torque.extend(torque.clone(options), {
+ onPause: function() {
+ self.fire('pause');
+ },
+ onStop: function() {
+ self.fire('stop');
+ },
+ onStart: function() {
+ self.fire('play');
+ },
+ onStepsRange: function() {
+ self.fire('change:stepsRange', self.animator.stepsRange());
+ }
+ }));
+
+ this.play = this.animator.start.bind(this.animator);
+ this.stop = this.animator.stop.bind(this.animator);
+ this.pause = this.animator.pause.bind(this.animator);
+ this.toggle = this.animator.toggle.bind(this.animator);
+ this.setDuration = this.animator.duration.bind(this.animator);
+ this.isRunning = this.animator.isRunning.bind(this.animator);
+
+
+ ol.CanvasLayer.call(this, options);
+
+ this.options.renderer = this.options.renderer || 'point';
+ this.options.provider = this.options.provider || 'windshaft';
+
+ if (this.options.tileJSON) this.options.provider = 'tileJSON';
+
+ this.provider = new this.providers[this.options.provider](options);
+ this.renderer = new this.renderers[this.options.renderer](this.getCanvas(), options);
+
+ options.ready = function() {
+ self.fire("change:bounds", {
+ bounds: self.provider.getBounds()
+ });
+ self.animator.steps(self.provider.getSteps());
+ self.animator.rescale();
+ self.fire('change:steps', {
+ steps: self.provider.getSteps()
+ });
+ self.setKeys(self.getKeys());
+ };
+
+ this.renderer.on("allIconsLoaded", this.render.bind(this));
+
+
+ // for each tile shown on the map request the data
+ this.on('tileAdded', function(t) {
+ var tileData = this.provider.getTileData(t, t.zoom, function(tileData) {
+ self._removeFromTilesLoading(t);
+ if (t.zoom !== self._tileGrid.getZForResolution(self._view.getResolution())) return;
+ self._tileLoaded(t, tileData);
+ self.fire('tileLoaded');
+ if (tileData) {
+ self.redraw();
+ }
+ });
+ }, this);
+
+ this.on('mapZoomStart', function(){
+ this.getCanvas().style.display = "none";
+ this._pauseOnZoom();
+ }, this);
+
+ this.on('mapZoomEnd', function() {
+ this.getCanvas().style.display = "block";
+ this._resumeOnZoom();
+ }, this);
+};
+
+ol.TorqueLayer.prototype = torque.extend({},
+ ol.CanvasLayer.prototype,
+ torque.Event,
+ {
+ providers: {
+ 'sql_api': torque.providers.json,
+ 'url_template': torque.providers.JsonArray,
+ 'windshaft': torque.providers.windshaft,
+ 'tileJSON': torque.providers.tileJSON
+ },
+
+ renderers: {
+ 'point': torque.renderer.Point,
+ 'pixel': torque.renderer.Rectangle
+ },
+
+ onAdd: function(map){
+ ol.CanvasLayer.prototype.setMap.call(this, map);
+ },
+
+ onRemove: function(map) {
+ this.fire('remove');
+ this._removeTileLoader();
+ },
+
+ _pauseOnZoom: function() {
+ this.wasRunning = this.isRunning();
+ if (this.wasRunning) {
+ this.pause();
+ }
+ },
+
+ _resumeOnZoom: function() {
+ if (this.wasRunning) {
+ this.play();
+ }
+ },
+
+ hide: function() {
+ if(this.hidden) return this;
+ this.pause();
+ this.clear();
+ this.hidden = true;
+ return this;
+ },
+
+ show: function() {
+ if(!this.hidden) return this;
+ this.hidden = false;
+ this.play();
+ if (this.options.steps === 1){
+ this.redraw();
+ }
+ return this;
+ },
+
+ setSQL: function(sql) {
+ if (this.provider.options.named_map) throw new Error("SQL queries on named maps are read-only");
+ if (!this.provider || !this.provider.setSQL) {
+ throw new Error("this provider does not support SQL");
+ }
+ this.provider.setSQL(sql);
+ this._reloadTiles();
+ return this;
+ },
+
+ setBlendMode: function(_) {
+ this.renderer.setBlendMode(_);
+ this.redraw();
+ },
+
+ setSteps: function(steps) {
+ this.provider.setSteps(steps);
+ this._reloadTiles();
+ },
+
+ setColumn: function(column, isTime) {
+ this.provider.setColumn(column, isTime);
+ this._reloadTiles();
+ },
+
+ getTimeBounds: function() {
+ return this.provider && this.provider.getKeySpan();
+ },
+
+ clear: function() {
+ var canvas = this.getCanvas();
+ canvas.width = canvas.width;
+ },
+
+ /**
+ * render the selectef key
+ * don't call this function directly, it's called by
+ * requestAnimationFrame. Use redraw to refresh it
+ */
+ render: function() {
+ if(this.hidden) return;
+ var t, tile, pos;
+ var canvas = this.getCanvas();
+ this.renderer.clearCanvas();
+ var ctx = canvas.getContext('2d');
+
+ // renders only a "frame"
+ for(t in this._tiles) {
+ tile = this._tiles[t];
+ if (tile) {
+ pos = this.getTilePos(tile.coord);
+ ctx.setTransform(1, 0, 0, 1, pos.x, pos.y);
+ this.renderer.renderTile(tile, this.keys);
+ }
+ }
+ this.renderer.applyFilters();
+ },
+
+ /**
+ * set key to be shown. If it's a single value
+ * it renders directly, if it's an array it renders
+ * accumulated
+ */
+ setKey: function(key, options) {
+ this.setKeys([key], options);
+ },
+
+ /**
+ * returns the array of keys being rendered
+ */
+ getKeys: function() {
+ return this.keys;
+ },
+
+ setKeys: function(keys, options) {
+ this.keys = keys;
+ this.animator.step(this.getKey());
+ this.redraw(options && options.direct);
+ this.fire('change:time', {
+ time: this.getTime(),
+ step: this.getKey(),
+ start: this.getKey(),
+ end: this.getLastKey()
+ });
+ },
+
+ getKey: function() {
+ return this.keys[0];
+ },
+
+ getLastKey: function() {
+ return this.keys[this.keys.length - 1];
+ },
+
+ /**
+ * helper function, does the same than ``setKey`` but only
+ * accepts scalars.
+ */
+ setStep: function(time) {
+ if(time === undefined || time.length !== undefined) {
+ throw new Error("setTime only accept scalars");
+ }
+ this.setKey(time);
+ },
+
+ renderRange: function(start, end) {
+ this.pause();
+ var keys = [];
+ for (var i = start; i <= end; i++) {
+ keys.push(i);
+ }
+ this.setKeys(keys);
+ },
+
+ resetRenderRange: function() {
+ this.stop();
+ this.play();
+ },
+
+ /**
+ * transform from animation step to Date object
+ * that contains the animation time
+ *
+ * ``step`` should be between 0 and ``steps - 1``
+ */
+ stepToTime: function(step) {
+ var times = this.provider.getKeySpan();
+ var time = times.start + (times.end - times.start)*(step/this.provider.getSteps());
+ return new Date(time);
+ },
+
+ timeToStep: function(timestamp) {
+ if (typeof timestamp === "Date") timestamp = timestamp.getTime();
+ if (!this.provider) return 0;
+ var times = this.provider.getKeySpan();
+ var step = (this.provider.getSteps() * (timestamp - times.start)) / (times.end - times.start);
+ return step;
+ },
+
+ getStep: function() {
+ return this.getKey();
+ },
+
+ /**
+ * returns the animation time defined by the data
+ * in the defined column. Date object
+ */
+ getTime: function() {
+ return this.stepToTime(this.getKey());
+ },
+
+ /**
+ * returns an object with the start and end times
+ */
+ getTimeSpan: function() {
+ return this.provider.getKeySpan();
+ },
+
+ /**
+ * set the cartocss for the current renderer
+ */
+ setCartoCSS: function(cartocss) {
+ if (this.provider.options.named_map) throw new Error("CartoCSS style on named maps is read-only");
+ if (!this.renderer) throw new Error('renderer is not valid');
+ var shader = new carto.RendererJS().render(cartocss);
+ this.renderer.setShader(shader);
+
+ // provider options
+ var options = torque.common.TorqueLayer.optionsFromLayer(shader.findLayer({ name: 'Map' }));
+ this.provider.setCartoCSS && this.provider.setCartoCSS(cartocss);
+ if(this.provider.setOptions(options)) {
+ this._reloadTiles();
+ }
+
+ torque.extend(this.options, options);
+
+ // animator options
+ if (options.animationDuration) {
+ this.animator.duration(options.animationDuration);
+ }
+ this.redraw();
+ return this;
+ },
+
+ /**
+ * get active points for a step in active zoom
+ * returns a list of bounding boxes [[] , [], []]
+ * empty list if there is no active pixels
+ */
+ getActivePointsBBox: function(step) {
+ var positions = [];
+ for(var t in this._tiles) {
+ var tile = this._tiles[t];
+ positions = positions.concat(this.renderer.getActivePointsBBox(tile, step));
+ }
+ return positions;
+ },
+
+ /**
+ * return an array with the values for all the pixels active for the step
+ */
+ getValues: function(step) {
+ var values = [];
+ step = step === undefined ? this.getKey(): step;
+ var t, tile;
+ for(t in this._tiles) {
+ tile = this._tiles[t];
+ this.renderer.getValues(tile, step, values);
+ }
+ return values;
+ },
+
+ /**
+ * return the value for position relative to map coordinates. null for no value
+ */
+ getValueForPos: function(x, y, step) {
+ step = step === undefined ? this.getKey(): step;
+ var t, tile, pos, value = null, xx, yy;
+ for(t in this._tiles) {
+ tile = this._tiles[t];
+ pos = this.getTilePos(tile.coord);
+ xx = x - pos.x;
+ yy = y - pos.y;
+ if (xx >= 0 && yy >= 0 && xx < this.renderer.TILE_SIZE && yy <= this.renderer.TILE_SIZE) {
+ value = this.renderer.getValueFor(tile, step, xx, yy);
+ }
+ if (value !== null) {
+ return value;
+ }
+ }
+ return null;
+ },
+
+ getValueForBBox: function(x, y, w, h) {
+ var xf = x + w, yf = y + h, _x=x;
+ var sum = 0;
+ for(_y = y; _y