Skip to content

Commit

Permalink
Merge pull request #574 from OpenGeoscience/improve_performance_canva…
Browse files Browse the repository at this point in the history
…s_heatmap

Improve performance canvas heatmap
  • Loading branch information
aashish24 committed May 13, 2016
2 parents aedf747 + 10da56c commit b3e15f8
Show file tree
Hide file tree
Showing 6 changed files with 338 additions and 58 deletions.
12 changes: 10 additions & 2 deletions src/canvas/canvasRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,14 @@ 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
Expand Down Expand Up @@ -91,9 +96,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) {
Expand Down
153 changes: 125 additions & 28 deletions src/canvas/heatmapFeature.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
var inherit = require('../inherit');
var registerFeature = require('../registry').registerFeature;
var heatmapFeature = require('../heatmapFeature');
var timestamp = require('../timestamp');

//////////////////////////////////////////////////////////////////////////////
/**
Expand All @@ -27,10 +28,17 @@ var canvas_heatmapFeature = function (arg) {
* @private
*/
////////////////////////////////////////////////////////////////////////////
var geo_event = require('../event');

var m_this = this,
m_typedBuffer,
m_typedClampedBuffer,
m_typedBufferData,
m_heatMapPosition,
s_exit = this._exit,
s_init = this._init,
s_update = this._update;
s_update = this._update,
m_renderTime = timestamp();

////////////////////////////////////////////////////////////////////////////
/**
Expand Down Expand Up @@ -117,18 +125,23 @@ var canvas_heatmapFeature = function (arg) {
*/
////////////////////////////////////////////////////////////////////////////
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;
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);
m_typedBufferData = new Uint32Array(m_typedBuffer);
}
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) {
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];
m_typedBufferData[k] = grad[j];
}
}
pixels.set(m_typedClampedBuffer);
};

////////////////////////////////////////////////////////////////////////////
Expand All @@ -138,24 +151,52 @@ 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;
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(pixelArray.data, m_this._grad);
context2d.putImageData(pixelArray, 0, 0);

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: '', 'transform-origin': '0px 0px'});

m_this._createCircle();
m_this._computeGradient();
var position = m_this.gcsPosition(),
intensityFunc = m_this.intensity(),
minIntensity = m_this.minIntensity(),
rangeIntensity = (m_this.maxIntensity() - minIntensity) || 1;
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 > 1 ? 1 : 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(pixelArray.data, m_this._grad);
context2d.putImageData(pixelArray, 0, 0);

m_heatMapPosition = {
zoom: map.zoom(),
gcsOrigin: 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);
}

return m_this;
};

Expand All @@ -167,6 +208,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;
};

Expand All @@ -186,6 +230,59 @@ var canvas_heatmapFeature = function (arg) {
return m_this;
};

////////////////////////////////////////////////////////////////////////////
/**
* Animate pan (and zoom)
* @protected
*/
////////////////////////////////////////////////////////////////////////////
this._animatePan = function (e) {

var map = m_this.layer().map(),
zoom = map.zoom(),
scale = Math.pow(2, (zoom - m_heatMapPosition.zoom)),
origin = map.gcsToDisplay(m_heatMapPosition.gcsOrigin, 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 transform = '' +
' translate(' + origin.x + 'px' + ',' + origin.y + 'px' + ')' +
' scale(' + scale + ')' +
' rotate(' + ((rotation - m_heatMapPosition.rotation) * 180 / Math.PI) + 'deg)';

m_this.layer().canvas()[0].style.transform = transform;

m_heatMapPosition.lastScale = scale;
m_heatMapPosition.lastOrigin.x = origin.x;
m_heatMapPosition.lastOrigin.y = origin.y;
m_heatMapPosition.lastRotation = rotation;

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());
}
};

////////////////////////////////////////////////////////////////////////////
/**
* Destroy
Expand Down
86 changes: 65 additions & 21 deletions src/heatmapFeature.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
var $ = require('jquery');
var inherit = require('./inherit');
var feature = require('./feature');
var transform = require('./transform');

//////////////////////////////////////////////////////////////////////////////
/**
Expand All @@ -12,25 +13,26 @@ var feature = require('./feature');
* @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
* point in terms of number of pixels.
* @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.
* @param {number} [updateDelay=1000] Delay in milliseconds after a zoom,
* rotate, or pan event before recomputing the heatmap.
* @returns {geo.heatmapFeature}
*/
//////////////////////////////////////////////////////////////////////////////
Expand All @@ -54,12 +56,15 @@ var heatmapFeature = function (arg) {
m_intensity,
m_maxIntensity,
m_minIntensity,
m_updateDelay,
m_gcsPosition,
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_maxIntensity = arg.maxIntensity !== undefined ? arg.maxIntensity : null;
m_minIntensity = arg.minIntensity !== undefined ? arg.minIntensity : null;
m_updateDelay = arg.updateDelay ? parseInt(arg.updateDelay, 10) : 1000;

////////////////////////////////////////////////////////////////////////////
/**
Expand Down Expand Up @@ -97,6 +102,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
Expand All @@ -115,6 +136,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
Expand Down Expand Up @@ -144,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},
Expand All @@ -171,23 +203,35 @@ var heatmapFeature = function (arg) {
////////////////////////////////////////////////////////////////////////////
this._build = function () {
var data = m_this.data(),
intensity = null;

if (!m_maxIntensity || !m_minIntensity) {
data.forEach(function (d) {
intensity = null,
position = [],
setMax = (m_maxIntensity === null || m_maxIntensity === undefined),
setMin = (m_minIntensity === null || m_minIntensity === undefined);

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);

m_this.buildTime().modified();
return m_this;
Expand Down
3 changes: 3 additions & 0 deletions src/transform.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading

0 comments on commit b3e15f8

Please sign in to comment.