diff --git a/lib/torque/leaflet/torque.js b/lib/torque/leaflet/torque.js index 9f8d8c7..155ea80 100644 --- a/lib/torque/leaflet/torque.js +++ b/lib/torque/leaflet/torque.js @@ -226,10 +226,11 @@ L.TorqueLayer = L.CanvasLayer.extend({ // all the points this.renderer._ctx.drawImage(tile._tileCache, 0, 0); } else { - this.renderer.renderTile(tile, this.key); + this.renderer.renderTile(tile, this.key, pos); } } } + this.renderer.flush(); // prepare caches if the animation is not running // don't cache if the key has just changed, this avoids to cache diff --git a/lib/torque/renderer/point.js b/lib/torque/renderer/point.js index 5130133..6059071 100644 --- a/lib/torque/renderer/point.js +++ b/lib/torque/renderer/point.js @@ -2,6 +2,7 @@ var torque = require('../'); var cartocss = require('./cartocss_render'); var Profiler = require('../profiler'); var carto = global.carto || require('carto'); +var heat = require('./simpleheat'); var TAU = Math.PI * 2; var DEFAULT_CARTOCSS = [ @@ -47,6 +48,8 @@ var carto = global.carto || require('carto'); this.options = options; this._canvas = canvas; this._ctx = canvas.getContext('2d'); + this._heat = heat(this._canvas); + this._heat.max(255); this._sprites = []; // sprites per layer this._shader = null; this.setCartoCSS(this.options.cartocss || DEFAULT_CARTOCSS); @@ -144,7 +147,8 @@ var carto = global.carto || require('carto'); // renders all the layers (and frames for each layer) from cartocss // renderTile: function(tile, key) { - var prof = Profiler.metric('torque.renderer.point.renderLayers').start(); + this._renderTile(tile, 0, 0, null, null); + /*var prof = Profiler.metric('torque.renderer.point.renderLayers').start(); var layers = this._shader.getLayers(); for(var i = 0, n = layers.length; i < n; ++i ) { var layer = layers[i]; @@ -159,6 +163,7 @@ var carto = global.carto || require('carto'); } } prof.end(true); + */ }, _createCanvas: function() { @@ -178,41 +183,35 @@ var carto = global.carto || require('carto'); // the torque tile // _renderTile: function(tile, key, frame_offset, sprites, shader, shaderVars) { - if(!this._canvas) return; + if(!this._canvas || !this._heat) return; var prof = Profiler.metric('torque.renderer.point.renderTile').start(); var ctx = this._ctx; - var blendMode = compop2canvas(shader.eval('comp-op')) || this.options.blendmode; - if(blendMode) { - ctx.globalCompositeOperation = blendMode; - } - if (this.options.cumulative && key > tile.maxDate) { - //TODO: precache because this tile is not going to change - key = tile.maxDate; - } + var tileMax = this.options.resolution * (this.TILE_SIZE/this.options.resolution - 1) var activePixels = tile.timeCount[key]; - if(activePixels) { + if (activePixels) { var pixelIndex = tile.timeIndex[key]; for(var p = 0; p < activePixels; ++p) { var posIdx = tile.renderDataPos[pixelIndex + p]; var c = tile.renderData[pixelIndex + p]; if(c) { - var sp = sprites[c]; - if(sp === undefined) { - sp = sprites[c] = this.generateSprite(shader, c, torque.extend({ zoom: tile.z, 'frame-offset': frame_offset }, shaderVars)); - } - if (sp) { - var x = tile.x[posIdx]- (sp.width >> 1); + var x = tile.x[posIdx]; var y = tileMax - tile.y[posIdx]; // flip mercator - ctx.drawImage(sp, x, y - (sp.height >> 1)); - } + this._heat.add([x, y, c]); + this._ctx.fillRect(x, y, 2, 2); } } } prof.end(true); }, + flush: function() { + if(!this._heat) return; + this._heat.draw(); + this._heat.clear(); + }, + setBlendMode: function(b) { this.options.blendmode = b; }, diff --git a/lib/torque/renderer/simpleheat.js b/lib/torque/renderer/simpleheat.js new file mode 100644 index 0000000..1b8e5c1 --- /dev/null +++ b/lib/torque/renderer/simpleheat.js @@ -0,0 +1,139 @@ +/* + (c) 2014, Vladimir Agafonkin + simpleheat, a tiny JavaScript library for drawing heatmaps with Canvas + https://github.com/mourner/simpleheat +*/ + +'use strict'; + +function simpleheat(canvas) { + // jshint newcap: false, validthis: true + if (!(this instanceof simpleheat)) { return new simpleheat(canvas); } + + this._canvas = canvas = typeof canvas === 'string' ? document.getElementById(canvas) : canvas; + + this._ctx = canvas.getContext('2d'); + this._width = canvas.width; + this._height = canvas.height; + + this._max = 1; + this._data = []; +} + +simpleheat.prototype = { + + defaultRadius: 25, + + defaultGradient: { + 0.4: 'blue', + 0.6: 'cyan', + 0.7: 'lime', + 0.8: 'yellow', + 1.0: 'red' + }, + + data: function (data) { + this._data = data; + return this; + }, + + max: function (max) { + this._max = max; + return this; + }, + + add: function (point) { + this._data.push(point); + return this; + }, + + clear: function () { + this._data = []; + return this; + }, + + radius: function (r, blur) { + blur = blur || 15; + + // create a grayscale blurred circle image that we'll use for drawing points + var circle = this._circle = document.createElement('canvas'), + ctx = circle.getContext('2d'), + r2 = this._r = r + blur; + + circle.width = circle.height = r2 * 2; + + ctx.shadowOffsetX = ctx.shadowOffsetY = 200; + ctx.shadowBlur = blur; + ctx.shadowColor = 'black'; + + ctx.beginPath(); + ctx.arc(r2 - 200, r2 - 200, r, 0, Math.PI * 2, true); + ctx.closePath(); + ctx.fill(); + + return this; + }, + + gradient: function (grad) { + // create a 256x1 gradient that we'll use to turn a grayscale heatmap into a colored one + var canvas = document.createElement('canvas'), + ctx = canvas.getContext('2d'), + gradient = ctx.createLinearGradient(0, 0, 0, 256); + + canvas.width = 1; + canvas.height = 256; + + for (var i in grad) { + gradient.addColorStop(i, grad[i]); + } + + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, 1, 256); + + this._grad = ctx.getImageData(0, 0, 1, 256).data; + + return this; + }, + + draw: function (minOpacity) { + if (!this._circle) { + this.radius(this.defaultRadius); + } + if (!this._grad) { + this.gradient(this.defaultGradient); + } + + var ctx = this._ctx; + + //ctx.clearRect(0, 0, this._width, this._height); + + // draw a grayscale heatmap by putting a blurred circle at each data point + for (var i = 0, len = this._data.length, p; i < len; i++) { + p = this._data[i]; + + ctx.globalAlpha = Math.max(p[2] / this._max, minOpacity === undefined ? 0.05 : minOpacity); + ctx.drawImage(this._circle, p[0] - this._r, p[1] - this._r); + } + + // colorize the heatmap, using opacity value of each pixel to get the right color from our gradient + //var colored = ctx.getImageData(0, 0, this._width, this._height); + //this._colorize(colored.data, this._grad); + //ctx.putImageData(colored, 0, 0); + + return this; + }, + + _colorize: function (pixels, gradient) { + for (var i = 3, len = pixels.length, j; i < len; i += 4) { + j = pixels[i] * 4; // get gradient color from opacity value + + if (j) { + pixels[i - 3] = gradient[j]; + pixels[i - 2] = gradient[j + 1]; + pixels[i - 1] = gradient[j + 2]; + } + } + } +}; + +module.exports = simpleheat;