diff --git a/src/canvas/heatmapFeature.js b/src/canvas/heatmapFeature.js index 68a96fa903..a9a301a021 100644 --- a/src/canvas/heatmapFeature.js +++ b/src/canvas/heatmapFeature.js @@ -1,3 +1,7 @@ +var inherit = require('../inherit'); +var registerFeature = require('../registry').registerFeature; +var heatmapFeature = require('../heatmapFeature'); + ////////////////////////////////////////////////////////////////////////////// /** * Create a new instance of class heatmapFeature @@ -7,16 +11,16 @@ * @class * @param {Object} arg Options object * @extends geo.heatmapFeature - * @returns {geo.canvas.heatmapFeature} + * @returns {canvas_heatmapFeature} */ ////////////////////////////////////////////////////////////////////////////// -geo.canvas.heatmapFeature = function (arg) { +canvas_heatmapFeature = function (arg) { 'use strict'; - if (!(this instanceof geo.canvas.heatmapFeature)) { - return new geo.canvas.heatmapFeature(arg); + if (!(this instanceof canvas_heatmapFeature)) { + return new canvas_heatmapFeature(arg); } - geo.heatmapFeature.call(this, arg); + heatmapFeature.call(this, arg); //////////////////////////////////////////////////////////////////////////// /** @@ -51,7 +55,7 @@ geo.canvas.heatmapFeature = function (arg) { c.hasOwnProperty('g') && c.hasOwnProperty('b') && c.hasOwnProperty('a')) { - color = 'rgba('+255 * c.r+','+255 * c.g+','+255 * c.b+','+255 * c.a+')'; + color = 'rgba('+255 * c.r+','+255 * c.g+','+255 * c.b+','+ c.a+')'; } return color; }; @@ -96,10 +100,9 @@ geo.canvas.heatmapFeature = function (arg) { var circle, ctx, r, r2; if (!m_this._circle) { circle = m_this._circle = document.createElement('canvas'), - ctx = circle.getContext('2d'), - r = m_this.style('radius'), - blur = m_this.style('blurRadius'); - + ctx = circle.getContext('2d'), + r = m_this.style('radius'), + blur = m_this.style('blurRadius'); r2 = blur + r; @@ -151,7 +154,8 @@ geo.canvas.heatmapFeature = function (arg) { m_this._computeGradient(); data.forEach(function (d) { pos = m_this.layer().map().gcsToDisplay(m_this.position()(d)); - intensity = m_this.intensity()(d) / m_this.maxIntensity(); + intensity = (m_this.intensity()(d) - m_this.minIntensity()) / + (m_this.maxIntensity() - m_this.minIntensity()); // Small values are not visible because globalAlpha < .01 // cannot be read from imageData context2d.globalAlpha = intensity < 0.01 ? 0.01 : intensity; @@ -171,7 +175,7 @@ geo.canvas.heatmapFeature = function (arg) { */ //////////////////////////////////////////////////////////////////////////// this._init = function () { - s_init.call(m_this, arg); //doesn't this get called implicitly by geo.heatmapFeature.call TODO ? + s_init.call(m_this, arg); return m_this; }; @@ -205,7 +209,8 @@ geo.canvas.heatmapFeature = function (arg) { return this; }; -inherit(geo.canvas.heatmapFeature, geo.heatmapFeature); +inherit(canvas_heatmapFeature, heatmapFeature); // Now register it -geo.registerFeature('canvas', 'heatmap', geo.canvas.heatmapFeature); +registerFeature('canvas', 'heatmap', canvas_heatmapFeature); +module.exports = canvas_heatmapFeature; \ No newline at end of file diff --git a/src/canvas/index.js b/src/canvas/index.js index 349a21f152..ad55e55bb2 100644 --- a/src/canvas/index.js +++ b/src/canvas/index.js @@ -4,5 +4,6 @@ module.exports = { canvasRenderer: require('./canvasRenderer'), quadFeature: require('./quadFeature'), + heatmapFeature: require('./heatmapFeature'), tileLayer: require('./tileLayer') }; diff --git a/src/core/heatmapFeature.js b/src/heatmapFeature.js similarity index 78% rename from src/core/heatmapFeature.js rename to src/heatmapFeature.js index a349f794cf..a93f1be10b 100644 --- a/src/core/heatmapFeature.js +++ b/src/heatmapFeature.js @@ -1,12 +1,6 @@ -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class heatmapFeature - * - * @class - * @extends geo.feature - * @returns {geo.heatmapFeature} - * - */ +var $ = require('jquery'); +var inherit = require('./inherit'); +var feature = require('./feature'); ////////////////////////////////////////////////////////////////////////////// /** @@ -34,13 +28,13 @@ ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// -geo.heatmapFeature = function (arg) { +var heatmapFeature = function (arg) { 'use strict'; - if (!(this instanceof geo.heatmapFeature)){ - return new geo.heatmapFeature(arg); + if (!(this instanceof heatmapFeature)){ + return new heatmapFeature(arg); } arg = arg || {}; - geo.feature.call(this, arg); + feature.call(this, arg); //////////////////////////////////////////////////////////////////////////// /** @@ -51,12 +45,14 @@ geo.heatmapFeature = function (arg) { m_position, m_intensity, m_maxIntensity, + m_minIntensity, s_init = this._init, s_data = this.data; m_position = arg.position || function (d) { return d; }; m_intensity = arg.intensity || function (d) { return 1; }; m_maxIntensity = arg.maxIntensity || 1; + m_minIntensity = arg.minIntensity ? arg.minIntensity : 0; //////////////////////////////////////////////////////////////////////////// /** @@ -76,6 +72,24 @@ geo.heatmapFeature = function (arg) { return m_this; }; + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set maxIntensity + * + * @returns {geo.heatmap} + */ + //////////////////////////////////////////////////////////////////////////// + this.minIntensity = function (val) { + if (val === undefined) { + return m_minIntensity; + } else { + m_minIntensity = val; + m_this.dataTime().modified(); + m_this.modified(); + } + return m_this; + }; + //////////////////////////////////////////////////////////////////////////// /** * Get/Set position accessor @@ -126,12 +140,11 @@ geo.heatmapFeature = function (arg) { opacity: 0.1, radius: 10, blurRadius: 10, - blur: 'Gaussian', color: {0: {r: 0, g: 0, b: 0.0, a: 0.0}, - .25: {r: 0, g: 0, b: 1, a: 1.0}, - .5: {r: 0, g: 1, b: 1, a: 1.0}, - .75: {r: 1, g: 1, b: 0, a: 1.0}, - 1: {r: 1, g: 0, b: 0, a: 1.0}} + .25: {r: 0, g: 0, b: 1, a: 0.5}, + .5: {r: 0, g: 1, b: 1, a: 0.6}, + .75: {r: 1, g: 1, b: 0, a: 0.7}, + 1: {r: 1, g: 0, b: 0, a: 0.8}} }, arg.style === undefined ? {} : arg.style ); @@ -148,4 +161,5 @@ geo.heatmapFeature = function (arg) { }; -inherit(geo.heatmapFeature, geo.feature) +inherit(heatmapFeature, feature) +module.exports = heatmapFeature; diff --git a/src/index.js b/src/index.js index 2ffb0e1773..9a5e2282a6 100644 --- a/src/index.js +++ b/src/index.js @@ -52,6 +52,7 @@ module.exports = $.extend({ pointFeature: require('./pointFeature'), polygonFeature: require('./polygonFeature'), quadFeature: require('./quadFeature'), + heatmapFeature: require('./heatmapFeature'), renderer: require('./renderer'), sceneObject: require('./sceneObject'), tile: require('./tile'), diff --git a/tests/cases/heatmap.js b/tests/cases/heatmap.js new file mode 100644 index 0000000000..458fd3beaf --- /dev/null +++ b/tests/cases/heatmap.js @@ -0,0 +1,94 @@ +// Test geo.core.osmLayer +var geo = require('../test-utils').geo; +var $ = require('jquery'); + +beforeEach(function () { + $('
').appendTo('body') + .css({width: '500px', height: '400px'}); +}); + +afterEach(function () { + $('#map-canvas-heatmap-feature').remove(); +}); + +describe('canvas heatmap feature', function () { + 'use strict'; + + var mockAnimationFrame = require('../test-utils').mockAnimationFrame; + var stepAnimationFrame = require('../test-utils').stepAnimationFrame; + var unmockAnimationFrame = require('../test-utils').unmockAnimationFrame; + + var map, width = 800, height = 600, layer, feature1, feature2, + testData = "Strength, Lat,Lon\ + 0.6,42.8584,-70.9301\ + 0.233,42.2776,-83.7409\ + 0.2,42.2776,-83.7409,"; + testData = testData.split(/\r\n|\n|\r/); + testData = testData.map( function (r) { + var fields = r.split(','); + return [fields[12], fields[24], fields[25]].map(parseFloat); + }); + testData.splice(0, 1); + + it('Setup map', function () { + map = geo.map({node: '#map-canvas-heatmap-feature', center: [0, 0], zoom: 3}); + layer = map.createLayer('feature', {'renderer': 'canvas'}); + map.resize(0, 0, width, height); + }); + + it('Add features to a layer', function () { + feature1 = layer.createFeature('heatmap') + .data(testData) + .intensity(function (d) { + return d[0]; + }) + .position(function (d) { + return { + x: d[2], + y: d[1] + }; + }) + .style('radius', 5) + .style('blurRadius', 15) + .style('opacity', 1.0); + + mockAnimationFrame(); + map.draw(); + stepAnimationFrame(new Date().getTime()); + expect(layer.children().length).toBe(1) + unmockAnimationFrame(); + }); + + it('Validate selection API option', function () { + expect(feature1.selectionAPI()).toBe(false); + }); + + it('Validate position', function () { + expect(feature1.position()([0.6, 42.8584, -70.9301])) + .toEqual({x:-70.9301, y:42.8584}); + }); + + it('Validate maximum intensity', function () { + expect(feature1.maxIntensity()).toBe(1); + }); + + it('Validate minimum intensity', function () { + expect(feature1.minIntensity()).toBe(0); + }); + + it('Remove a feature from a layer', function () { + layer.deleteFeature(feature1).draw(); + expect(layer.children().length).toBe(0); + }); + + it('Compute gradient', function () { + feature1.style("color", {0: {r: 0, g: 0, b: 0.0, a: 0.0}, + 0.25: {r: 0, g: 0, b: 1, a: 0.5}, + 0.5: {r: 0, g: 1, b: 1, a: 0.6}, + 0.75: {r: 1, g: 1, b: 0, a: 0.7}, + 1: {r: 1, g: 0, b: 0, a: 0.1}}); + feature1._computeGradient(); + expect(layer.node()[0].children[0].getContext('2d'). + getImageData(1, 0, 1, 1).data.length).toBe(4) + }); +});