轉自原文 Cesium熱力圖實現
生成熱力圖的算法我是用的一個熱力圖插件 heatmap.js。
heatmap中熱力圖生成原理:
heatmap中首先會根據輸入的漸進色參數,在內部生成一個0-255色值的調色板。
var _getColorPalette = function(config) {
var gradientConfig = config.gradient || config.defaultGradient;
var paletteCanvas = document.createElement('canvas');
var paletteCtx = paletteCanvas.getContext('2d');
paletteCanvas.width = 256;
paletteCanvas.height = 1;
var gradient = paletteCtx.createLinearGradient(0, 0, 256, 1);
for (var key in gradientConfig) {
gradient.addColorStop(key, gradientConfig[key]);
}
paletteCtx.fillStyle = gradient;
paletteCtx.fillRect(0, 0, 256, 1);
return paletteCtx.getImageData(0, 0, 256, 1).data;
};
對於輸入的點數據,會根據點坐標生成一個黑色圓陰影效果。
//生成一個陰影模板
var _getPointTemplate = function(radius, blurFactor) {
var tplCanvas = document.createElement('canvas');
var tplCtx = tplCanvas.getContext('2d');
var x = radius;
var y = radius;
tplCanvas.width = tplCanvas.height = radius*2;
if (blurFactor == 1) {
tplCtx.beginPath();
tplCtx.arc(x, y, radius, 0, 2 * Math.PI, false);
tplCtx.fillStyle = 'rgba(0,0,0,1)';
tplCtx.fill();
} else {
var gradient = tplCtx.createRadialGradient(x, y, radius*blurFactor, x, y, radius);
gradient.addColorStop(0, 'rgba(0,0,0,1)');
gradient.addColorStop(1, 'rgba(0,0,0,0)');
tplCtx.fillStyle = gradient;
tplCtx.fillRect(0, 0, 2*radius, 2*radius);
}
var tpl;
if (!this._templates[radius]) {
this._templates[radius] = tpl = _getPointTemplate(radius, blur);
} else {
tpl = this._templates[radius];
}
// value from minimum / value range
// => [0, 1]
//根據value值設置陰影的alpha通道,后面可以通過alpha值獲取value值
var templateAlpha = (value-min)/(max-min);
// this fixes #176: small values are not visible because globalAlpha < .01 cannot be read from imageData
shadowCtx.globalAlpha = templateAlpha < .01 ? .01 : templateAlpha;
shadowCtx.drawImage(tpl, rectX, rectY);
// update renderBoundaries
if (rectX < this._renderBoundaries[0]) {
this._renderBoundaries[0] = rectX;
}
if (rectY < this._renderBoundaries[1]) {
this._renderBoundaries[1] = rectY;
}
if (rectX + 2*radius > this._renderBoundaries[2]) {
this._renderBoundaries[2] = rectX + 2*radius;
}
if (rectY + 2*radius > this._renderBoundaries[3]) {
this._renderBoundaries[3] = rectY + 2*radius;
}
}
首先呢,陰影是黑色的,所以接下來heatmap會進行一個像素點重新着色的過程,根據每個點的alpha值*4(rgba步長)得出一個offset,然后從調色板上取顏色。因為上面設置了陰影透明度效果是遞減的,所以在獲取顏色的時候,就能獲得一個平滑的漸變效果。這樣就得到了熱力圖。
_colorize: function() {
var x = this._renderBoundaries[0];
var y = this._renderBoundaries[1];
var width = this._renderBoundaries[2] - x;
var height = this._renderBoundaries[3] - y;
var maxWidth = this._width;
var maxHeight = this._height;
var opacity = this._opacity;
var maxOpacity = this._maxOpacity;
var minOpacity = this._minOpacity;
var useGradientOpacity = this._useGradientOpacity;
if (x < 0) {
x = 0;
}
if (y < 0) {
y = 0;
}
if (x + width > maxWidth) {
width = maxWidth - x;
}
if (y + height > maxHeight) {
height = maxHeight - y;
}
var img = this.shadowCtx.getImageData(x, y, width, height);
var imgData = img.data;
var len = imgData.length;
var palette = this._palette;
for (var i = 3; i < len; i+= 4) {
var alpha = imgData[i];
var offset = alpha * 4;
if (!offset) {
continue;
}
var finalAlpha;
if (opacity > 0) {
finalAlpha = opacity;
} else {
if (alpha < maxOpacity) {
if (alpha < minOpacity) {
finalAlpha = minOpacity;
} else {
finalAlpha = alpha;
}
} else {
finalAlpha = maxOpacity;
}
}
imgData[i-3] = palette[offset];
imgData[i-2] = palette[offset + 1];
imgData[i-1] = palette[offset + 2];
imgData[i] = useGradientOpacity ? palette[offset + 3] : finalAlpha;
}
img.data = imgData;
this.ctx.putImageData(img, x, y);
this._renderBoundaries = [1000, 1000, 0, 0];
},
采用這種利用透明度來對應獲取顏色值的好處就是這種漸變的過程比較柔和,漸變的效果更好。
so一開始為什么先繪制alpha漸變的黑點呢?是因為在純色圖像上方便計算它的alpha分量,這樣兩點相交的區域就會根據alpha分量進行疊加,在轉成彩圖的時候就可以生成相應的值。
為什么不直接用彩色點疊加呢?是因為彩色點的RGBA並不是簡單的線性疊加關系。
在Cesium上使用的原理比較簡單,就是根據輸入點的坐標范圍計算一個包圍盒,創建一個rectangle geometry。然后呢,通過heatmap.js生成熱力圖,當做紋理貼到rectangle上面。在每一層級設置不同的radius,相當於在相機縮放的時候每一級都會生成一張熱力圖,然后更換紋理,實現縮放時的聚合離散效果。
這個過程需要注意的是以下幾點:
1. 如何將經緯度值映射到紋理上對應位置?
首先需要計算生成紋理的寬高像素,這里我仿照了cesiumheatmap的算法,根據rectangle投影后的范圍和初始heatmap的設置的canvasSize參數來計算出一個寬高值。
2.用heatmap繪出的canvas貼到rectangle上會有黑色背景
這個可以在shader里面將黑色像素過濾掉即可。
'vec4 heightValue = texture2D(image, materialInput.st);' +
'if(heightValue.r<1.0/255.0) heightValue.a= 0.0; ' +
'if(heightValue.r<1.0/255.0) heightValue.a= 0.0; ' +
3.縮放實現聚合離散
上面提到過了,根據幾個層級范圍設置一個radius數組,相機縮放到哪個層級就相應的改變它的radius進行重繪,然后替換紋理。
最終實現效果還可以,能夠平滑過渡。
在Openlayer中實現熱力圖起始是很方便的。具體可參考下面的幾篇文章。


進一步學習的參考資料
至於繪制的過程和原理、及完整代碼,可以參考
http://www.wangshaoxing.com/blog/how-to-draw-a-heatmap.html
code
https://github.com/wshxbqq/WebGL-HeatMap
