前言
编写这个专栏主要目的是对工作之中基于Cesium实现过的功能进行整合,有自己琢磨实现的,也有参考其他大神后整理实现的,初步算了算现在有差不多实现小130个左右的功能,后续也会不断的追加,所以暂时打算一周2-3更的样子来更新本专栏(尽可能把代码简洁一些)。博文内容如存在错误或者有可改进之处,也希望在这里和各位大佬交流提高一下。
更多内容/样例/demo说明:DEJA_VU3D完整功能目录
专栏内容本着尽可能简洁的原则,上一篇文章
DEJA_VU3D - Cesium功能集 之 082-热力图绘制原理_总要学点什么的博客-CSDN博客
我们有讲过热力图实现的原理,本篇我们就来详细的介绍基于Cesium来实现热力图的绘制,文章中包含了所有的源代码,最终的实现效果大致如下:
原理上一篇已经说过了,废话不多说,直接上代码:
关键代码
/**
* 创建热力图对象
* @param {*} box 范围对象,经纬度值-{west,south,east,north}
* @param {*} data 待绘制热力图的数据-[{x1,y1,value1},{x2,y2,value2},...]
* @returns 热力图结果对象
*/
createHeatmap(box, data) {
const mercator_WestSouth = this.WGS84ToWebMercator(box.west, box.south, 0); //左下位置(墨卡托)
const mercator_EastNorth = this.WGS84ToWebMercator(box.east, box.north, 0); //右上位置(墨卡托)
const diffDegrees_X = mercator_EastNorth.x - mercator_WestSouth.x;
const diffDegrees_Y = mercator_EastNorth.y - mercator_WestSouth.y;
const diffMax = Math.max(diffDegrees_X, diffDegrees_Y);
let multiple = diffMax / 300; //适当扩大倍数,以便绘制canvas
const width = Math.ceil(diffDegrees_X / multiple);
const height = Math.ceil(diffDegrees_Y / multiple);
this.mercator_WestSouth = mercator_WestSouth;
this.mercator_EastNorth = mercator_EastNorth;
this.diffDegrees_X = diffDegrees_X;
this.diffDegrees_Y = diffDegrees_Y;
let canvasData = [];
data.forEach((element) => {
const curMercator = this.WGS84ToWebMercator(
Number(element.x),
Number(element.y),
0
);
const per_X = (curMercator.x - mercator_WestSouth.x) / diffDegrees_X;
const currentPix_X = Math.ceil(per_X * width);
const per_Y = (curMercator.y - mercator_WestSouth.y) / diffDegrees_Y;
const currentPix_Y = Math.ceil(per_Y * height);
const currentValue = Number(element.value);
canvasData.push({
x: currentPix_X,
y: height - currentPix_Y - 1,
value: currentValue,
});
});
let canvas = new Canvas(width, height);
let context = canvas.getContext("2d");
context.clearRect(0, 0, canvas.width, canvas.height);
let circle = this.createCircle(this._size);
let circleHalfWidth = circle.width / 2;
let circleHalfHeight = circle.height / 2;
// 按透明度分类
let dataOrderByAlpha = {};
canvasData.forEach((item) => {
let alpha =
item.value < this._min
? 0
: Math.min(1, item.value / this._max).toFixed(2);
dataOrderByAlpha[alpha] = dataOrderByAlpha[alpha] || [];
dataOrderByAlpha[alpha].push(item);
});
// 绘制不同透明度的圆形
for (let i in dataOrderByAlpha) {
if (isNaN(i)) continue;
let _data = dataOrderByAlpha[i];
context.beginPath();
context.globalAlpha = i;
_data.forEach((item) => {
context.drawImage(
circle,
item.x - circleHalfWidth,
item.y - circleHalfHeight
);
});
}
// 圆形着色
let intensity = new Intensity();
let colored = context.getImageData(
0,
0,
context.canvas.width,
context.canvas.height
);
const options = { min: 0, max: this._max, size: this._size };
this.colorize(options, colored.data, intensity.getImageData());
context.clearRect(0, 0, context.canvas.width, context.canvas.height);
context.putImageData(colored, 0, 0);
let entity = this._viewer.entities.add({
name: "rectangle",
rectangle: {
coordinates: Cesium.Rectangle.fromDegrees(
box.west,
box.south,
box.east,
box.north
),
material: new Cesium.ImageMaterialProperty({
image: canvas,
transparent: true,
}),
// heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
},
});
this._canvas = canvas;
this._result = { box, data, canvasData, entity };
return this._result;
}
完整demo
热力图主要实现类HeatmapIntensity.js
/*
* Cesium 热力图主类
* @Author: Wang jianLei
* @Date: 2022-10-20 15:12:27
* @Last Modified by: Wang JianLei
* @Last Modified time: 2022-10-23 22:53:01
*/
import { Intensity, Canvas } from "./Intensity";
const Cesium = window.Cesium;
class HeatmapIntensity {
constructor(viewer, option = {}) {
if (!viewer) throw new Error("no viewer object!");
this._viewer = viewer;
this._min = option.min || 0; //最小值
this._max = option.max || 100; //最大值
this._size = option.size || 20; //光圈大小,像素值
this._result = undefined; //热力图结果
this._canvas = undefined; //离屏canvas
}
get min() {
return this._min;
}
set min(val) {
this._min = val;
this.updateHeatmap(this._result.canvasData);
}
get max() {
return this._max;
}
set max(val) {
this._max = val;
this.updateHeatmap(this._result.canvasData);
}
get size() {
return this._size;
}
set size(val) {
this._size = val;
this.updateHeatmap(this._result.canvasData);
}
get result() {
return this._result;
}
/**
* 创建热力图对象
* @param {*} box 范围对象,经纬度值-{west,south,east,north}
* @param {*} data 待绘制热力图的数据-[{x1,y1,value1},{x2,y2,value2},...]
* @returns 热力图结果对象
*/
createHeatmap(box, data) {
const mercator_WestSouth = this.WGS84ToWebMercator(box.west, box.south, 0); //左下位置(墨卡托)
const mercator_EastNorth = this.WGS84ToWebMercator(box.east, box.north, 0); //右上位置(墨卡托)
const diffDegrees_X = mercator_EastNorth.x - mercator_WestSouth.x;
const diffDegrees_Y = mercator_EastNorth.y - mercator_WestSouth.y;
const diffMax = Math.max(diffDegrees_X, diffDegrees_Y);
let multiple = diffMax / 300; //适当扩大倍数,以便绘制canvas
const width = Math.ceil(diffDegrees_X / multiple);
const height = Math.ceil(diffDegrees_Y / multiple);
this.mercator_WestSouth = mercator_WestSouth;
this.mercator_EastNorth = mercator_EastNorth;
this.diffDegrees_X = diffDegrees_X;
this.diffDegrees_Y = diffDegrees_Y;
let canvasData = [];
data.forEach((element) => {
const curMercator = this.WGS84ToWebMercator(
Number(element.x),
Number(element.y),
0
);
const per_X = (curMercator.x - mercator_WestSouth.x) / diffDegrees_X;
const currentPix_X = Math.ceil(per_X * width);
const per_Y = (curMercator.y - mercator_WestSouth.y) / diffDegrees_Y;
const currentPix_Y = Math.ceil(per_Y * height);
const currentValue = Number(element.value);
canvasData.push({
x: currentPix_X,
y: height - currentPix_Y - 1,
value: currentValue,
});
});
let canvas = new Canvas(width, height);
let context = canvas.getContext("2d");
context.clearRect(0, 0, canvas.width, canvas.height);
let circle = this.createCircle(this._size);
let circleHalfWidth = circle.width / 2;
let circleHalfHeight = circle.height / 2;
// 按透明度分类
let dataOrderByAlpha = {};
canvasData.forEach((item) => {
let alpha =
item.value < this._min
? 0
: Math.min(1, item.value / this._max).toFixed(2);
dataOrderByAlpha[alpha] = dataOrderByAlpha[alpha] || [];
dataOrderByAlpha[alpha].push(item);
});
// 绘制不同透明度的圆形
for (let i in dataOrderByAlpha) {
if (isNaN(i)) continue;
let _data = dataOrderByAlpha[i];
context.beginPath();
context.globalAlpha = i;
_data.forEach((item) => {
context.drawImage(
circle,
item.x - circleHalfWidth,
item.y - circleHalfHeight
);
});
}
// 圆形着色
let intensity = new Intensity();
let colored = context.getImageData(
0,
0,
context.canvas.width,
context.canvas.height
);
const options = { min: 0, max: this._max, size: this._size };
this.colorize(options, colored.data, intensity.getImageData());
context.clearRect(0, 0, context.canvas.width, context.canvas.height);
context.putImageData(colored, 0, 0);
let entity = this._viewer.entities.add({
name: "rectangle",
rectangle: {
coordinates: Cesium.Rectangle.fromDegrees(
box.west,
box.south,
box.east,
box.north
),
material: new Cesium.ImageMaterialProperty({
image: canvas,
transparent: true,
}),
// heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
},
});
this._canvas = canvas;
this._result = { box, data, canvasData, entity };
return this._result;
}
/**
* 更新热力图数据
* @param {*} data 待绘制热力图的数据-[{x1,y1,value1},{x2,y2,value2},...]
*/
setData(data) {
if (!this._result) {
return;
}
let canvasData = [];
const height = this._canvas.height;
const width = this._canvas.width;
data.forEach((element) => {
const curMercator = this.WGS84ToWebMercator(
Number(element.x),
Number(element.y),
0
);
const per_X =
(curMercator.x - this.mercator_WestSouth.x) / this.diffDegrees_X;
const currentPix_X = Math.ceil(per_X * width);
const per_Y =
(curMercator.y - this.mercator_WestSouth.y) / this.diffDegrees_Y;
const currentPix_Y = Math.ceil(per_Y * height);
const currentValue = Number(element.value);
canvasData.push({
x: currentPix_X,
y: height - currentPix_Y - 1,
value: currentValue,
});
});
this._result.data = data;
this._result.canvasData = canvasData;
this.updateHeatmap(canvasData);
}
/**
* 更新热力图
* @param {*} data 参考canvasData
* @returns 热力图结果对象
*/
updateHeatmap(data) {
let canvas = this._canvas;
let context = canvas.getContext("2d");
context.clearRect(0, 0, canvas.width, canvas.height);
let circle = this.createCircle(this._size);
let circleHalfWidth = circle.width / 2;
let circleHalfHeight = circle.height / 2;
// 按透明度分类
let dataOrderByAlpha = {};
data.forEach((item) => {
let alpha =
item.value < this._min
? 0
: Math.min(1, item.value / this._max).toFixed(2);
dataOrderByAlpha[alpha] = dataOrderByAlpha[alpha] || [];
dataOrderByAlpha[alpha].push(item);
});
// 绘制不同透明度的圆形
for (let i in dataOrderByAlpha) {
if (isNaN(i)) continue;
let _data = dataOrderByAlpha[i];
context.beginPath();
context.globalAlpha = i;
_data.forEach((item) => {
context.drawImage(
circle,
item.x - circleHalfWidth,
item.y - circleHalfHeight
);
});
}
// 圆形着色
let intensity = new Intensity();
let colored = context.getImageData(
0,
0,
context.canvas.width,
context.canvas.height
);
const options = { min: 0, max: this._max, size: this._size };
this.colorize(options, colored.data, intensity.getImageData());
context.clearRect(0, 0, context.canvas.width, context.canvas.height);
context.putImageData(colored, 0, 0);
this._result.entity.rectangle.material = new Cesium.ImageMaterialProperty({
image: canvas,
transparent: true,
});
return this._result;
}
createCircle(size) {
let shadowBlur = size / 2;
let r2 = size + shadowBlur;
let offsetDistance = 10000;
let circle = new Canvas(r2 * 2, r2 * 2);
let context = circle.getContext("2d");
context.shadowBlur = shadowBlur;
context.shadowColor = "black";
context.shadowOffsetX = context.shadowOffsetY = offsetDistance;
context.beginPath();
context.arc(
r2 - offsetDistance,
r2 - offsetDistance,
size,
0,
Math.PI * 2,
true
);
context.closePath();
context.fill();
return circle;
}
colorize(options, pixels, gradient) {
let max = options.max;
let min = options.min;
let diff = max - min;
let range = options.range || null;
let jMin = 0;
let jMax = 1024;
if (range && range.length === 2) {
jMin = ((range[0] - min) / diff) * 1024;
}
if (range && range.length === 2) {
jMax = ((range[1] - min) / diff) * 1024;
}
let maxOpacity = options.maxOpacity || 0.8;
for (let i = 3, len = pixels.length, j; i < len; i += 4) {
j = pixels[i] * 4; // get gradient color from opacity value
if (pixels[i] / 256 > maxOpacity) {
pixels[i] = 256 * maxOpacity;
}
if (j && j >= jMin && j <= jMax) {
pixels[i - 3] = gradient[j];
pixels[i - 2] = gradient[j + 1];
pixels[i - 1] = gradient[j + 2];
} else {
pixels[i] = 0;
}
}
}
WGS84ToWebMercator(lng, lat, height) {
let mercator = {};
let x = (lng * 20037508.34) / 180;
let y = Math.log(Math.tan(((90 + lat) * Math.PI) / 360)) / (Math.PI / 180);
y = (y * 20037508.34) / 180;
mercator.x = x;
mercator.y = y;
mercator.z = height;
return mercator;
}
clearAll() {
this._result && this._viewer.entities.remove(this._result.entity);
this._result = undefined;
}
}
export default HeatmapIntensity;
透明度转换方法文件Intensity.js
function Intensity(options) {
options = options || {};
this.gradient = options.gradient || {
0.25: "rgba(0, 0, 255, 1)",
0.55: "rgba(0, 255, 0, 1)",
0.85: "rgba(255, 255, 0, 1)",
1.0: "rgba(255, 0, 0, 1)",
};
this.maxSize = options.maxSize || 35;
this.minSize = options.minSize || 0;
this.max = options.max || 100;
this.min = options.min || 0;
this.initPalette();
}
Intensity.prototype.setMax = function (value) {
this.max = value || 100;
};
Intensity.prototype.setMin = function (value) {
this.min = value || 0;
};
Intensity.prototype.setMaxSize = function (maxSize) {
this.maxSize = maxSize || 35;
};
Intensity.prototype.setMinSize = function (minSize) {
this.minSize = minSize || 0;
};
Intensity.prototype.initPalette = function () {
let gradient = this.gradient;
let canvas = new Canvas(256, 1);
let paletteCtx = (this.paletteCtx = canvas.getContext("2d"));
let lineGradient = paletteCtx.createLinearGradient(0, 0, 256, 1);
for (let key in gradient) {
lineGradient.addColorStop(parseFloat(key), gradient[key]);
}
paletteCtx.fillStyle = lineGradient;
paletteCtx.fillRect(0, 0, 256, 1);
};
Intensity.prototype.getColor = function (value) {
let imageData = this.getImageData(value);
return (
"rgba(" +
imageData[0] +
", " +
imageData[1] +
", " +
imageData[2] +
", " +
imageData[3] / 256 +
")"
);
};
Intensity.prototype.getImageData = function (value) {
let imageData = this.paletteCtx.getImageData(0, 0, 256, 1).data;
if (value === undefined) {
return imageData;
}
let max = this.max;
let min = this.min;
if (value > max) {
value = max;
}
if (value < min) {
value = min;
}
let index = Math.floor(((value - min) / (max - min)) * (256 - 1)) * 4;
return [
imageData[index],
imageData[index + 1],
imageData[index + 2],
imageData[index + 3],
];
};
/**
* @param Number value
* @param Number max of value
* @param Number max of size
* @param Object other options
*/
Intensity.prototype.getSize = function (value) {
let size = 0;
let max = this.max;
let min = this.min;
let maxSize = this.maxSize;
let minSize = this.minSize;
if (value > max) {
value = max;
}
if (value < min) {
value = min;
}
size = minSize + ((value - min) / (max - min)) * (maxSize - minSize);
return size;
};
Intensity.prototype.getLegend = function (options) {
let gradient = this.gradient;
let width = options.width || 20;
let height = options.height || 180;
let canvas = new Canvas(width, height);
let paletteCtx = canvas.getContext("2d");
let lineGradient = paletteCtx.createLinearGradient(0, height, 0, 0);
for (let key in gradient) {
lineGradient.addColorStop(parseFloat(key), gradient[key]);
}
paletteCtx.fillStyle = lineGradient;
paletteCtx.fillRect(0, 0, width, height);
return canvas;
};
// 构造一个离屏canvas
function Canvas(width, height) {
let canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
return canvas;
}
export { Intensity, Canvas };
前端界面调用HeatmapCustom.vue
<template>
<div id="cesiumContainer">
<div class="canvas-main">
<el-button size="mini" @click="startDraw" :disabled="bool"
>开始绘制</el-button
>
<el-button size="mini" @click="autoUpdate" :disabled="!bool"
>自动更新</el-button
>
</div>
</div>
</template>
<script>
import HeatmapIntensity from "./HeatmapIntensity";
const Cesium = window.Cesium;
let viewer = undefined;
export default {
data() {
return {
mapData: [],
bool: false,
};
},
mounted() {
let key =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIwZDhhOThhNy0zMzUzLTRiZDktYWM3Ni00NGI5MGY2N2UwZDUiLCJpZCI6MjQzMjYsInNjb3BlcyI6WyJhc3IiLCJnYyJdLCJpYXQiOjE1ODUwMzUwNDh9.DYuDF_RPKe5_8w849_y-sutM68LM51O9o3bTt_3rF1w";
Cesium.Ion.defaultAccessToken = key;
window.viewer = viewer = new Cesium.Viewer("cesiumContainer", {
imageryProvider: new Cesium.ArcGisMapServerImageryProvider({
url: "https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer",
}),
terrainProvider: Cesium.createWorldTerrain(),
geocoder: true,
homeButton: true,
sceneModePicker: true,
baseLayerPicker: true,
navigationHelpButton: true,
animation: true,
timeline: true,
fullscreenButton: true,
vrButton: true,
//关闭点选出现的提示框
selectionIndicator: true,
infoBox: true,
});
viewer._cesiumWidget._creditContainer.style.display = "none"; // 隐藏版权
this.initData();
},
methods: {
startDraw() {
const option = {
min: 0,
max: 100,
size: 20,
};
window.heatmapObj = new HeatmapIntensity(viewer, option);
const box = {
west: 110,
south: 40.5,
east: 110.5,
north: 41,
};
window.heatmapObj.createHeatmap(box, this.mapData);
this.bool = true;
},
autoUpdate() {
setInterval(() => {
let data = [];
for (let i = 0; i < 100; i++) {
let obj = {};
obj.x = this.randomNum(110, 110.5, 5);
obj.y = this.randomNum(40.5, 41, 5);
obj.value = this.randomNum(0, 100, 2);
data.push(obj);
}
window.heatmapObj.setData(data);
}, 1000);
},
initData() {
this.mapData = [];
for (let i = 0; i < 100; i++) {
let obj = {};
obj.x = this.randomNum(110, 110.5, 5);
obj.y = this.randomNum(40.5, 41, 5);
obj.value = this.randomNum(0, 100, 2);
this.mapData.push(obj);
}
this.mapData.forEach((element) => {
viewer.entities.add({
position: Cesium.Cartesian3.fromDegrees(
Number(element.x),
Number(element.y)
),
point: {
pixelSize: 5,
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
},
});
});
viewer.flyTo(viewer.entities);
},
randomNum(maxNum, minNum, decimalNum) {
// 获取指定范围内的随机数, decimalNum指小数保留多少位
let max = 0,
min = 0;
minNum <= maxNum
? ((min = minNum), (max = maxNum))
: ((min = maxNum), (max = minNum));
let result = undefined;
switch (arguments.length) {
case 1:
result = Math.floor(Math.random() * (max + 1));
break;
case 2:
result = Math.floor(Math.random() * (max - min + 1) + min);
break;
case 3:
result = (Math.random() * (max - min) + min).toFixed(decimalNum);
break;
default:
result = Math.random();
break;
}
return result;
},
},
};
</script>
<style lang="scss" scoped>
#cesiumContainer {
width: 100%;
height: 100%;
position: relative;
background: rgba(255, 0, 0, 0.322);
.canvas-main {
position: absolute;
margin: 10px;
padding: 10px;
z-index: 10;
background: rgba(255, 255, 0, 0.329);
}
}
</style>
运行效果
综上!
如果客官您有问题,可以在本文下留言!
如果客官您有什么建议意见,可以在本文下留言!
如果客官您有批评指正,可以在本文下沟通讨论!
如果实例demo有数据缺失,评论留下您的邮箱地址!
如果客官您有其他的功能需求,可以在本文下留言,不管能不能实现,总会给出回复!