OpenLayers Heatmap Layer (using <canvas>)
Usage
var heat = new Heatmap.Layer("Heatmap");
heat.addSource(new Heatmap.Source(new OpenLayers.LonLat(9.434, 54.740)));
heat.addSource(new Heatmap.Source(new OpenLayers.LonLat(9.833, 54.219)));
...
map.addLayer(heat);
map.zoomToExtent(heat.getDataExtent());
Code
/*
* Copyright (c) 2010 Bjoern Hoehrmann <http://bjoern.hoehrmann.de/>.
* This module is licensed under the same terms as OpenLayers itself.
*
*/
Heatmap = {};
/**
* Class: Heatmap.Source
*/
Heatmap.Source = OpenLayers.Class({
/**
* APIProperty: lonlat
* {OpenLayers.LonLat} location of the heat source
*/
lonlat: null,
/**
* APIProperty: radius
* {Number} Heat source radius
*/
radius: null,
/**
* APIProperty: intensity
* {Number} Heat source intensity
*/
intensity: null,
/**
* Constructor: Heatmap.Source
* Create a heat source.
*
* Parameters:
* lonlat - {OpenLayers.LonLat} Coordinates of the heat source
* radius - {Number} Optional radius
* intensity - {Number} Optional intensity
*/
initialize: function(lonlat, radius, intensity) {
this.lonlat = lonlat;
this.radius = radius;
this.intensity = intensity;
},
CLASS_NAME: 'Heatmap.Source'
});
/**
* Class: Heatmap.Layer
*
* Inherits from:
* - <OpenLayers.Layer>
*/
Heatmap.Layer = OpenLayers.Class(OpenLayers.Layer, {
/**
* APIProperty: isBaseLayer
* {Boolean} Heatmap layer is never a base layer.
*/
isBaseLayer: false,
/**
* Property: points
* {Array(<Heatmap.Source>)} internal coordinate list
*/
points: null,
/**
* Property: cache
* {Object} Hashtable with CanvasGradient objects
*/
cache: null,
/**
* Property: gradient
* {Array(Number)} RGBA gradient map used to colorize the intensity map.
*/
gradient: null,
/**
* Property: canvas
* {DOMElement} Canvas element.
*/
canvas: null,
/**
* APIProperty: defaultRadius
* {Number} Heat source default radius
*/
defaultRadius: null,
/**
* APIProperty: defaultIntensity
* {Number} Heat source default intensity
*/
defaultIntensity: null,
/**
* Constructor: Heatmap.Layer
* Create a heatmap layer.
*
* Parameters:
* name - {String} Name of the Layer
* options - {Object} Hashtable of extra options to tag onto the layer
*/
initialize: function(name, options) {
OpenLayers.Layer.prototype.initialize.apply(this, arguments);
this.points = [];
this.cache = {};
this.canvas = document.createElement('canvas');
this.canvas.style.position = 'absolute';
this.defaultRadius = 20;
this.defaultIntensity = 0.2;
this.setGradientStops({
0.00: 0xffffff00,
0.10: 0x99e9fdff,
0.20: 0x00c9fcff,
0.30: 0x00e9fdff,
0.30: 0x00a5fcff,
0.40: 0x0078f2ff,
0.50: 0x0e53e9ff,
0.60: 0x4a2cd9ff,
0.70: 0x890bbfff,
0.80: 0x99019aff,
0.90: 0x990664ff,
0.99: 0x660000ff,
1.00: 0x000000ff
});
// For some reason OpenLayers.Layer.setOpacity assumes there is
// an additional div between the layer's div and its contents.
var sub = document.createElement('div');
sub.appendChild(this.canvas);
this.div.appendChild(sub);
},
/**
* APIMethod: setGradientStops
* ...
*
* Parameters:
* stops - {Object} Hashtable with stop position as keys and colors
* as values. Stop positions are numbers between 0
* and 1, color values numbers in 0xRRGGBBAA form.
*/
setGradientStops: function(stops) {
// There is no need to perform the linear interpolation manually,
// it is sufficient to let the canvas implementation do that.
var ctx = document.createElement('canvas').getContext('2d');
var grd = ctx.createLinearGradient(0, 0, 256, 0);
for (var i in stops) {
grd.addColorStop(i, 'rgba(' +
((stops[i] >> 24) & 0xFF) + ',' +
((stops[i] >> 16) & 0xFF) + ',' +
((stops[i] >> 8) & 0xFF) + ',' +
((stops[i] >> 0) & 0xFF) + ')');
}
ctx.fillStyle = grd;
ctx.fillRect(0, 0, 256, 1);
this.gradient = ctx.getImageData(0, 0, 256, 1).data;
},
/**
* APIMethod: addSource
* Adds a heat source to the layer.
*
* Parameters:
* source - {<Heatmap.Source>}
*/
addSource: function(source) {
this.points.push(source);
},
/**
* APIMethod: removeSource
* Removes a heat source from the layer.
*
* Parameters:
* source - {<Heatmap.Source>}
*/
removeSource: function(source) {
if (this.points && this.points.length) {
OpenLayers.Util.removeItem(this.points, source);
}
},
/**
* Method: moveTo
*
* Parameters:
* bounds - {<OpenLayers.Bounds>}
* zoomChanged - {Boolean}
* dragging - {Boolean}
*/
moveTo: function(bounds, zoomChanged, dragging) {
OpenLayers.Layer.prototype.moveTo.apply(this, arguments);
// The code is too slow to update the rendering during dragging.
if (dragging)
return;
// Pick some point on the map and use it to determine the offset
// between the map's 0,0 coordinate and the layer's 0,0 position.
var someLoc = new OpenLayers.LonLat(0,0);
var offsetX = this.map.getViewPortPxFromLonLat(someLoc).x -
this.map.getLayerPxFromLonLat(someLoc).x;
var offsetY = this.map.getViewPortPxFromLonLat(someLoc).y -
this.map.getLayerPxFromLonLat(someLoc).y;
this.canvas.width = this.map.getSize().w;
this.canvas.height = this.map.getSize().h;
var ctx = this.canvas.getContext('2d');
ctx.save(); // Workaround for a bug in Google Chrome
ctx.fillStyle = 'transparent';
ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
ctx.restore();
for (var i in this.points) {
var src = this.points[i];
var rad = src.radius || this.defaultRadius;
var int = src.intensity || this.defaultIntensity;
var pos = this.map.getLayerPxFromLonLat(src.lonlat);
var x = pos.x - rad + offsetX;
var y = pos.y - rad + offsetY;
if (!this.cache[int]) {
this.cache[int] = {};
}
if (!this.cache[int][rad]) {
var grd = ctx.createRadialGradient(rad, rad, 0, rad, rad, rad);
grd.addColorStop(0.0, 'rgba(0, 0, 0, ' + int + ')');
grd.addColorStop(1.0, 'transparent');
this.cache[int][rad] = grd;
}
ctx.fillStyle = this.cache[int][rad];
ctx.translate(x, y);
ctx.fillRect(0, 0, 2 * rad, 2 * rad);
ctx.translate(-x, -y);
}
var dat = ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
var dim = this.canvas.width * this.canvas.height * 4;
var pix = dat.data;
for (var p = 0; p < dim; /* */) {
var a = pix[ p + 3 ] * 4;
pix[ p++ ] = this.gradient[ a++ ];
pix[ p++ ] = this.gradient[ a++ ];
pix[ p++ ] = this.gradient[ a++ ];
pix[ p++ ] = this.gradient[ a++ ];
}
ctx.putImageData(dat, 0, 0);
// Unfortunately OpenLayers does not currently support layers that
// remain in a fixed position with respect to the screen location
// of the base layer, so this puts this layer manually back into
// that position using one point's offset as determined earlier.
this.canvas.style.left = (-offsetX) + 'px';
this.canvas.style.top = (-offsetY) + 'px';
},
/**
* APIMethod: getDataExtent
* Calculates the max extent which includes all of the heat sources.
*
* Returns:
* {<OpenLayers.Bounds>}
*/
getDataExtent: function () {
var maxExtent = null;
if (this.points && (this.points.length > 0)) {
var maxExtent = new OpenLayers.Bounds();
for(var i = 0, len = this.points.length; i < len; ++i) {
var point = this.points[i];
maxExtent.extend(point.lonlat);
}
}
return maxExtent;
},
CLASS_NAME: 'Heatmap.Layer'
});