一、简介
热力图是一种用颜色来表示数据密度或强度的图表。它通过将数据映射到不同的颜色,以展示数据的分布和变化情况。热力图通常用于显示大量数据的空间分布或趋势,帮助用户快速理解数据的模式和关系。
热力图的设计原理是基于人眼对颜色的敏感度,不同颜色的明暗程度可以传达不同的数据密度或强度。一般来说,较亮的颜色(如红色或黄色)表示高密度或高强度的数据,而较暗的颜色(如蓝色或绿色)表示低密度或低强度的数据。
热力图能反映哪些信息:
a、数据密度:热力图可以显示数据的分布密度,通过颜色的明暗程度来表示不同区域或点的数据密集程度。较亮的颜色表示高密度,较暗的颜色表示低密度。
b、数据强度:热力图可以显示数据的强度或权重。通过颜色的明暗程度来表示数据的强弱程度。较亮的颜色表示高强度,较暗的颜色表示低强度。
c、空间分布:热力图可以展示数据在空间上的分布情况。通过在地图或图表上显示颜色的分布,可以清晰地看到数据的空间分布模式和趋势。
二、热力图数据构成
热力图数据一般是具有经纬度信息和属性信息的一系列点集合。 经纬度用于展示空间分布趋势,属性信息用于展示强度。以高德地图 热力图开发示例 的数据为例,(北京部分“公园”数据)
数据格式:
let heatmapData = [{
"lng": 116.191031,
"lat": 39.988585,
"count": 10
}, {
"lng": 116.389275,
"lat": 39.925818,
"count": 11
}, {
"lng": 116.287444,
"lat": 39.810742,
"count": 12
}, {
"lng": 116.481707,
"lat": 39.940089,
"count": 13
},
...
]
三、如何绘制热力图
绘制热力图,我们一般使用热力图插件来进行绘制,这里使用heatmap.js这个插件,该插件被很多前端地图库采用,如Leaflet、GMAPS、Openlayers。该插件已经具备强大的热力图绘制功能,绘制热力图已不是难点,难点在于以下两点:
a、坐标转换
b、如何将绘制的结果正确地显示到Cesium上
1、坐标转换
heatmap.js使用的是屏幕坐标,即原点在屏幕左上角,往右为x轴正方向,往下为y轴正方向。我们拿到的热力图数据一般是包含经纬度信息和属性信息的点集合,这里选择高德地图api开发热力图示例中的数据进行测试 高德热力图
let heatmapData = [{
"lng": 116.191031,
"lat": 39.988585,
"count": 10
}, {
"lng": 116.389275,
"lat": 39.925818,
"count": 11
},
...
];
第一步,我们先获取热力图数据的范围大小
let xMin = 180, yMin = 90, xMax = 0, yMax = 0;
//获取热力图数据的矩形范围
heatmapData.forEach(data => {
xMin = Math.min(data.lng, xMin);
yMin = Math.min(data.lat, yMin);
xMax = Math.max(data.lng, xMax);
yMax = Math.max(data.lat, yMax);
})
有了范围,我们就可以定义绘制热力图画布的大小,以下代码获取了热力图数据矩形范围的长和宽
//热力图数据矩形范围的长和宽
let w = xMax - xMin;
let h = yMax - yMin;
第二步,定义画布大小,我们根据上面的宽度和高度定义用于绘制热力图画布的像素宽高
//定义用于绘制热力图画布的宽高
let canvasWidth = w * 500;
let canvasHeight = h * 500;
第三步、映射经纬度坐标到画布坐标有了热力图数据的经纬度范围和绘制热力图画布的宽高,我们就可以通过映射关系将热力图数据的经纬度坐标转为画布上的像素坐标了
//将经纬度数据坐标转换到画布的坐标系上
let convertData = [];
heatmapData.forEach(data => {
let x = (data.lng - xMin) / w * canvasWidth;
let y = -(data.lat - yMax) / h * canvasHeight;
convertData.push({
x: x,
y: y,
value: data.count
})
})
因为经纬度坐标和屏幕坐标的z轴方向是反的,所以在代码中我们将每个经纬度转到范围矩形的左上角,此时y值为负的,我们做个取反操作,这样就能正确表示为屏幕坐标了。
坐标数据转换成功后,我们通过heatmap.js将热力图数据绘制出来,完整代码如下:
//创建热力图对象
let heatmapInstance = h337.create({
container: document.querySelector('#map3dContainer'),
radius: 25, //给定半径
maxOpacity: 0.8,
minOpacity: 0,
});
let heatmapData = [{
"lng": 116.191031,
"lat": 39.988585,
"count": 10
},
...
];
let xMin = 180, yMin = 90, xMax = 0, yMax = 0;
//获取热力图数据的矩形范围
heatmapData.forEach(data => {
xMin = Math.min(data.lng, xMin);
yMin = Math.min(data.lat, yMin);
xMax = Math.max(data.lng, xMax);
yMax = Math.max(data.lat, yMax);
})
//热力图数据矩形范围的长和宽
let w = xMax - xMin;
let h = yMax - yMin;
//定义用于绘制热力图画布的宽高
let canvasWidth = w * 500;
let canvasHeight = h * 500;
//将经纬度数据坐标转换到画布的坐标系上
let convertData = [];
heatmapData.forEach(data => {
let x = (data.lng - xMin) / w * canvasWidth;
let y = -(data.lat - yMax) / h * canvasHeight;
convertData.push({
x: x,
y: y,
value: data.count
})
})
heatmapInstance.setData({
data: convertData,
max:100
});
对比高度网站上的热力图,结果一致
二、使用绘制结果
Cesium支持纹理贴图,我们可以将绘制的结果作为一张纹理贴到Cesium的Entity或Primtive上,我们创建一个Entity,Entity的几何体选择Rectangle,至于这里为什么要选择Rectangle,那是因为Rectangle刚好是需要一个矩形范围的坐标,Rectangle指定左下右上的坐标后它的纹理坐标刚好匹配左下右上的顺序,通过代码测试一下。
1、创建一个矩形对象
new Cesium.Rectangle(west, south, east, north)
2、创建Entity,选择几何体为rectangle,并将绘制结果作为材质设置给几何对象
let rect=new Cesium.Rectangle(Cesium.Math.toRadians(xMin), Cesium.Math.toRadians(yMin), Cesium.Math.toRadians(xMax), Cesium.Math.toRadians(yMax));
viewer.entities.add({
rectangle:{
coordinates:rect,
material:heatmapInstance._renderer.canvas
}
})
至此我们已经成功将热力图显示到cesium上,我们将对应的点位添加到场景中,看看结果是否正确。
完整代码:
let heatmapData = [{
"lng": 116.191031,
"lat": 39.988585,
"count": 10
},
...
];
let xMin = 180, yMin = 90, xMax = 0, yMax = 0;
//获取热力图数据的矩形范围
heatmapData.forEach(data => {
xMin = Math.min(data.lng, xMin);
yMin = Math.min(data.lat, yMin);
xMax = Math.max(data.lng, xMax);
yMax = Math.max(data.lat, yMax);
})
//热力图数据矩形范围的长和宽
let w = xMax - xMin;
let h = yMax - yMin;
//定义用于绘制热力图画布的宽高
let canvasWidth = w * 500;
let canvasHeight = h * 500;
//将经纬度数据坐标转换到画布的坐标系上
let convertData = [];
heatmapData.forEach(data => {
let x = (data.lng - xMin) / w * canvasWidth;
let y = -(data.lat - yMax) / h * canvasHeight;
convertData.push({
x: x,
y: y,
value: data.count
})
})
let container=document.createElement("div");
container.style.height=canvasHeight+"px";
container.style.width=canvasWidth+"px";
document.body.append(container);
let heatmapInstance = h337.create({
container: container,
radius: 25, //给定半径
maxOpacity: 0.8,
minOpacity: 0,
});
heatmapInstance.setData({
data: convertData,
max:100
});
let rect=new Cesium.Rectangle(Cesium.Math.toRadians(xMin), Cesium.Math.toRadians(yMin), Cesium.Math.toRadians(xMax), esium.Math.toRadians(yMax));
viewer.entities.add({
rectangle:{
coordinates:rect,
material:heatmapInstance._renderer.canvas
}
})
三、绘制3D立体曲面热力图
在上面学习中,我们已经成功绘制了热力图,并正确显示在Cesium中,这一节我们来看看如何实现3D立体曲面热力图,3D立体曲面热力图在视觉效果上比二维热力图更好。
3D立体曲面热力图的实现思路是在二维热力图的基础上,在顶点着色器中根据热力值修改顶点,要在顶点着色器中修改顶点,我们有以下两种方式可以实现:
a、自定义DrawCommand,需要完全自定义绘制命令
b、使用Primitive,在Primtive的顶点着色器中修改输出值
我们学习力Primitive的相关知识,本次进阶学习只展示相关的知识去实现,所以采用第二种方式。在Primitie相关章节,我们已经知道如何在着色器中获取模型坐标,如果您还没看,那么先去学习一下再回来。我们先通过Primitive创建一个几何图形,这里还是选择 Rectangle:
let rect = new Cesium.Rectangle(Cesium.Math.toRadians(xMin), Cesium.Math.toRadians(yMin), Cesium.Math.toRadians(xMax), Cesium.Math.toRadians(yMax));
const rectangle = new Cesium.RectangleGeometry({
rectangle: rect,
height: 10000.0,
});
const geometry = Cesium.RectangleGeometry.createGeometry(rectangle);
viewer.scene.primitives.add(new Cesium.Primitive({
geometryInstances: new Cesium.GeometryInstance({
geometry: geometry
}),
appearance: new Cesium.EllipsoidSurfaceAppearance({
material: Cesium.Material.fromType('Checkerboard')
})
}));
然后我们将热力图绘制结果像在Entity中那样作为材质赋给Primitive
viewer.scene.primitives.add(new Cesium.Primitive({
geometryInstances: new Cesium.GeometryInstance({
geometry: geometry
}),
appearance: new Cesium.MaterialAppearance({
material: new Cesium.Material({
fabric: {
type: 'Image',
uniforms: {
image:heatmapInstance._renderer.canvas赋值
}
}
})
})
}));
最后,我们更改下顶点着色器
vertexShaderSource: `
in vec3 position3DHigh;
in vec3 position3DLow;
in vec2 st;
in float batchId;
uniform sampler2D image_0;
out vec3 v_positionEC;
in vec3 normal;
out vec3 v_normalEC;
out vec2 v_st;
void main(){
vec4 p = czm_computePosition();
vec3 v_positionMC = position3DHigh + position3DLow;
v_normalEC = czm_normal * normal;
v_positionEC = (czm_modelViewRelativeToEye * p).xyz;
v_st = st;
vec4 color = texture(image_0, v_st);
vec3 upDir = normalize(v_positionMC.xyz);
p += vec4(color.r *upDir * 1000., 0.0);
gl_Position = czm_modelViewProjectionRelativeToEye * p;
}`
在顶点着色器中,我们通过纹理坐标将当前像素的纹理值获取出来,因为热力图热力值越大说明红色值越大,所以该处我们直接采用红色值为热力值,我们将要输出的坐标按上方向加上红色值,因为红色值数值比较小,所以我们乘上一个固定值,这样顶点就会在原来的基础上被抬高,运行代码发现结果和我们预想的并不一样。
出现上面的情况,是因为为了保证渲染性能,Cesium并没有对Rectangle进行足够的细分,可能此时的Rectanlge就几何顶点,所以达不到预期效果。我们可以通过构造Reatngle时传入细分的参数。
const rectangle = new Cesium.RectangleGeometry({
rectangle: rect,
height: 1000.0,
granularity: 0.00005,
});
granularity弧度值,表示两个经度之间的间隔,Cesium会根据该值将Rectangle内部划分为很多小的网格,这样就有足够多的顶点数据了,该值越大显示效果越好,不过预示着性能更低。最后的效果如下:
完整代码:
let heatmapData = [{
"lng": 116.191031,
"lat": 39.988585,
"count": 10
}...];
let xMin = 180, yMin = 90, xMax = 0, yMax = 0;
//获取热力图数据的矩形范围
heatmapData.forEach(data => {
xMin = Math.min(data.lng, xMin);
yMin = Math.min(data.lat, yMin);
xMax = Math.max(data.lng, xMax);
yMax = Math.max(data.lat, yMax);
})
//热力图数据矩形范围的长和宽
let w = xMax - xMin;
let h = yMax - yMin;
//定义用于绘制热力图画布的宽高
let canvasWidth = w * 500;
let canvasHeight = h * 500;
//将经纬度数据坐标转换到画布的坐标系上
let convertData = [];
heatmapData.forEach(data => {
let x = (data.lng - xMin) / w * canvasWidth;
let y = -(data.lat - yMax) / h * canvasHeight;
convertData.push({
x: x,
y: y,
value: data.count
})
})
let container = document.createElement("div");
container.style.height = canvasHeight + "px";
container.style.width = canvasWidth + "px";
document.body.append(container);
let heatmapInstance = h337.create({
container: container,
radius: 10, //给定半径
maxOpacity: 1,
minOpacity: 0,
});
heatmapInstance.setData({
data: convertData,
max: 100
});
let rect = new Cesium.Rectangle(Cesium.Math.toRadians(xMin), Cesium.Math.toRadians(yMin), Cesium.Math.toRadians(xMax),esium.Math.toRadians(yMax));
const rectangle = new Cesium.RectangleGeometry({
rectangle: rect,
height: 1000.0,
granularity: 0.00005,
});
const geometry = Cesium.RectangleGeometry.createGeometry(rectangle);
viewer.scene.primitives.add(new Cesium.Primitive({
geometryInstances: new Cesium.GeometryInstance({
geometry: geometry
}),
appearance: new Cesium.MaterialAppearance({
material: new Cesium.Material({
fabric: {
type: 'Image',
uniforms: {
image: heatmapInstance._renderer.canvas
}
}
}),
vertexShaderSource: `
in vec3 position3DHigh;
in vec3 position3DLow;
in vec2 st;
in float batchId;
uniform sampler2D image_0;
out vec3 v_positionEC;
in vec3 normal;
out vec3 v_normalEC;
out vec2 v_st;
void main(){
vec4 p = czm_computePosition();
vec3 v_positionMC = position3DHigh + position3DLow;
v_normalEC = czm_normal * normal;
v_positionEC = (czm_modelViewRelativeToEye * p).xyz;
v_st = st;
vec4 color = texture(image_0, v_st);
vec3 upDir = normalize(v_positionMC.xyz);
p += vec4(color.r *upDir * 1000., 0.0);
gl_Position = czm_modelViewProjectionRelativeToEye * p;
}`
})
}));
四、最后补充下Rectangle细分
图形渲染中只认识三角形,并没有什么矩形、多边形,所以一个图形会被扯分为多个三角形,而一个矩形,最简单的就是由两个三角形组成,每个三角形3个顶点,那么此时有6个顶点。
而Cesium中的矩形几何考虑了地球曲率的影响,所以一个矩形不一定是由两个三角形组成,可能会扯分为多个三角形。我们可以在创建完geometry对象后,从对象中获取顶点数据查看一个Rectangle是如何分为多个三角形的。
let vs = geometry.attributes.position.values;//顶点数组
let positions = [];
for (let i = 0; i < vs.length - 2; i+=3) {
let c = {
x: vs[i],
y: vs[i + 1],
z: vs[i + 2],
}
positions.push(c);
}
let indices = geometry.indices;//索引
for (let i = 0; i < indices.length - 2;i+=3) {
const element = indices[i];
viewer.entities.add({
polygon:{
hierarchy: new Cesium.PolygonHierarchy([
positions[indices[i]],
positions[indices[i+1]],
positions[indices[i+2]]
]),
material: Cesium.Color.fromRandom(),
}
})
}
当我们设置细分参数后,我们看看结果。至此热力图的知识功能基本上掌握
const rectangle = new Cesium.RectangleGeometry({
rectangle: rect,
height: 10000.0,
granularity: 0.0005,
});