From e9592b2d785a5dd45b830f0fe7a1765711606dde Mon Sep 17 00:00:00 2001 From: Aashish Chaudhary Date: Wed, 20 Apr 2016 00:07:59 -0400 Subject: [PATCH 01/16] Using typed array to increase performance --- src/canvas/heatmapFeature.js | 65 ++++++++++++++++++++++++++++-------- 1 file changed, 52 insertions(+), 13 deletions(-) diff --git a/src/canvas/heatmapFeature.js b/src/canvas/heatmapFeature.js index 54fc3476f9..2f5113fe3a 100644 --- a/src/canvas/heatmapFeature.js +++ b/src/canvas/heatmapFeature.js @@ -28,6 +28,9 @@ var canvas_heatmapFeature = function (arg) { */ //////////////////////////////////////////////////////////////////////////// var m_this = this, + m_typedBuffer = null, + m_typedClampedBuffer = null, + m_typedBufferData = null, s_exit = this._exit, s_init = this._init, s_update = this._update; @@ -116,19 +119,55 @@ var canvas_heatmapFeature = function (arg) { * @protected */ //////////////////////////////////////////////////////////////////////////// - this._colorize = function (pixels, gradient) { - var i, j; - for (i = 0; i < pixels.length; i += 4) { - // Get opacity from the temporary canvas image, - // then multiply by 4 to get the color index on linear gradient - j = pixels[i + 3] * 4; - if (j) { - pixels[i] = gradient[j]; - pixels[i + 1] = gradient[j + 1]; - pixels[i + 2] = gradient[j + 2]; - pixels[i + 3] = m_this.style('opacity') * gradient[j + 3]; + this._colorize = function (context2d, width, height, imageData, gradient) { + var isLittleEndian = true, i, j, index; + + // Determine whether Uint32 is little- or big-endian. + if (!m_typedBuffer || (m_typedBuffer.length != imageData.data.length)) { + m_typedBuffer = new ArrayBuffer(imageData.data.length), + m_typedClampedBuffer = new Uint8ClampedArray(m_typedBuffer), + m_typedBufferData = new Uint32Array(m_typedBuffer); + } + + m_typedBufferData[1] = 0x0a0b0c0d; + + isLittleEndian = true; + if (m_typedBuffer[4] === 0x0a && + m_typedBuffer[5] === 0x0b && + m_typedBuffer[6] === 0x0c && + m_typedBuffer[7] === 0x0d) { + isLittleEndian = false; + } + + if (isLittleEndian) { + i = 0; + for (j = 0; j < (width * height * 4); j += 4) { + index = imageData.data[j + 3] * 4; + if (index) { + m_typedBufferData[i] = + (gradient[index + 3] << 24) | + (gradient[index + 2] << 16) | + (gradient[index + 1] << 8) | + gradient[index]; + } + i += 1; + } + } else { + i = 0; + for (j = 0; j < (width * height * 4); j += 4) { + index = imageData.data[j + 3] * 4; + if (index) { + m_typedBufferData[i] = + (gradient[index] << 24) | + (gradient[index + 1] << 16) | + (gradient[index + 2] << 8) | + gradient[index + 3]; + } } } + + imageData.data.set(m_typedClampedBuffer); + context2d.putImageData(imageData, 0, 0); }; //////////////////////////////////////////////////////////////////////////// @@ -141,6 +180,7 @@ var canvas_heatmapFeature = function (arg) { var data = m_this.data() || [], radius = m_this.style('radius') + m_this.style('blurRadius'), pos, intensity, canvas, pixelArray; + m_this._createCircle(); m_this._computeGradient(); data.forEach(function (d) { @@ -154,8 +194,7 @@ var canvas_heatmapFeature = function (arg) { }); canvas = m_this.layer().canvas()[0]; pixelArray = context2d.getImageData(0, 0, canvas.width, canvas.height); - m_this._colorize(pixelArray.data, m_this._grad); - context2d.putImageData(pixelArray, 0, 0); + m_this._colorize(context2d, canvas.width, canvas.height, pixelArray, m_this._grad); return m_this; }; From 19557f8d23fda51142fe45d1cf76037c53af5721 Mon Sep 17 00:00:00 2001 From: Aashish Chaudhary Date: Thu, 21 Apr 2016 11:03:44 -0400 Subject: [PATCH 02/16] Using event data for transformation --- src/canvas/canvasRenderer.js | 13 ++++- src/canvas/heatmapFeature.js | 101 +++++++++++++++++++++++++++++------ 2 files changed, 95 insertions(+), 19 deletions(-) diff --git a/src/canvas/canvasRenderer.js b/src/canvas/canvasRenderer.js index c24338799e..3029aa43ca 100644 --- a/src/canvas/canvasRenderer.js +++ b/src/canvas/canvasRenderer.js @@ -25,9 +25,15 @@ var canvasRenderer = function (arg) { var m_this = this, m_renderAnimFrameRef = null, + m_clearCanvas = true, s_init = this._init, s_exit = this._exit; + + this.clearCanvas = function(arg) { + m_clearCanvas = arg; + } + //////////////////////////////////////////////////////////////////////////// /** * Get API used by the renderer @@ -91,9 +97,12 @@ var canvasRenderer = function (arg) { map = layer.map(), camera = map.camera(), viewport = camera._viewport; + // Clear the canvas. - m_this.context2d.setTransform(1, 0, 0, 1, 0, 0); - m_this.context2d.clearRect(0, 0, viewport.width, viewport.height); + if (m_clearCanvas) { + m_this.context2d.setTransform(1, 0, 0, 1, 0, 0); + m_this.context2d.clearRect(0, 0, viewport.width, viewport.height); + } var features = layer.features(); for (var i = 0; i < features.length; i += 1) { diff --git a/src/canvas/heatmapFeature.js b/src/canvas/heatmapFeature.js index 2f5113fe3a..7fe6e9fe3c 100644 --- a/src/canvas/heatmapFeature.js +++ b/src/canvas/heatmapFeature.js @@ -1,6 +1,7 @@ var inherit = require('../inherit'); var registerFeature = require('../registry').registerFeature; var heatmapFeature = require('../heatmapFeature'); +var timestamp = require('../timestamp'); ////////////////////////////////////////////////////////////////////////////// /** @@ -27,13 +28,22 @@ var canvas_heatmapFeature = function (arg) { * @private */ //////////////////////////////////////////////////////////////////////////// + var geo_event = require('../event'); + var m_this = this, m_typedBuffer = null, m_typedClampedBuffer = null, m_typedBufferData = null, + m_heatMapZoom, + m_lastZoom, + m_lastScale = 0, s_exit = this._exit, s_init = this._init, - s_update = this._update; + s_update = this._update, + m_currentX = 0, + m_currentY = 0, + m_renderTime = timestamp(), + m_translate; //////////////////////////////////////////////////////////////////////////// /** @@ -75,7 +85,7 @@ var canvas_heatmapFeature = function (arg) { gradient.addColorStop(stop, m_this._convertColor(colors[stop])); } - context2d.fillStyle = gradient; + context2d.fillStyle = gradient; context2d.fillRect(0, 0, 1, 256); m_this._grad = context2d.getImageData(0, 0, 1, 256).data; } @@ -181,20 +191,32 @@ var canvas_heatmapFeature = function (arg) { radius = m_this.style('radius') + m_this.style('blurRadius'), pos, intensity, canvas, pixelArray; - m_this._createCircle(); - m_this._computeGradient(); - data.forEach(function (d) { - pos = m_this.layer().map().gcsToDisplay(m_this.position()(d)); - 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; - context2d.drawImage(m_this._circle, pos.x - radius, pos.y - radius); - }); - canvas = m_this.layer().canvas()[0]; - pixelArray = context2d.getImageData(0, 0, canvas.width, canvas.height); - m_this._colorize(context2d, canvas.width, canvas.height, pixelArray, m_this._grad); + if (m_renderTime.getMTime() < m_this.buildTime().getMTime()) { + m_this._createCircle(); + m_this._computeGradient(); + data.forEach(function (d) { + pos = m_this.layer().map().gcsToDisplay(m_this.position()(d)); + 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; + context2d.drawImage(m_this._circle, pos.x - radius, pos.y - radius); + }); + canvas = m_this.layer().canvas()[0]; + pixelArray = context2d.getImageData(0, 0, canvas.width, canvas.height); + m_this._colorize(context2d, canvas.width, canvas.height, pixelArray, m_this._grad); + + m_heatMapZoom = m_this.layer().map().zoom(); + m_translate = {x: 0, y: 0}; + m_lastZoom = null; + } + + + m_renderTime.modified(); + + m_this.layer().renderer().clearCanvas(false); + return m_this; }; @@ -206,6 +228,9 @@ var canvas_heatmapFeature = function (arg) { //////////////////////////////////////////////////////////////////////////// this._init = function () { s_init.call(m_this, arg); + + m_this.geoOn(geo_event.pan, m_this._animatePan); + return m_this; }; @@ -225,6 +250,48 @@ var canvas_heatmapFeature = function (arg) { return m_this; }; + //////////////////////////////////////////////////////////////////////////// + /** + * Animate pan (and zoom) + * @protected + */ + //////////////////////////////////////////////////////////////////////////// + this._animatePan = function (e) { + + var zoom = m_this.layer().map().zoom(), + scale = Math.pow(2, (zoom - m_heatMapZoom)); + + if (!e.screenDelta) { + return + } + + var translate = {x: e.screenDelta.x, + y: e.screenDelta.y}; + + console.log(e); + console.log(e.screenDelta); + + if (zoom !== m_lastZoom && translate.x !== m_translate.x && + translate.y !== m_translate.y) { + var transform = 'translate(' + translate.x + 'px' + ',' + + translate.y + 'px' + ')' + 'scale(' + scale + ')'; + + m_this.layer().canvas().css('transform-origin', '50% 50%'); + m_this.layer().canvas().css('transform', transform); + + m_translate = translate; + m_lastZoom = zoom; + } + + // if (zoom !== m_heatMapZoom) { + // if (m_prevRequest) { + // // Cancel it + // } else { + // id = setTimeout(_renderOnCanvas, 1000); + // } + // } + }; + //////////////////////////////////////////////////////////////////////////// /** * Destroy @@ -243,4 +310,4 @@ inherit(canvas_heatmapFeature, heatmapFeature); // Now register it registerFeature('canvas', 'heatmap', canvas_heatmapFeature); -module.exports = canvas_heatmapFeature; +module.exports = canvas_heatmapFeature; \ No newline at end of file From fadaa91bbb6e02c12a6704dd734daeaba3ec67be Mon Sep 17 00:00:00 2001 From: David Manthey Date: Tue, 3 May 2016 16:06:16 -0400 Subject: [PATCH 03/16] Fix intermediate transforms and update delays on the heatmap. The update delay can be specified. --- src/canvas/canvasRenderer.js | 5 +- src/canvas/heatmapFeature.js | 130 ++++++++++++++++++++--------------- src/heatmapFeature.js | 20 ++++++ 3 files changed, 95 insertions(+), 60 deletions(-) diff --git a/src/canvas/canvasRenderer.js b/src/canvas/canvasRenderer.js index 3029aa43ca..2d0e8090ba 100644 --- a/src/canvas/canvasRenderer.js +++ b/src/canvas/canvasRenderer.js @@ -29,10 +29,9 @@ var canvasRenderer = function (arg) { s_init = this._init, s_exit = this._exit; - - this.clearCanvas = function(arg) { + this.clearCanvas = function (arg) { m_clearCanvas = arg; - } + }; //////////////////////////////////////////////////////////////////////////// /** diff --git a/src/canvas/heatmapFeature.js b/src/canvas/heatmapFeature.js index 7fe6e9fe3c..282aeb5ca9 100644 --- a/src/canvas/heatmapFeature.js +++ b/src/canvas/heatmapFeature.js @@ -34,16 +34,11 @@ var canvas_heatmapFeature = function (arg) { m_typedBuffer = null, m_typedClampedBuffer = null, m_typedBufferData = null, - m_heatMapZoom, - m_lastZoom, - m_lastScale = 0, + m_heatMapPosition, s_exit = this._exit, s_init = this._init, s_update = this._update, - m_currentX = 0, - m_currentY = 0, - m_renderTime = timestamp(), - m_translate; + m_renderTime = timestamp(); //////////////////////////////////////////////////////////////////////////// /** @@ -85,7 +80,7 @@ var canvas_heatmapFeature = function (arg) { gradient.addColorStop(stop, m_this._convertColor(colors[stop])); } - context2d.fillStyle = gradient; + context2d.fillStyle = gradient; context2d.fillRect(0, 0, 1, 256); m_this._grad = context2d.getImageData(0, 0, 1, 256).data; } @@ -133,9 +128,9 @@ var canvas_heatmapFeature = function (arg) { var isLittleEndian = true, i, j, index; // Determine whether Uint32 is little- or big-endian. - if (!m_typedBuffer || (m_typedBuffer.length != imageData.data.length)) { - m_typedBuffer = new ArrayBuffer(imageData.data.length), - m_typedClampedBuffer = new Uint8ClampedArray(m_typedBuffer), + if (!m_typedBuffer || (m_typedBuffer.length !== imageData.data.length)) { + m_typedBuffer = new ArrayBuffer(imageData.data.length); + m_typedClampedBuffer = new Uint8ClampedArray(m_typedBuffer); m_typedBufferData = new Uint32Array(m_typedBuffer); } @@ -146,7 +141,7 @@ var canvas_heatmapFeature = function (arg) { m_typedBuffer[5] === 0x0b && m_typedBuffer[6] === 0x0c && m_typedBuffer[7] === 0x0d) { - isLittleEndian = false; + isLittleEndian = false; } if (isLittleEndian) { @@ -157,10 +152,10 @@ var canvas_heatmapFeature = function (arg) { m_typedBufferData[i] = (gradient[index + 3] << 24) | (gradient[index + 2] << 16) | - (gradient[index + 1] << 8) | + (gradient[index + 1] << 8) | gradient[index]; - } - i += 1; + } + i += 1; } } else { i = 0; @@ -168,9 +163,9 @@ var canvas_heatmapFeature = function (arg) { index = imageData.data[j + 3] * 4; if (index) { m_typedBufferData[i] = - (gradient[index] << 24) | + (gradient[index] << 24) | (gradient[index + 1] << 16) | - (gradient[index + 2] << 8) | + (gradient[index + 2] << 8) | gradient[index + 3]; } } @@ -187,15 +182,22 @@ var canvas_heatmapFeature = function (arg) { */ //////////////////////////////////////////////////////////////////////////// this._renderOnCanvas = function (context2d, map) { - var data = m_this.data() || [], - radius = m_this.style('radius') + m_this.style('blurRadius'), - pos, intensity, canvas, pixelArray; if (m_renderTime.getMTime() < m_this.buildTime().getMTime()) { + var data = m_this.data() || [], + radius = m_this.style('radius') + m_this.style('blurRadius'), + pos, intensity, canvas, pixelArray, + layer = m_this.layer(), + viewport = map.camera()._viewport; + + context2d.setTransform(1, 0, 0, 1, 0, 0); + context2d.clearRect(0, 0, viewport.width, viewport.height); + layer.canvas().css('transform', ''); + m_this._createCircle(); m_this._computeGradient(); data.forEach(function (d) { - pos = m_this.layer().map().gcsToDisplay(m_this.position()(d)); + pos = map.gcsToDisplay(m_this.position()(d)); intensity = (m_this.intensity()(d) - m_this.minIntensity()) / (m_this.maxIntensity() - m_this.minIntensity()); // Small values are not visible because globalAlpha < .01 @@ -203,20 +205,22 @@ var canvas_heatmapFeature = function (arg) { context2d.globalAlpha = intensity < 0.01 ? 0.01 : intensity; context2d.drawImage(m_this._circle, pos.x - radius, pos.y - radius); }); - canvas = m_this.layer().canvas()[0]; + canvas = layer.canvas()[0]; pixelArray = context2d.getImageData(0, 0, canvas.width, canvas.height); m_this._colorize(context2d, canvas.width, canvas.height, pixelArray, m_this._grad); - m_heatMapZoom = m_this.layer().map().zoom(); - m_translate = {x: 0, y: 0}; - m_lastZoom = null; + m_heatMapPosition = { + zoom: map.zoom(), + origin: map.displayToGcs({x: 0, y: 0}, null), + rotation: map.rotation(), + lastScale: undefined, + lastOrigin: {x: 0, y: 0}, + lastRotation: undefined + }; + m_renderTime.modified(); + layer.renderer().clearCanvas(false); } - - m_renderTime.modified(); - - m_this.layer().renderer().clearCanvas(false); - return m_this; }; @@ -258,38 +262,50 @@ var canvas_heatmapFeature = function (arg) { //////////////////////////////////////////////////////////////////////////// this._animatePan = function (e) { - var zoom = m_this.layer().map().zoom(), - scale = Math.pow(2, (zoom - m_heatMapZoom)); - - if (!e.screenDelta) { - return + var map = m_this.layer().map(), + zoom = map.zoom(), + scale = Math.pow(2, (zoom - m_heatMapPosition.zoom)), + origin = map.gcsToDisplay(m_heatMapPosition.origin, null), + rotation = map.rotation(); + + if (m_heatMapPosition.lastScale === scale && + m_heatMapPosition.lastOrigin.x === origin.x && + m_heatMapPosition.lastOrigin.y === origin.y && + m_heatMapPosition.lastRotation === rotation) { + return; } - var translate = {x: e.screenDelta.x, - y: e.screenDelta.y}; - - console.log(e); - console.log(e.screenDelta); + var transform = '' + + ' translate(' + origin.x + 'px' + ',' + origin.y + 'px' + ')' + + ' scale(' + scale + ')' + + ' rotate(' + ((rotation - m_heatMapPosition.rotation) * 180 / Math.PI) + 'deg)'; - if (zoom !== m_lastZoom && translate.x !== m_translate.x && - translate.y !== m_translate.y) { - var transform = 'translate(' + translate.x + 'px' + ',' + - translate.y + 'px' + ')' + 'scale(' + scale + ')'; + m_this.layer().canvas().css('transform-origin', '0px 0px'); + m_this.layer().canvas().css('transform', transform); - m_this.layer().canvas().css('transform-origin', '50% 50%'); - m_this.layer().canvas().css('transform', transform); + m_heatMapPosition.lastScale = scale; + m_heatMapPosition.lastOrigin.x = origin.x; + m_heatMapPosition.lastOrigin.y = origin.y; + m_heatMapPosition.lastRotation = rotation; - m_translate = translate; - m_lastZoom = zoom; + if (m_heatMapPosition.timeout) { + window.clearTimeout(m_heatMapPosition.timeout); + m_heatMapPosition.timeout = undefined; + } + /* This conditional can change if we compute the heatmap beyond the visable + * viewport so that we don't have to update on pans as often. If we are + * close to where the heatmap was originally computed, don't bother + * updating it. */ + if (parseFloat(scale.toFixed(4)) !== 1 || + parseFloat((rotation - m_heatMapPosition.rotation).toFixed(4)) !== 0 || + parseFloat(origin.x.toFixed(1)) !== 0 || + parseFloat(origin.y.toFixed(1)) !== 0) { + m_heatMapPosition.timeout = window.setTimeout(function () { + m_heatMapPosition.timeout = undefined; + m_this.buildTime().modified(); + m_this.layer().draw(); + }, m_this.updateDelay()); } - - // if (zoom !== m_heatMapZoom) { - // if (m_prevRequest) { - // // Cancel it - // } else { - // id = setTimeout(_renderOnCanvas, 1000); - // } - // } }; //////////////////////////////////////////////////////////////////////////// @@ -310,4 +326,4 @@ inherit(canvas_heatmapFeature, heatmapFeature); // Now register it registerFeature('canvas', 'heatmap', canvas_heatmapFeature); -module.exports = canvas_heatmapFeature; \ No newline at end of file +module.exports = canvas_heatmapFeature; diff --git a/src/heatmapFeature.js b/src/heatmapFeature.js index 3c1f17e470..133d8fb98b 100644 --- a/src/heatmapFeature.js +++ b/src/heatmapFeature.js @@ -31,6 +31,8 @@ var feature = require('./feature'); * intensity must be a positive real number will be used to normalize all * intensities with a dataset. If no value is given, then a it will * be computed. + * @param {number} [updateDelay=1000] Delay in milliseconds after a zoom, + * rotate, or pan event before recomputing the heatmap. * @returns {geo.heatmapFeature} */ ////////////////////////////////////////////////////////////////////////////// @@ -54,12 +56,14 @@ var heatmapFeature = function (arg) { m_intensity, m_maxIntensity, m_minIntensity, + m_updateDelay, s_init = this._init; m_position = arg.position || function (d) { return d; }; m_intensity = arg.intensity || function (d) { return 1; }; m_maxIntensity = arg.maxIntensity || null; m_minIntensity = arg.minIntensity ? arg.minIntensity : null; + m_updateDelay = arg.updateDelay ? parseInt(arg.updateDelay, 10) : 1000; //////////////////////////////////////////////////////////////////////////// /** @@ -97,6 +101,22 @@ var heatmapFeature = function (arg) { return m_this; }; + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set updateDelay + * + * @returns {geo.heatmap} + */ + //////////////////////////////////////////////////////////////////////////// + this.updateDelay = function (val) { + if (val === undefined) { + return m_updateDelay; + } else { + m_updateDelay = parseInt(val, 10); + } + return m_this; + }; + //////////////////////////////////////////////////////////////////////////// /** * Get/Set position accessor From 037c106c7324884dfb201a00b0f5372f7b2472b4 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Tue, 3 May 2016 16:23:37 -0400 Subject: [PATCH 04/16] A minor speed-up can be obtained by directly setting transforms rather than using jQUery's css function. This reduces a bit of jitter when panning a rotated heat map. --- src/canvas/heatmapFeature.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/canvas/heatmapFeature.js b/src/canvas/heatmapFeature.js index 282aeb5ca9..67cb4e70e1 100644 --- a/src/canvas/heatmapFeature.js +++ b/src/canvas/heatmapFeature.js @@ -192,7 +192,7 @@ var canvas_heatmapFeature = function (arg) { context2d.setTransform(1, 0, 0, 1, 0, 0); context2d.clearRect(0, 0, viewport.width, viewport.height); - layer.canvas().css('transform', ''); + layer.canvas().css({transform: '', 'transform-origin': '0px 0px'}); m_this._createCircle(); m_this._computeGradient(); @@ -280,8 +280,7 @@ var canvas_heatmapFeature = function (arg) { ' scale(' + scale + ')' + ' rotate(' + ((rotation - m_heatMapPosition.rotation) * 180 / Math.PI) + 'deg)'; - m_this.layer().canvas().css('transform-origin', '0px 0px'); - m_this.layer().canvas().css('transform', transform); + m_this.layer().canvas()[0].style.transform = transform; m_heatMapPosition.lastScale = scale; m_heatMapPosition.lastOrigin.x = origin.x; From d0cb87c3ca1b4aabaa61ba8343a206fb0219b792 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Wed, 4 May 2016 11:08:47 -0400 Subject: [PATCH 05/16] Rename origin to gcsOrigin to make it more obvious it isn't in display coordinates. --- src/canvas/heatmapFeature.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/canvas/heatmapFeature.js b/src/canvas/heatmapFeature.js index 67cb4e70e1..4c6c5aa43b 100644 --- a/src/canvas/heatmapFeature.js +++ b/src/canvas/heatmapFeature.js @@ -211,7 +211,7 @@ var canvas_heatmapFeature = function (arg) { m_heatMapPosition = { zoom: map.zoom(), - origin: map.displayToGcs({x: 0, y: 0}, null), + gcsOrigin: map.displayToGcs({x: 0, y: 0}, null), rotation: map.rotation(), lastScale: undefined, lastOrigin: {x: 0, y: 0}, @@ -265,7 +265,7 @@ var canvas_heatmapFeature = function (arg) { var map = m_this.layer().map(), zoom = map.zoom(), scale = Math.pow(2, (zoom - m_heatMapPosition.zoom)), - origin = map.gcsToDisplay(m_heatMapPosition.origin, null), + origin = map.gcsToDisplay(m_heatMapPosition.gcsOrigin, null), rotation = map.rotation(); if (m_heatMapPosition.lastScale === scale && From 5e23638ff84c28f032f2c32a615834858ee6499b Mon Sep 17 00:00:00 2001 From: David Manthey Date: Wed, 4 May 2016 17:18:09 -0400 Subject: [PATCH 06/16] Reduce number of function calls for a minor speed improvement. --- src/canvas/heatmapFeature.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/canvas/heatmapFeature.js b/src/canvas/heatmapFeature.js index 4c6c5aa43b..06fda45cc8 100644 --- a/src/canvas/heatmapFeature.js +++ b/src/canvas/heatmapFeature.js @@ -196,10 +196,14 @@ var canvas_heatmapFeature = function (arg) { m_this._createCircle(); m_this._computeGradient(); - data.forEach(function (d) { - pos = map.gcsToDisplay(m_this.position()(d)); - intensity = (m_this.intensity()(d) - m_this.minIntensity()) / - (m_this.maxIntensity() - m_this.minIntensity()); + var position = m_this.position(), + intensityFunc = m_this.intensity(), + minIntensity = m_this.minIntensity(), + maxIntensity = m_this.maxIntensity(); + data.forEach(function (d, idx) { + pos = m_this.featureGcsToDisplay(position(d)); + intensity = (intensityFunc(d) - minIntensity) / + (maxIntensity - minIntensity); // Small values are not visible because globalAlpha < .01 // cannot be read from imageData context2d.globalAlpha = intensity < 0.01 ? 0.01 : intensity; From 47e461a44538896e04d674d122284793bbce3b02 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Thu, 5 May 2016 08:21:08 -0400 Subject: [PATCH 07/16] Precompute gcs positions for heatmaps. This will only change if the data changes or the projection changes, which will cause it to be recomputed then. Otherwise, this reduces update time by 10% of so. --- src/canvas/heatmapFeature.js | 4 ++-- src/heatmapFeature.js | 20 +++++++++++++++++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/canvas/heatmapFeature.js b/src/canvas/heatmapFeature.js index 06fda45cc8..4ba8a16f3a 100644 --- a/src/canvas/heatmapFeature.js +++ b/src/canvas/heatmapFeature.js @@ -196,12 +196,12 @@ var canvas_heatmapFeature = function (arg) { m_this._createCircle(); m_this._computeGradient(); - var position = m_this.position(), + var position = m_this.gcsPosition(), intensityFunc = m_this.intensity(), minIntensity = m_this.minIntensity(), maxIntensity = m_this.maxIntensity(); data.forEach(function (d, idx) { - pos = m_this.featureGcsToDisplay(position(d)); + pos = map.worldToDisplay(position[idx]); intensity = (intensityFunc(d) - minIntensity) / (maxIntensity - minIntensity); // Small values are not visible because globalAlpha < .01 diff --git a/src/heatmapFeature.js b/src/heatmapFeature.js index 133d8fb98b..d5bf9173a4 100644 --- a/src/heatmapFeature.js +++ b/src/heatmapFeature.js @@ -1,6 +1,7 @@ var $ = require('jquery'); var inherit = require('./inherit'); var feature = require('./feature'); +var transform = require('./transform'); ////////////////////////////////////////////////////////////////////////////// /** @@ -57,6 +58,7 @@ var heatmapFeature = function (arg) { m_maxIntensity, m_minIntensity, m_updateDelay, + m_gcsPosition, s_init = this._init; m_position = arg.position || function (d) { return d; }; @@ -135,6 +137,18 @@ var heatmapFeature = function (arg) { return m_this; }; + //////////////////////////////////////////////////////////////////////////// + /** + * Get pre-computed gcs position accessor + * + * @returns {geo.heatmap} + */ + //////////////////////////////////////////////////////////////////////////// + this.gcsPosition = function () { + this._update(); + return m_gcsPosition; + }; + //////////////////////////////////////////////////////////////////////////// /** * Get/Set intensity @@ -191,10 +205,12 @@ var heatmapFeature = function (arg) { //////////////////////////////////////////////////////////////////////////// this._build = function () { var data = m_this.data(), - intensity = null; + intensity = null, + position = []; if (!m_maxIntensity || !m_minIntensity) { data.forEach(function (d) { + position.push(m_this.position()(d)); intensity = m_this.intensity()(d); if (!m_maxIntensity && !m_minIntensity) { m_maxIntensity = m_minIntensity = intensity; @@ -208,6 +224,8 @@ var heatmapFeature = function (arg) { } }); } + m_gcsPosition = transform.transformCoordinates( + m_this.gcs(), m_this.layer().map().gcs(), position); m_this.buildTime().modified(); return m_this; From f2c6a68c1c4c9df0ed1ca340ea4a5d550194c894 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Thu, 5 May 2016 09:06:35 -0400 Subject: [PATCH 08/16] Loops can be faster than functions. Switching the core drawImage loop sped up large datasets by 40% or so. --- src/canvas/heatmapFeature.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/canvas/heatmapFeature.js b/src/canvas/heatmapFeature.js index 4ba8a16f3a..965a053e32 100644 --- a/src/canvas/heatmapFeature.js +++ b/src/canvas/heatmapFeature.js @@ -199,16 +199,15 @@ var canvas_heatmapFeature = function (arg) { var position = m_this.gcsPosition(), intensityFunc = m_this.intensity(), minIntensity = m_this.minIntensity(), - maxIntensity = m_this.maxIntensity(); - data.forEach(function (d, idx) { + rangeIntensity = (m_this.maxIntensity() - minIntensity) || 1; + for (var idx = data.length - 1; idx >= 0; idx -= 1) { pos = map.worldToDisplay(position[idx]); - intensity = (intensityFunc(d) - minIntensity) / - (maxIntensity - minIntensity); + intensity = (intensityFunc(data[idx]) - minIntensity) / rangeIntensity; // Small values are not visible because globalAlpha < .01 // cannot be read from imageData context2d.globalAlpha = intensity < 0.01 ? 0.01 : intensity; context2d.drawImage(m_this._circle, pos.x - radius, pos.y - radius); - }); + } canvas = layer.canvas()[0]; pixelArray = context2d.getImageData(0, 0, canvas.width, canvas.height); m_this._colorize(context2d, canvas.width, canvas.height, pixelArray, m_this._grad); From f52faa5682ae935d6e891312a3e72abbc6f85128 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Fri, 6 May 2016 08:10:30 -0400 Subject: [PATCH 09/16] Fix some issues setting max and min intensity. Precalculating position must always be done, too. --- src/canvas/heatmapFeature.js | 5 ++++- src/heatmapFeature.js | 40 +++++++++++++++++++++--------------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/src/canvas/heatmapFeature.js b/src/canvas/heatmapFeature.js index 965a053e32..e5bd8d2894 100644 --- a/src/canvas/heatmapFeature.js +++ b/src/canvas/heatmapFeature.js @@ -203,9 +203,12 @@ var canvas_heatmapFeature = function (arg) { for (var idx = data.length - 1; idx >= 0; idx -= 1) { pos = map.worldToDisplay(position[idx]); intensity = (intensityFunc(data[idx]) - minIntensity) / rangeIntensity; + if (intensity <= 0) { + continue; + } // Small values are not visible because globalAlpha < .01 // cannot be read from imageData - context2d.globalAlpha = intensity < 0.01 ? 0.01 : intensity; + context2d.globalAlpha = intensity < 0.01 ? 0.01 : (intensity > 1 ? 1 : intensity); context2d.drawImage(m_this._circle, pos.x - radius, pos.y - radius); } canvas = layer.canvas()[0]; diff --git a/src/heatmapFeature.js b/src/heatmapFeature.js index d5bf9173a4..ec8fff5cb2 100644 --- a/src/heatmapFeature.js +++ b/src/heatmapFeature.js @@ -63,8 +63,8 @@ var heatmapFeature = function (arg) { m_position = arg.position || function (d) { return d; }; m_intensity = arg.intensity || function (d) { return 1; }; - m_maxIntensity = arg.maxIntensity || null; - m_minIntensity = arg.minIntensity ? arg.minIntensity : null; + m_maxIntensity = arg.maxIntensity !== undefined ? arg.maxIntensity : null; + m_minIntensity = arg.minIntensity !== undefined ? arg.minIntensity : null; m_updateDelay = arg.updateDelay ? parseInt(arg.updateDelay, 10) : 1000; //////////////////////////////////////////////////////////////////////////// @@ -206,23 +206,31 @@ var heatmapFeature = function (arg) { this._build = function () { var data = m_this.data(), intensity = null, - position = []; + position = [], + setMax = (m_maxIntensity === null || m_maxIntensity === undefined), + setMin = (m_minIntensity === null || m_minIntensity === undefined); - if (!m_maxIntensity || !m_minIntensity) { - data.forEach(function (d) { - position.push(m_this.position()(d)); + data.forEach(function (d) { + position.push(m_this.position()(d)); + if (setMax || setMin) { intensity = m_this.intensity()(d); - if (!m_maxIntensity && !m_minIntensity) { - m_maxIntensity = m_minIntensity = intensity; - } else { - if (intensity > m_maxIntensity) { - m_maxIntensity = intensity; - } - if (intensity < m_minIntensity) { - m_minIntensity = intensity; - } + if (m_maxIntensity === null || m_maxIntensity === undefined) { + m_maxIntensity = intensity; } - }); + if (m_minIntensity === null || m_minIntensity === undefined) { + m_minIntensity = intensity; + } + if (setMax && intensity > m_maxIntensity) { + m_maxIntensity = intensity; + } + if (setMin && intensity < m_minIntensity) { + m_minIntensity = intensity; + } + + } + }); + if (setMin && setMax && m_minIntensity === m_maxIntensity) { + m_minIntensity -= 1; } m_gcsPosition = transform.transformCoordinates( m_this.gcs(), m_this.layer().map().gcs(), position); From b4801bd2ac56c0b53db37cdbafd9760c18b36c7d Mon Sep 17 00:00:00 2001 From: Aashish Chaudhary Date: Wed, 11 May 2016 13:40:22 -0400 Subject: [PATCH 10/16] Fixed Endian check Earlier looking at the wrong buffer on which indexing was undefined --- src/canvas/heatmapFeature.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/canvas/heatmapFeature.js b/src/canvas/heatmapFeature.js index e5bd8d2894..c3191c3ab4 100644 --- a/src/canvas/heatmapFeature.js +++ b/src/canvas/heatmapFeature.js @@ -137,10 +137,10 @@ var canvas_heatmapFeature = function (arg) { m_typedBufferData[1] = 0x0a0b0c0d; isLittleEndian = true; - if (m_typedBuffer[4] === 0x0a && - m_typedBuffer[5] === 0x0b && - m_typedBuffer[6] === 0x0c && - m_typedBuffer[7] === 0x0d) { + if (m_typedClampedBuffer[4] === 0x0a && + m_typedClampedBuffer[5] === 0x0b && + m_typedClampedBuffer[6] === 0x0c && + m_typedClampedBuffer[7] === 0x0d) { isLittleEndian = false; } From 8f25c0a4e684f10ebd6a1c9c83d7c8999e293db8 Mon Sep 17 00:00:00 2001 From: Aashish Chaudhary Date: Wed, 11 May 2016 22:10:45 -0400 Subject: [PATCH 11/16] Do not use typed array on big-endian systems --- src/canvas/heatmapFeature.js | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/canvas/heatmapFeature.js b/src/canvas/heatmapFeature.js index c3191c3ab4..8cf89fc67a 100644 --- a/src/canvas/heatmapFeature.js +++ b/src/canvas/heatmapFeature.js @@ -125,7 +125,7 @@ var canvas_heatmapFeature = function (arg) { */ //////////////////////////////////////////////////////////////////////////// this._colorize = function (context2d, width, height, imageData, gradient) { - var isLittleEndian = true, i, j, index; + var isLittleEndian = true, i, j, index, pixels; // Determine whether Uint32 is little- or big-endian. if (!m_typedBuffer || (m_typedBuffer.length !== imageData.data.length)) { @@ -157,21 +157,20 @@ var canvas_heatmapFeature = function (arg) { } i += 1; } + imageData.data.set(m_typedClampedBuffer); } else { - i = 0; - for (j = 0; j < (width * height * 4); j += 4) { - index = imageData.data[j + 3] * 4; - if (index) { - m_typedBufferData[i] = - (gradient[index] << 24) | - (gradient[index + 1] << 16) | - (gradient[index + 2] << 8) | - gradient[index + 3]; + pixels = imageData.data; + for (i = 0; i < pixels.length; i += 4) { + j = pixels[i + 3] * 4; + if (j) { + pixels[i] = gradient[j]; + pixels[i + 1] = gradient[j + 1]; + pixels[i + 2] = gradient[j + 2]; + pixels[i + 3] = gradient[j + 3] } } } - imageData.data.set(m_typedClampedBuffer); context2d.putImageData(imageData, 0, 0); }; From ec76824128e23a29a1526261a11d451830c3d2d3 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Thu, 12 May 2016 09:12:49 -0400 Subject: [PATCH 12/16] Simplify _colorize. This is slightly faster because it operates on 32 bit values instead of 8 bit values. For 100,000 points in a controlled test, the average render time went from 517.7 ms to 499.5 ms (a 3.5% improvement). Additionally, there is only one code path, rather than two, which simplifies testing. --- src/canvas/heatmapFeature.js | 64 ++++++++++-------------------------- 1 file changed, 17 insertions(+), 47 deletions(-) diff --git a/src/canvas/heatmapFeature.js b/src/canvas/heatmapFeature.js index 8cf89fc67a..902d7c511d 100644 --- a/src/canvas/heatmapFeature.js +++ b/src/canvas/heatmapFeature.js @@ -31,9 +31,9 @@ var canvas_heatmapFeature = function (arg) { var geo_event = require('../event'); var m_this = this, - m_typedBuffer = null, - m_typedClampedBuffer = null, - m_typedBufferData = null, + m_typedBuffer, + m_typedClampedBuffer, + m_typedBufferData, m_heatMapPosition, s_exit = this._exit, s_init = this._init, @@ -124,54 +124,23 @@ var canvas_heatmapFeature = function (arg) { * @protected */ //////////////////////////////////////////////////////////////////////////// - this._colorize = function (context2d, width, height, imageData, gradient) { - var isLittleEndian = true, i, j, index, pixels; - - // Determine whether Uint32 is little- or big-endian. - if (!m_typedBuffer || (m_typedBuffer.length !== imageData.data.length)) { - m_typedBuffer = new ArrayBuffer(imageData.data.length); + this._colorize = function (pixels, gradient) { + var grad = new Uint32Array(gradient.buffer); + var i, j, k, pixlen = pixels.length; + if (!m_typedBuffer || m_typedBuffer.length !== pixlen) { + m_typedBuffer = new ArrayBuffer(pixlen); m_typedClampedBuffer = new Uint8ClampedArray(m_typedBuffer); m_typedBufferData = new Uint32Array(m_typedBuffer); } - - m_typedBufferData[1] = 0x0a0b0c0d; - - isLittleEndian = true; - if (m_typedClampedBuffer[4] === 0x0a && - m_typedClampedBuffer[5] === 0x0b && - m_typedClampedBuffer[6] === 0x0c && - m_typedClampedBuffer[7] === 0x0d) { - isLittleEndian = false; - } - - if (isLittleEndian) { - i = 0; - for (j = 0; j < (width * height * 4); j += 4) { - index = imageData.data[j + 3] * 4; - if (index) { - m_typedBufferData[i] = - (gradient[index + 3] << 24) | - (gradient[index + 2] << 16) | - (gradient[index + 1] << 8) | - gradient[index]; - } - i += 1; - } - imageData.data.set(m_typedClampedBuffer); - } else { - pixels = imageData.data; - for (i = 0; i < pixels.length; i += 4) { - j = pixels[i + 3] * 4; - if (j) { - pixels[i] = gradient[j]; - pixels[i + 1] = gradient[j + 1]; - pixels[i + 2] = gradient[j + 2]; - pixels[i + 3] = gradient[j + 3] - } + for (i = 3, k = 0; i < pixlen; i += 4, k += 1) { + // Get opacity from the temporary canvas image and look up the final + // value from gradient + j = pixels[i]; + if (j) { + m_typedBufferData[k] = grad[j]; } } - - context2d.putImageData(imageData, 0, 0); + pixels.set(m_typedClampedBuffer); }; //////////////////////////////////////////////////////////////////////////// @@ -212,7 +181,8 @@ var canvas_heatmapFeature = function (arg) { } canvas = layer.canvas()[0]; pixelArray = context2d.getImageData(0, 0, canvas.width, canvas.height); - m_this._colorize(context2d, canvas.width, canvas.height, pixelArray, m_this._grad); + m_this._colorize(pixelArray.data, m_this._grad); + context2d.putImageData(pixelArray, 0, 0); m_heatMapPosition = { zoom: map.zoom(), From a1db91b216e1c28443dab892ec545144c82397e2 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Thu, 12 May 2016 10:16:03 -0400 Subject: [PATCH 13/16] Remove opacity as a heatmap feature property. Every layer has an opacity property. Unless there is a real need to have multiple features in the same layer with different per-feature opacities, I think we should stick with the current model. --- src/heatmapFeature.js | 2 -- tests/cases/heatmap.js | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/heatmapFeature.js b/src/heatmapFeature.js index ec8fff5cb2..8a55a68290 100644 --- a/src/heatmapFeature.js +++ b/src/heatmapFeature.js @@ -13,7 +13,6 @@ var transform = require('./transform'); * @param {Object|string|Function} [color] Color transfer function that. * will be used to evaluate color of each pixel using normalized intensity * as the look up value. - * @param {number|Function} [opacity=1] Homogeneous opacity for each pixel. * @param {Object|Function} [radius=10] Radius of a point in terms of number * of pixels. * @param {Object|Function} [blurRadius=10] Gaussian blur radius for each @@ -178,7 +177,6 @@ var heatmapFeature = function (arg) { var defaultStyle = $.extend( {}, { - opacity: 0.1, radius: 10, blurRadius: 10, color: {0: {r: 0, g: 0, b: 0.0, a: 0.0}, diff --git a/tests/cases/heatmap.js b/tests/cases/heatmap.js index 87cbf99ef1..a771f59b42 100644 --- a/tests/cases/heatmap.js +++ b/tests/cases/heatmap.js @@ -42,8 +42,7 @@ describe('canvas heatmap feature', function () { }; }) .style('radius', 5) - .style('blurRadius', 15) - .style('opacity', 1.0); + .style('blurRadius', 15); mockAnimationFrame(); map.draw(); From 3b8fa85543824d519cfd592b3f11cd9574b2e56b Mon Sep 17 00:00:00 2001 From: David Manthey Date: Thu, 12 May 2016 10:21:32 -0400 Subject: [PATCH 14/16] Switch to a single var statement. --- src/canvas/heatmapFeature.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/canvas/heatmapFeature.js b/src/canvas/heatmapFeature.js index 902d7c511d..db80c6ef49 100644 --- a/src/canvas/heatmapFeature.js +++ b/src/canvas/heatmapFeature.js @@ -125,8 +125,9 @@ var canvas_heatmapFeature = function (arg) { */ //////////////////////////////////////////////////////////////////////////// this._colorize = function (pixels, gradient) { - var grad = new Uint32Array(gradient.buffer); - var i, j, k, pixlen = pixels.length; + var grad = new Uint32Array(gradient.buffer), + pixlen = pixels.length, + i, j, k; if (!m_typedBuffer || m_typedBuffer.length !== pixlen) { m_typedBuffer = new ArrayBuffer(pixlen); m_typedClampedBuffer = new Uint8ClampedArray(m_typedBuffer); From ee6500ebaf9a2378df03ce2febcf86e879b41442 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Thu, 12 May 2016 13:30:54 -0400 Subject: [PATCH 15/16] Complete coverage of src/heatmapFeature.js. Fix some of the documentation. --- src/heatmapFeature.js | 6 +-- tests/cases/heatmap.js | 99 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 3 deletions(-) diff --git a/src/heatmapFeature.js b/src/heatmapFeature.js index 8a55a68290..e40c10fc67 100644 --- a/src/heatmapFeature.js +++ b/src/heatmapFeature.js @@ -20,14 +20,14 @@ var transform = require('./transform'); * @param {Object|Function} [position] Position of the data. Default is * (data). The position is an Object which specifies the location of the * data in geo-spatial context. - * @param {boolean} [intensity] Scalar value of each data point. Scalar + * @param {Object|Function} [intensity] Scalar value of each data point. Scalar * value must be a positive real number and will be used to compute * the weight for each data point. - * @param {boolean} [maxIntensity=null] Maximum intensity of the data. Maximum + * @param {number} [maxIntensity=null] Maximum intensity of the data. Maximum * intensity must be a positive real number and will be used to normalize all * intensities with a dataset. If no value is given, then a it will * be computed. - * @param {boolean} [minIntensity=null] Minimum intensity of the data. Minimum + * @param {number} [minIntensity=null] Minimum intensity of the data. Minimum * intensity must be a positive real number will be used to normalize all * intensities with a dataset. If no value is given, then a it will * be computed. diff --git a/tests/cases/heatmap.js b/tests/cases/heatmap.js index a771f59b42..a6e578522b 100644 --- a/tests/cases/heatmap.js +++ b/tests/cases/heatmap.js @@ -84,3 +84,102 @@ describe('canvas heatmap feature', function () { .getImageData(1, 0, 1, 1).data.length).toBe(4); }); }); + +describe('core.heatmapFeature', function () { + var map, layer; + var heatmapFeature = require('../../src/heatmapFeature'); + var data = []; + + it('Setup map', function () { + map = geo.map({node: '#map-canvas-heatmap-feature', center: [0, 0], zoom: 3}); + layer = map.createLayer('feature', {'renderer': 'canvas'}); + for (var i = 0; i < 100; i += 1) { + data.push({a: i % 10, b: i % 9, c: i % 8}); + } + }); + + describe('class accessors', function () { + it('maxIntensity', function () { + var heatmap = heatmapFeature({layer: layer}); + expect(heatmap.maxIntensity()).toBe(null); + expect(heatmap.maxIntensity(7)).toBe(heatmap); + expect(heatmap.maxIntensity()).toBe(7); + heatmap = heatmapFeature({layer: layer, maxIntensity: 8}); + expect(heatmap.maxIntensity()).toBe(8); + }); + it('minIntensity', function () { + var heatmap = heatmapFeature({layer: layer}); + expect(heatmap.minIntensity()).toBe(null); + expect(heatmap.minIntensity(2)).toBe(heatmap); + expect(heatmap.minIntensity()).toBe(2); + heatmap = heatmapFeature({layer: layer, minIntensity: 3}); + expect(heatmap.minIntensity()).toBe(3); + }); + it('updateDelay', function () { + var heatmap = heatmapFeature({layer: layer}); + expect(heatmap.updateDelay()).toBe(1000); + expect(heatmap.updateDelay(40)).toBe(heatmap); + expect(heatmap.updateDelay()).toBe(40); + heatmap = heatmapFeature({layer: layer, updateDelay: 50}); + expect(heatmap.updateDelay()).toBe(50); + }); + it('position', function () { + var heatmap = heatmapFeature({layer: layer}); + expect(heatmap.position()('abc')).toBe('abc'); + expect(heatmap.position(function (d) { + return {x: d.a, y: d.b}; + })).toBe(heatmap); + expect(heatmap.position()(data[0])).toEqual({x: 0, y: 0}); + expect(heatmap.position()(data[84])).toEqual({x: 4, y: 3}); + heatmap = heatmapFeature({layer: layer, position: function (d) { + return {x: d.b, y: d.c}; + }}); + expect(heatmap.position()(data[0])).toEqual({x: 0, y: 0}); + expect(heatmap.position()(data[87])).toEqual({x: 6, y: 7}); + }); + it('intensity', function () { + var heatmap = heatmapFeature({layer: layer}); + expect(heatmap.intensity()('abc')).toBe(1); + expect(heatmap.intensity(function (d) { + return d.c; + })).toBe(heatmap); + expect(heatmap.intensity()(data[0])).toEqual(0); + expect(heatmap.intensity()(data[67])).toEqual(3); + heatmap = heatmapFeature({layer: layer, intensity: function (d) { + return d.a; + }}); + expect(heatmap.intensity()(data[0])).toEqual(0); + expect(heatmap.intensity()(data[67])).toEqual(7); + }); + }); + describe('_build', function () { + it('intensity ranges', function () { + var heatmap = heatmapFeature({layer: layer, position: function (d) { + return {x: d.a, y: d.b}; + }, intensity: function (d) { + return d.c; + }}).data(data); + heatmap.gcs('EPSG:3857'); + heatmap._build(); + expect(heatmap.minIntensity()).toBe(0); + expect(heatmap.maxIntensity()).toBe(7); + heatmap.intensity(function () { return 2; }); + heatmap.maxIntensity(null).minIntensity(null); + heatmap._build(); + expect(heatmap.minIntensity()).toBe(1); + expect(heatmap.maxIntensity()).toBe(2); + }); + it('gcsPosition', function () { + var heatmap = heatmapFeature({layer: layer, position: function (d) { + return {x: d.a, y: d.b}; + }}).data(data); + heatmap.gcs('EPSG:3857'); + // we have to call build since we didn't attach this to the layer in the + // normal way + heatmap._build(); + var pos = heatmap.gcsPosition(); + expect(pos[0]).toEqual({x: 0, y: 0}); + expect(pos[84]).toEqual({x: 4, y: 3}); + }); + }); +}); From 62b0e77546f46d476c60782d26847788fc07f57e Mon Sep 17 00:00:00 2001 From: David Manthey Date: Thu, 12 May 2016 14:46:30 -0400 Subject: [PATCH 16/16] Add tests for _animatePan. Fix an issue where a transform of a zero length array would throw an error. --- src/transform.js | 3 +++ tests/cases/heatmap.js | 35 ++++++++++++++++++++++++++++++----- tests/cases/transform.js | 5 +++++ 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/src/transform.js b/src/transform.js index 0aa0e30352..e32f7021c1 100644 --- a/src/transform.js +++ b/src/transform.js @@ -414,6 +414,9 @@ transform.transformCoordinatesArray = function (trans, coordinates, numberOfComp output.length = coordinates.length; count = coordinates.length; + if (!coordinates.length) { + return output; + } if (coordinates[0] instanceof Array || coordinates[0] instanceof Object) { offset = 1; diff --git a/tests/cases/heatmap.js b/tests/cases/heatmap.js index a6e578522b..2070cca082 100644 --- a/tests/cases/heatmap.js +++ b/tests/cases/heatmap.js @@ -22,6 +22,13 @@ describe('canvas heatmap feature', function () { testData = [[0.6, 42.8584, -70.9301], [0.233, 42.2776, -83.7409], [0.2, 42.2776, -83.7409]]; + var clock; + beforeEach(function () { + clock = sinon.useFakeTimers(); + }); + afterEach(function () { + clock.restore(); + }); it('Setup map', function () { map = geo.map({node: '#map-canvas-heatmap-feature', center: [0, 0], zoom: 3}); @@ -68,11 +75,6 @@ describe('canvas heatmap feature', function () { expect(feature1.minIntensity()).toBe(0.2); }); - 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}, @@ -83,6 +85,29 @@ describe('canvas heatmap feature', function () { expect(layer.node()[0].children[0].getContext('2d') .getImageData(1, 0, 1, 1).data.length).toBe(4); }); + it('_animatePan', function () { + map.draw(); + var buildTime = feature1.buildTime().getMTime(); + map.pan({x: 10, y: 0}); + expect(feature1.buildTime().getMTime()).toBe(buildTime); + clock.tick(800); + map.pan({x: 10, y: 0}); + expect(feature1.buildTime().getMTime()).toBe(buildTime); + clock.tick(800); + expect(feature1.buildTime().getMTime()).toBe(buildTime); + clock.tick(800); + expect(feature1.buildTime().getMTime()).not.toBe(buildTime); + buildTime = feature1.buildTime().getMTime(); + map.pan({x: 0, y: 0}); + expect(feature1.buildTime().getMTime()).toBe(buildTime); + clock.tick(2000); + expect(feature1.buildTime().getMTime()).toBe(buildTime); + }); + it('Remove a feature from a layer', function () { + layer.deleteFeature(feature1).draw(); + expect(layer.children().length).toBe(0); + }); + }); describe('core.heatmapFeature', function () { diff --git a/tests/cases/transform.js b/tests/cases/transform.js index e75348b73b..5f499b6a4e 100644 --- a/tests/cases/transform.js +++ b/tests/cases/transform.js @@ -230,6 +230,11 @@ describe('geo.transform', function () { expect(closeToEqual(geo.transform.transformCoordinates(source, target, {x: 1, y: 2}), {x: 1, y: -2})).toBe(true); expect(closeToEqual(geo.transform.transformCoordinates(source, target, {x: 3, y: 4, z: 5}), {x: 3, y: -4, z: 5})).toBe(true); }); + it('empty array', function () { + var res = geo.transform.transformCoordinates(source, target, []); + expect(res instanceof Array).toBe(true); + expect(res.length).toBe(0); + }); it('coordinate format - array with single object', function () { var res; res = geo.transform.transformCoordinates(source, target, [{x: 1, y: 2}]);