From 46e9839da1fe61d3f073ad123a71f1d89fa2a3b8 Mon Sep 17 00:00:00 2001 From: Anna Shchurova Date: Mon, 18 Jul 2016 11:36:17 -0400 Subject: [PATCH] Fix #263 Implementation of torque layer for OpenLayers. Must be using OpenLayers 3.17 or later. --- Makefile | 1 + examples/navy_ol.html | 73 +++++ lib/torque/index.js | 2 + lib/torque/leaflet/canvas_layer.js | 2 +- lib/torque/leaflet/torque.js | 4 +- lib/torque/ol/canvas_layer.js | 131 ++++++++ lib/torque/ol/index.js | 3 + lib/torque/ol/ol_tileloader_mixin.js | 169 ++++++++++ lib/torque/ol/torque.js | 443 +++++++++++++++++++++++++++ 9 files changed, 825 insertions(+), 3 deletions(-) create mode 100644 examples/navy_ol.html create mode 100644 lib/torque/ol/canvas_layer.js create mode 100644 lib/torque/ol/index.js create mode 100644 lib/torque/ol/ol_tileloader_mixin.js create mode 100644 lib/torque/ol/torque.js 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