931 lines
31 KiB
JavaScript
931 lines
31 KiB
JavaScript
|
||
|
||
(function (w) {
|
||
// the heatmapFactory creates heatmap instances
|
||
var heatmapFactory = (function(){
|
||
|
||
// store object constructor
|
||
// a heatmap contains a store
|
||
// the store has to know about the heatmap in order to trigger heatmap updates when datapoints get added
|
||
var store = function store(hmap) {
|
||
|
||
var _ = {
|
||
// data is a two dimensional array
|
||
// a datapoint gets saved as data[point-x-value][point-y-value]
|
||
// the value at [point-x-value][point-y-value] is the occurrence of the datapoint
|
||
data: [],
|
||
// tight coupling of the heatmap object
|
||
heatmap: hmap
|
||
};
|
||
// the max occurrence - the heatmaps radial gradient alpha transition is based on it
|
||
this.max = 1;
|
||
|
||
this.get = function (key) {
|
||
return _[key];
|
||
};
|
||
this.set = function (key, value) {
|
||
_[key] = value;
|
||
};
|
||
}
|
||
|
||
store.prototype = {
|
||
// function for adding datapoints to the store
|
||
// datapoints are usually defined by x and y but could also contain a third parameter which represents the occurrence
|
||
addDataPoint: function (x, y) {
|
||
if (x < 0 || y < 0)
|
||
return;
|
||
|
||
var me = this,
|
||
heatmap = me.get("heatmap"),
|
||
data = me.get("data");
|
||
|
||
if (!data[x])
|
||
data[x] = [];
|
||
|
||
if (!data[x][y])
|
||
data[x][y] = 0;
|
||
|
||
// if count parameter is set increment by count otherwise by 1
|
||
data[x][y] += (arguments.length < 3) ? 1 : arguments[2];
|
||
|
||
me.set("data", data);
|
||
// do we have a new maximum?
|
||
if (me.max < data[x][y]) {
|
||
// max changed, we need to redraw all existing(lower) datapoints
|
||
heatmap.get("actx").clearRect(0, 0, heatmap.get("width"), heatmap.get("height"));
|
||
me.setDataSet({ max: data[x][y], data: data }, true);
|
||
return;
|
||
}
|
||
heatmap.drawAlpha(x, y, data[x][y], true);
|
||
},
|
||
setDataSet: function (obj, internal) {
|
||
var me = this,
|
||
heatmap = me.get("heatmap"),
|
||
data = [],
|
||
d = obj.data,
|
||
dlen = d.length;
|
||
// clear the heatmap before the data set gets drawn
|
||
heatmap.clear();
|
||
this.max = obj.max;
|
||
// if a legend is set, update it
|
||
heatmap.get("legend") && heatmap.get("legend").update(obj.max);
|
||
|
||
if (internal != null && internal) {
|
||
for (var one in d) {
|
||
// jump over undefined indexes
|
||
if (one === undefined)
|
||
continue;
|
||
for (var two in d[one]) {
|
||
if (two === undefined)
|
||
continue;
|
||
// if both indexes are defined, push the values into the array
|
||
heatmap.drawAlpha(one, two, d[one][two], false);
|
||
}
|
||
}
|
||
} else {
|
||
while (dlen--) {
|
||
var point = d[dlen];
|
||
heatmap.drawAlpha(point.x, point.y, point.count, false);
|
||
if (!data[point.x])
|
||
data[point.x] = [];
|
||
|
||
if (!data[point.x][point.y])
|
||
data[point.x][point.y] = 0;
|
||
|
||
data[point.x][point.y] = point.count;
|
||
}
|
||
}
|
||
heatmap.colorize();
|
||
this.set("data", d);
|
||
},
|
||
exportDataSet: function () {
|
||
var me = this,
|
||
data = me.get("data"),
|
||
exportData = [];
|
||
|
||
for (var one in data) {
|
||
// jump over undefined indexes
|
||
if (one === undefined)
|
||
continue;
|
||
for (var two in data[one]) {
|
||
if (two === undefined)
|
||
continue;
|
||
// if both indexes are defined, push the values into the array
|
||
exportData.push({ x: parseInt(one, 10), y: parseInt(two, 10), count: data[one][two] });
|
||
}
|
||
}
|
||
|
||
return { max: me.max, data: exportData };
|
||
},
|
||
generateRandomDataSet: function (points) {
|
||
var heatmap = this.get("heatmap"),
|
||
w = heatmap.get("width"),
|
||
h = heatmap.get("height");
|
||
var randomset = {},
|
||
max = Math.floor(Math.random() * 1000 + 1);
|
||
randomset.max = max;
|
||
var data = [];
|
||
while (points--) {
|
||
data.push({ x: Math.floor(Math.random() * w + 1), y: Math.floor(Math.random() * h + 1), count: Math.floor(Math.random() * max + 1) });
|
||
}
|
||
randomset.data = data;
|
||
this.setDataSet(randomset);
|
||
}
|
||
};
|
||
|
||
var legend = function legend(config) {
|
||
this.config = config;
|
||
|
||
var _ = {
|
||
element: null,
|
||
labelsEl: null,
|
||
gradientCfg: null,
|
||
ctx: null
|
||
};
|
||
this.get = function (key) {
|
||
return _[key];
|
||
};
|
||
this.set = function (key, value) {
|
||
_[key] = value;
|
||
};
|
||
this.init();
|
||
};
|
||
legend.prototype = {
|
||
init: function () {
|
||
var me = this,
|
||
config = me.config,
|
||
title = config.title || "Legend",
|
||
position = config.position,
|
||
offset = config.offset || 10,
|
||
gconfig = config.gradient,
|
||
labelsEl = document.createElement("ul"),
|
||
labelsHtml = "",
|
||
grad, element, gradient, positionCss = "";
|
||
|
||
me.processGradientObject();
|
||
|
||
// Positioning
|
||
|
||
// top or bottom
|
||
if (position.indexOf('t') > -1) {
|
||
positionCss += 'top:' + offset + 'px;';
|
||
} else {
|
||
positionCss += 'bottom:' + offset + 'px;';
|
||
}
|
||
|
||
// left or right
|
||
if (position.indexOf('l') > -1) {
|
||
positionCss += 'left:' + offset + 'px;';
|
||
} else {
|
||
positionCss += 'right:' + offset + 'px;';
|
||
}
|
||
|
||
element = document.createElement("div");
|
||
element.style.cssText = "border-radius:5px;position:absolute;" + positionCss + "font-family:Helvetica; width:256px;z-index:10000000000; background:rgba(255,255,255,1);padding:10px;border:1px solid black;margin:0;";
|
||
element.innerHTML = "<h3 style='padding:0;margin:0;text-align:center;font-size:16px;'>" + title + "</h3>";
|
||
// create gradient in canvas
|
||
labelsEl.style.cssText = "position:relative;font-size:12px;display:block;list-style:none;list-style-type:none;margin:0;height:15px;";
|
||
|
||
|
||
// create gradient element
|
||
gradient = document.createElement("div");
|
||
gradient.style.cssText = ["position:relative;display:block;width:256px;height:15px;border-bottom:1px solid black; background-image:url(", me.createGradientImage(), ");"].join("");
|
||
|
||
element.appendChild(labelsEl);
|
||
element.appendChild(gradient);
|
||
|
||
me.set("element", element);
|
||
me.set("labelsEl", labelsEl);
|
||
|
||
me.update(1);
|
||
},
|
||
processGradientObject: function () {
|
||
// create array and sort it
|
||
var me = this,
|
||
gradientConfig = this.config.gradient,
|
||
gradientArr = [];
|
||
|
||
for (var key in gradientConfig) {
|
||
if (gradientConfig.hasOwnProperty(key)) {
|
||
gradientArr.push({ stop: key, value: gradientConfig[key] });
|
||
}
|
||
}
|
||
gradientArr.sort(function (a, b) {
|
||
return (a.stop - b.stop);
|
||
});
|
||
gradientArr.unshift({ stop: 0, value: 'rgba(0,0,0,0)' });
|
||
|
||
me.set("gradientArr", gradientArr);
|
||
},
|
||
createGradientImage: function () {
|
||
var me = this,
|
||
gradArr = me.get("gradientArr"),
|
||
length = gradArr.length,
|
||
canvas = document.createElement("canvas"),
|
||
ctx = canvas.getContext("2d"),
|
||
grad;
|
||
// the gradient in the legend including the ticks will be 256x15px
|
||
canvas.width = "256";
|
||
canvas.height = "15";
|
||
|
||
grad = ctx.createLinearGradient(0, 5, 256, 10);
|
||
|
||
for (var i = 0; i < length; i++) {
|
||
grad.addColorStop(1 / (length - 1) * i, gradArr[i].value);
|
||
}
|
||
|
||
ctx.fillStyle = grad;
|
||
ctx.fillRect(0, 5, 256, 10);
|
||
ctx.strokeStyle = "black";
|
||
ctx.beginPath();
|
||
|
||
for (var i = 0; i < length; i++) {
|
||
ctx.moveTo(((1 / (length - 1) * i * 256) >> 0) + .5, 0);
|
||
ctx.lineTo(((1 / (length - 1) * i * 256) >> 0) + .5, (i == 0) ? 15 : 5);
|
||
}
|
||
ctx.moveTo(255.5, 0);
|
||
ctx.lineTo(255.5, 15);
|
||
ctx.moveTo(255.5, 4.5);
|
||
ctx.lineTo(0, 4.5);
|
||
|
||
ctx.stroke();
|
||
|
||
// we re-use the context for measuring the legends label widths
|
||
me.set("ctx", ctx);
|
||
|
||
return canvas.toDataURL();
|
||
},
|
||
getElement: function () {
|
||
return this.get("element");
|
||
},
|
||
update: function (max) {
|
||
var me = this,
|
||
gradient = me.get("gradientArr"),
|
||
ctx = me.get("ctx"),
|
||
labels = me.get("labelsEl"),
|
||
labelText, labelsHtml = "", offset;
|
||
|
||
for (var i = 0; i < gradient.length; i++) {
|
||
|
||
labelText = max * gradient[i].stop >> 0;
|
||
offset = (ctx.measureText(labelText).width / 2) >> 0;
|
||
|
||
if (i == 0) {
|
||
offset = 0;
|
||
}
|
||
if (i == gradient.length - 1) {
|
||
offset *= 2;
|
||
}
|
||
labelsHtml += '<li style="position:absolute;left:' + (((((1 / (gradient.length - 1) * i * 256) || 0)) >> 0) - offset + .5) + 'px">' + labelText + '</li>';
|
||
}
|
||
labels.innerHTML = labelsHtml;
|
||
}
|
||
};
|
||
|
||
// heatmap object constructor
|
||
var heatmap = function heatmap(config) {
|
||
// private variables
|
||
var _ = {
|
||
radius: 40,
|
||
element: {},
|
||
canvas: {},
|
||
acanvas: {},
|
||
ctx: {},
|
||
actx: {},
|
||
legend: null,
|
||
visible: true,
|
||
width: 0,
|
||
height: 0,
|
||
max: false,
|
||
gradient: false,
|
||
opacity: 180,
|
||
premultiplyAlpha: false,
|
||
bounds: {
|
||
l: 1000,
|
||
r: 0,
|
||
t: 1000,
|
||
b: 0
|
||
},
|
||
debug: false
|
||
};
|
||
// heatmap store containing the datapoints and information about the maximum
|
||
// accessible via instance.store
|
||
this.store = new store(this);
|
||
|
||
this.get = function (key) {
|
||
return _[key];
|
||
};
|
||
this.set = function (key, value) {
|
||
_[key] = value;
|
||
};
|
||
// configure the heatmap when an instance gets created
|
||
this.configure(config);
|
||
// and initialize it
|
||
this.init();
|
||
};
|
||
|
||
// public functions
|
||
heatmap.prototype = {
|
||
configure: function (config) {
|
||
var me = this,
|
||
rout, rin;
|
||
|
||
me.set("radius", config["radius"] || 40);
|
||
me.set("element", (config.element instanceof Object) ? config.element : document.getElementById(config.element));
|
||
me.set("visible", (config.visible != null) ? config.visible : true);
|
||
me.set("max", config.max || false);
|
||
me.set("gradient", config.gradient || { 0.45: "rgb(0,0,255)", 0.55: "rgb(0,255,255)", 0.65: "rgb(0,255,0)", 0.95: "yellow", 1.0: "rgb(255,0,0)" }); // default is the common blue to red gradient
|
||
me.set("opacity", parseInt(255 / (100 / config.opacity), 10) || 180);
|
||
me.set("width", config.width || 0);
|
||
me.set("height", config.height || 0);
|
||
me.set("debug", config.debug);
|
||
|
||
if (config.legend) {
|
||
var legendCfg = config.legend;
|
||
legendCfg.gradient = me.get("gradient");
|
||
me.set("legend", new legend(legendCfg));
|
||
}
|
||
|
||
},
|
||
resize: function () {
|
||
var me = this,
|
||
element = me.get("element"),
|
||
canvas = me.get("canvas"),
|
||
acanvas = me.get("acanvas");
|
||
canvas.width = acanvas.width = me.get("width") || element.style.width.replace(/px/, "") || me.getWidth(element);
|
||
this.set("width", canvas.width);
|
||
canvas.height = acanvas.height = me.get("height") || element.style.height.replace(/px/, "") || me.getHeight(element);
|
||
this.set("height", canvas.height);
|
||
},
|
||
|
||
init: function () {
|
||
var me = this,
|
||
canvas = document.createElement("canvas"),
|
||
acanvas = document.createElement("canvas"),
|
||
ctx = canvas.getContext("2d"),
|
||
actx = acanvas.getContext("2d"),
|
||
element = me.get("element");
|
||
|
||
|
||
me.initColorPalette();
|
||
|
||
me.set("canvas", canvas);
|
||
me.set("ctx", ctx);
|
||
me.set("acanvas", acanvas);
|
||
me.set("actx", actx);
|
||
|
||
me.resize();
|
||
canvas.style.cssText = acanvas.style.cssText = "position:absolute;top:0;left:0;z-index:1;";
|
||
|
||
if (!me.get("visible"))
|
||
canvas.style.display = "none";
|
||
|
||
element.appendChild(canvas);
|
||
if (me.get("legend")) {
|
||
element.appendChild(me.get("legend").getElement());
|
||
}
|
||
|
||
// debugging purposes only
|
||
if (me.get("debug"))
|
||
document.body.appendChild(acanvas);
|
||
|
||
actx.shadowOffsetX = 15000;
|
||
actx.shadowOffsetY = 15000;
|
||
actx.shadowBlur = 15;
|
||
},
|
||
initColorPalette: function () {
|
||
|
||
var me = this,
|
||
canvas = document.createElement("canvas"),
|
||
gradient = me.get("gradient"),
|
||
ctx, grad, testData;
|
||
|
||
canvas.width = "1";
|
||
canvas.height = "256";
|
||
ctx = canvas.getContext("2d");
|
||
grad = ctx.createLinearGradient(0, 0, 1, 256);
|
||
|
||
// Test how the browser renders alpha by setting a partially transparent pixel
|
||
// and reading the result. A good browser will return a value reasonably close
|
||
// to what was set. Some browsers (e.g. on Android) will return a ridiculously wrong value.
|
||
testData = ctx.getImageData(0, 0, 1, 1);
|
||
testData.data[0] = testData.data[3] = 64; // 25% red & alpha
|
||
testData.data[1] = testData.data[2] = 0; // 0% blue & green
|
||
ctx.putImageData(testData, 0, 0);
|
||
testData = ctx.getImageData(0, 0, 1, 1);
|
||
me.set("premultiplyAlpha", (testData.data[0] < 60 || testData.data[0] > 70));
|
||
|
||
for (var x in gradient) {
|
||
grad.addColorStop(x, gradient[x]);
|
||
}
|
||
|
||
ctx.fillStyle = grad;
|
||
ctx.fillRect(0, 0, 1, 256);
|
||
|
||
me.set("gradient", ctx.getImageData(0, 0, 1, 256).data);
|
||
},
|
||
getWidth: function (element) {
|
||
var width = element.offsetWidth;
|
||
if (element.style.paddingLeft) {
|
||
width += element.style.paddingLeft;
|
||
}
|
||
if (element.style.paddingRight) {
|
||
width += element.style.paddingRight;
|
||
}
|
||
|
||
return width;
|
||
},
|
||
getHeight: function (element) {
|
||
var height = element.offsetHeight;
|
||
if (element.style.paddingTop) {
|
||
height += element.style.paddingTop;
|
||
}
|
||
if (element.style.paddingBottom) {
|
||
height += element.style.paddingBottom;
|
||
}
|
||
|
||
return height;
|
||
},
|
||
colorize: function (x, y) {
|
||
// get the private variables
|
||
var me = this,
|
||
width = me.get("width"),
|
||
radius = me.get("radius"),
|
||
height = me.get("height"),
|
||
actx = me.get("actx"),
|
||
ctx = me.get("ctx"),
|
||
x2 = radius * 3,
|
||
premultiplyAlpha = me.get("premultiplyAlpha"),
|
||
palette = me.get("gradient"),
|
||
opacity = me.get("opacity"),
|
||
bounds = me.get("bounds"),
|
||
left, top, bottom, right,
|
||
image, imageData, length, alpha, offset, finalAlpha;
|
||
|
||
if (x != null && y != null) {
|
||
if (x + x2 > width) {
|
||
x = width - x2;
|
||
}
|
||
if (x < 0) {
|
||
x = 0;
|
||
}
|
||
if (y < 0) {
|
||
y = 0;
|
||
}
|
||
if (y + x2 > height) {
|
||
y = height - x2;
|
||
}
|
||
left = x;
|
||
top = y;
|
||
right = x + x2;
|
||
bottom = y + x2;
|
||
|
||
} else {
|
||
if (bounds['l'] < 0) {
|
||
left = 0;
|
||
} else {
|
||
left = bounds['l'];
|
||
}
|
||
if (bounds['r'] > width) {
|
||
right = width;
|
||
} else {
|
||
right = bounds['r'];
|
||
}
|
||
if (bounds['t'] < 0) {
|
||
top = 0;
|
||
} else {
|
||
top = bounds['t'];
|
||
}
|
||
if (bounds['b'] > height) {
|
||
bottom = height;
|
||
} else {
|
||
bottom = bounds['b'];
|
||
}
|
||
}
|
||
|
||
image = actx.getImageData(left, top, right - left, bottom - top);
|
||
imageData = image.data;
|
||
length = imageData.length;
|
||
// loop thru the area
|
||
for (var i = 3; i < length; i += 4) {
|
||
|
||
// [0] -> r, [1] -> g, [2] -> b, [3] -> alpha
|
||
alpha = imageData[i],
|
||
offset = alpha * 4;
|
||
|
||
if (!offset)
|
||
continue;
|
||
|
||
// we ve started with i=3
|
||
// set the new r, g and b values
|
||
finalAlpha = (alpha < opacity) ? alpha : opacity;
|
||
imageData[i - 3] = palette[offset];
|
||
imageData[i - 2] = palette[offset + 1];
|
||
imageData[i - 1] = palette[offset + 2];
|
||
|
||
if (premultiplyAlpha) {
|
||
// To fix browsers that premultiply incorrectly, we'll pass in a value scaled
|
||
// appropriately so when the multiplication happens the correct value will result.
|
||
imageData[i - 3] /= 255 / finalAlpha;
|
||
imageData[i - 2] /= 255 / finalAlpha;
|
||
imageData[i - 1] /= 255 / finalAlpha;
|
||
}
|
||
|
||
// we want the heatmap to have a gradient from transparent to the colors
|
||
// as long as alpha is lower than the defined opacity (maximum), we'll use the alpha value
|
||
imageData[i] = finalAlpha;
|
||
}
|
||
// the rgb data manipulation didn't affect the ImageData object(defined on the top)
|
||
// after the manipulation process we have to set the manipulated data to the ImageData object
|
||
image.data = imageData;
|
||
ctx.putImageData(image, left, top);
|
||
},
|
||
drawAlpha: function (x, y, count, colorize) {
|
||
// storing the variables because they will be often used
|
||
var me = this,
|
||
radius = me.get("radius"),
|
||
ctx = me.get("actx"),
|
||
max = me.get("max"),
|
||
bounds = me.get("bounds"),
|
||
xb = x - (1.5 * radius) >> 0, yb = y - (1.5 * radius) >> 0,
|
||
xc = x + (1.5 * radius) >> 0, yc = y + (1.5 * radius) >> 0;
|
||
|
||
ctx.shadowColor = ('rgba(0,0,0,' + ((count) ? (count / me.store.max) : '0.1') + ')');
|
||
|
||
ctx.shadowOffsetX = 15000;
|
||
ctx.shadowOffsetY = 15000;
|
||
ctx.shadowBlur = 15;
|
||
|
||
ctx.beginPath();
|
||
ctx.arc(x - 15000, y - 15000, radius, 0, Math.PI * 2, true);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
if (colorize) {
|
||
// finally colorize the area
|
||
me.colorize(xb, yb);
|
||
} else {
|
||
// or update the boundaries for the area that then should be colorized
|
||
if (xb < bounds["l"]) {
|
||
bounds["l"] = xb;
|
||
}
|
||
if (yb < bounds["t"]) {
|
||
bounds["t"] = yb;
|
||
}
|
||
if (xc > bounds['r']) {
|
||
bounds['r'] = xc;
|
||
}
|
||
if (yc > bounds['b']) {
|
||
bounds['b'] = yc;
|
||
}
|
||
}
|
||
},
|
||
toggleDisplay: function () {
|
||
var me = this,
|
||
visible = me.get("visible"),
|
||
canvas = me.get("canvas");
|
||
|
||
if (!visible)
|
||
canvas.style.display = "block";
|
||
else
|
||
canvas.style.display = "none";
|
||
|
||
me.set("visible", !visible);
|
||
},
|
||
// dataURL export
|
||
getImageData: function () {
|
||
return this.get("canvas").toDataURL();
|
||
},
|
||
clear: function () {
|
||
var me = this,
|
||
w = me.get("width"),
|
||
h = me.get("height");
|
||
|
||
me.store.set("data", []);
|
||
// @TODO: reset stores max to 1
|
||
//me.store.max = 1;
|
||
me.get("ctx").clearRect(0, 0, w, h);
|
||
me.get("actx").clearRect(0, 0, w, h);
|
||
},
|
||
cleanup: function () {
|
||
var me = this;
|
||
me.get("element").removeChild(me.get("canvas"));
|
||
}
|
||
};
|
||
|
||
return {
|
||
create: function (config) {
|
||
return new heatmap(config);
|
||
},
|
||
util: {
|
||
mousePosition: function (ev) {
|
||
// this doesn't work right
|
||
// rather use
|
||
/*
|
||
// this = element to observe
|
||
var x = ev.pageX - this.offsetLeft;
|
||
var y = ev.pageY - this.offsetTop;
|
||
|
||
*/
|
||
var x, y;
|
||
|
||
if (ev.layerX) { // Firefox
|
||
x = ev.layerX;
|
||
y = ev.layerY;
|
||
} else if (ev.offsetX) { // Opera
|
||
x = ev.offsetX;
|
||
y = ev.offsetY;
|
||
}
|
||
if (typeof (x) == 'undefined')
|
||
return;
|
||
|
||
return [x, y];
|
||
}
|
||
}
|
||
};
|
||
})();
|
||
w.h337 = w.heatmapFactory = heatmapFactory;
|
||
}) (window);
|
||
|
||
|
||
/*==============================以上部分为heatmap.js的核心代码,只负责热力图的展现====================================*/
|
||
|
||
|
||
/*==============================以下部分为专为百度地图打造的覆盖物===================================================*/
|
||
/**
|
||
* @fileoverview 百度地图的热力图功能,对外开放。
|
||
* 主要基于http://www.patrick-wied.at/static/heatmapjs/index.html 修改而得
|
||
|
||
* 主入口类是<a href="symbols/BMapLib.Heatmap.html">Heatmap</a>,
|
||
* 基于Baidu Map API 2.0。
|
||
*
|
||
* @author Baidu Map Api Group
|
||
* @version 1.0
|
||
*/
|
||
|
||
/**
|
||
* @namespace
|
||
* BMap的所有library类均放在BMapLib命名空间下
|
||
*/
|
||
var BMapLib = window.BMapLib = BMapLib || {};
|
||
|
||
|
||
(function () {
|
||
/**
|
||
* @exports HeatmapOverlay as BMapLib.HeatmapOverlay
|
||
*/
|
||
var HeatmapOverlay =
|
||
/**
|
||
* 热力图的覆盖物
|
||
* @class 热力图的覆盖物
|
||
* 实例化该类后,使用map.addOverlay即可以添加热力图
|
||
*
|
||
* @constructor
|
||
* @param {Json Object} opts 可选的输入参数,非必填项。可输入选项包括:<br />
|
||
* {"<b>radius</b>" : {String} 热力图的半径,
|
||
* <br />"<b>visible</b>" : {Number} 热力图是否显示,
|
||
* <br />"<b>gradient</b>" : {JSON} 热力图的渐变区间,
|
||
* <br />"<b>opacity</b>" : {Number} 热力的透明度,
|
||
*
|
||
* @example <b>参考示例:</b><br />
|
||
* var map = new BMapGL.Map("container");<br />map.centerAndZoom(new BMapGL.Point(116.404, 39.915), 15);<br />var heatmapOverlay = new BMapLib.HeatmapOverlay({"radius":10, "visible":true, "opacity":70});<br />heatmapOverlay.setDataSet(data);//data是热力图的详细数据
|
||
*/
|
||
|
||
BMapLib.HeatmapOverlay = function (opts) {
|
||
this.conf = opts;
|
||
this.heatmap = null;
|
||
this.latlngs = [];
|
||
this.bounds = null;
|
||
}
|
||
|
||
HeatmapOverlay.prototype = new BMapGL.Overlay();
|
||
|
||
HeatmapOverlay.prototype.initialize = function (map) {
|
||
this._map = map;
|
||
var el = document.createElement("div");
|
||
el.style.position = "absolute";
|
||
el.style.top = 0;
|
||
el.style.left = 0;
|
||
el.style.border = 0;
|
||
el.style.width = this._map.getSize().width + "px";
|
||
el.style.height = this._map.getSize().height + "px";
|
||
this.conf.element = el;
|
||
|
||
if (!isSupportCanvas()) {//判断是否支持Canvas.
|
||
return el;
|
||
}
|
||
map.getContainer().appendChild(el);
|
||
this.heatmap = h337.create(this.conf);
|
||
this._div = el;
|
||
return el;
|
||
}
|
||
|
||
HeatmapOverlay.prototype.draw = function () {
|
||
if (!isSupportCanvas()) {//判断是否支持Canvas.
|
||
return;
|
||
}
|
||
var currentBounds = this._map.getBounds();
|
||
|
||
if (currentBounds.equals(this.bounds)) {
|
||
return;
|
||
}
|
||
this.bounds = currentBounds;
|
||
|
||
var ne = this._map.pointToOverlayPixel(currentBounds.getNorthEast()),
|
||
sw = this._map.pointToOverlayPixel(currentBounds.getSouthWest()),
|
||
topY = ne.y,
|
||
leftX = sw.x,
|
||
h = sw.y - ne.y,
|
||
w = ne.x - sw.x;
|
||
|
||
this.conf.element.style.left = leftX + 'px';
|
||
this.conf.element.style.top = topY + 'px';
|
||
this.conf.element.style.width = w + 'px';
|
||
this.conf.element.style.height = h + 'px';
|
||
this.heatmap.store.get("heatmap").resize();
|
||
|
||
if (this.latlngs.length > 0) {
|
||
this.heatmap.clear();
|
||
|
||
var len = this.latlngs.length;
|
||
d = {
|
||
max: this.heatmap.store.max,
|
||
data: []
|
||
};
|
||
|
||
while (len--) {
|
||
var latlng = this.latlngs[len].latlng;
|
||
|
||
if (!currentBounds.containsPoint(latlng)) {
|
||
continue;
|
||
}
|
||
|
||
var divPixel = this._map.pointToOverlayPixel(latlng),
|
||
screenPixel = new BMapGL.Pixel(divPixel.x - leftX, divPixel.y - topY);
|
||
var roundedPoint = this.pixelTransform(screenPixel);
|
||
d.data.push({
|
||
x: roundedPoint.x,
|
||
y: roundedPoint.y,
|
||
count: this.latlngs[len].c
|
||
});
|
||
}
|
||
this.heatmap.store.setDataSet(d);
|
||
}
|
||
}
|
||
|
||
|
||
//内部使用的坐标转化
|
||
HeatmapOverlay.prototype.pixelTransform = function (p) {
|
||
var w = this.heatmap.get("width"),
|
||
h = this.heatmap.get("height");
|
||
|
||
while (p.x < 0) {
|
||
p.x += w;
|
||
}
|
||
|
||
while (p.x > w) {
|
||
p.x -= w;
|
||
}
|
||
|
||
while (p.y < 0) {
|
||
p.y += h;
|
||
}
|
||
|
||
while (p.y > h) {
|
||
p.y -= h;
|
||
}
|
||
|
||
p.x = (p.x >> 0);
|
||
p.y = (p.y >> 0);
|
||
|
||
return p;
|
||
}
|
||
|
||
/**
|
||
* 设置热力图展现的详细数据, 实现之后,即可以立刻展现
|
||
* @param {Json Object } data
|
||
* {"<b>max</b>" : {Number} 权重的最大值,
|
||
* <br />"<b>data</b>" : {Array} 坐标详细数据,格式如下 <br/>
|
||
* {"lng":116.421969,"lat":39.913527,"count":3}, 其中<br/>
|
||
* lng lat分别为经纬度, count权重值
|
||
*/
|
||
HeatmapOverlay.prototype.setDataSet = function (data) {
|
||
this.data = data;
|
||
if (!isSupportCanvas()) {//判断是否支持Canvas.
|
||
return;
|
||
}
|
||
var currentBounds = this._map.getBounds();
|
||
var mapdata = {
|
||
max: data.max,
|
||
data: []
|
||
};
|
||
var d = data.data,
|
||
dlen = d.length;
|
||
|
||
|
||
this.latlngs = [];
|
||
|
||
while (dlen--) {
|
||
var latlng = new BMapGL.Point(d[dlen].lng, d[dlen].lat);
|
||
if (!currentBounds.containsPoint(latlng)) {
|
||
continue;
|
||
}
|
||
this.latlngs.push({
|
||
latlng: latlng,
|
||
c: d[dlen].count
|
||
});
|
||
|
||
var divPixel = this._map.pointToOverlayPixel(latlng),
|
||
leftX = this._map.pointToOverlayPixel(currentBounds.getSouthWest()).x,
|
||
topY = this._map.pointToOverlayPixel(currentBounds.getNorthEast()).y,
|
||
screenPixel = new BMapGL.Pixel(divPixel.x - leftX, divPixel.y - topY);
|
||
var point = this.pixelTransform(screenPixel);
|
||
|
||
mapdata.data.push({
|
||
x: point.x,
|
||
y: point.y,
|
||
count: d[dlen].count
|
||
});
|
||
}
|
||
this.heatmap.clear();
|
||
this.heatmap.store.setDataSet(mapdata);
|
||
}
|
||
|
||
/**
|
||
* 添加热力图的详细坐标点
|
||
* @param {Number} lng 经度坐标
|
||
* @param {Number} lat 经度坐标
|
||
* @param {Number} count 经度坐标
|
||
*/
|
||
HeatmapOverlay.prototype.addDataPoint = function (lng, lat, count) {
|
||
|
||
if (!isSupportCanvas()) {
|
||
return;
|
||
}
|
||
if (this.data && this.data.data) {
|
||
this.data.data.push({
|
||
lng: lng,
|
||
lat: lat,
|
||
count: count
|
||
});
|
||
}
|
||
|
||
var latlng = new BMapGL.Point(lng, lat),
|
||
point = this.pixelTransform(this._map.pointToOverlayPixel(latlng));
|
||
|
||
this.heatmap.store.addDataPoint(point.x, point.y, count);
|
||
this.latlngs.push({
|
||
latlng: latlng,
|
||
c: count
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 更改热力图的展现或者关闭
|
||
*/
|
||
|
||
HeatmapOverlay.prototype.toggle = function () {
|
||
if (!isSupportCanvas()) {//判断是否支持Canvas.
|
||
return;
|
||
}
|
||
this.heatmap.toggleDisplay();
|
||
}
|
||
/**
|
||
* 设置热力图展现的配置
|
||
* @param {Json Object} options 可选的输入参数,非必填项。可输入选项包括:<br />
|
||
* {"<b>radius</b>" : {String} 热力图的半径,
|
||
* <br />"<b>visible</b>" : {Number} 热力图是否显示,
|
||
* <br />"<b>gradient</b>" : {JSON} 热力图的渐变区间,
|
||
* <br />"<b>opacity</b>" : {Number} 热力的透明度,}
|
||
*/
|
||
HeatmapOverlay.prototype.setOptions = function (options) {
|
||
if (!isSupportCanvas()) {//判断是否支持Canvas.
|
||
return;
|
||
}
|
||
|
||
if (options) {
|
||
for (var key in options) {
|
||
this.heatmap.set(key, options[key]);
|
||
|
||
if (key == "gradient") {
|
||
this.heatmap.initColorPalette();
|
||
continue;
|
||
}
|
||
|
||
if (key == 'opacity') {
|
||
this.heatmap.set(key, parseInt(255 / (100 / options[key]), 10));
|
||
|
||
}
|
||
}
|
||
|
||
if (this.data) {
|
||
this.setDataSet(this.data);//重新渲染
|
||
}
|
||
}
|
||
}
|
||
|
||
function isSupportCanvas() {
|
||
var elem = document.createElement('canvas');
|
||
return !!(elem.getContext && elem.getContext('2d'));
|
||
}
|
||
})()
|