撒点功能是gis项目中,最常用的功能。如果点位比较密集,就会出现点位图标重叠问题,如果点位过多,会影响绘制的性能,就会导致页面出现卡顿等问题。点聚合是一种比较好的解决方式。本篇对Entity图元的点聚合进行封装,在数据量不是太大的情况下,可以取得比较好的显示效果,如果数据量比较大,使用Entity图元的点聚合仍然会出现性能问题,可以通过对primitive点图元进行封装,支持更大体量的点聚合,后续primitive图元篇重再介绍。
1 EntityCluster 类
前面的篇章介绍过CustomDataSource类,它有一个属性clustering 可以完成改数据源的点聚合的功能。clustering是一个EntityCluster类对象,定义屏幕空间对象(广告牌、点、标签)如何聚集。其参数如下:
通过对EntityCluster类的聚合事件clusterEvent的监听,可以动态的设置聚合后的点样式。clusterEvent回调函数返回一个点位集合和聚合后的entity图元。使用示例如下:
dataSource.clustering.clusterEvent.addEventListener(function(entities, cluster) {
cluster.label.show = false;
cluster.billboard.show = true;
cluster.billboard.image = this.getCluserCanvasImage(entities.length);
});
2 点聚合样式
根据聚合点位集合的大小,可以设置聚合点图标的大小和颜色,这里通过canvas动态绘制聚合点图标的方式,给出两种点聚合样式。如果对聚合样式有更高的要求,可以通过外部传入样式图片的方式,这里不再详细说明。
2.1 点聚合样式1
getCluserCanvasImage(num) {
if (this.cluserCanvasImages[num]) {
return this.cluserCanvasImages[num];
}
//创建cavas
let canvas = document.createElement("canvas");
//canvas大小
let size = 12 * (num + "").length + 50;
canvas.width = canvas.height = size;
//获取canvas 上下文
let ctx = canvas.getContext("2d");
//绘制
ctx.beginPath();
ctx.globalAlpha = 0.5;
(ctx.fillStyle = this.getCluseColor(num)),
ctx.arc(size / 2, size / 2, size / 2 - 5, 0, 2 * Math.PI),
ctx.fill(),
ctx.beginPath(),
(ctx.globalAlpha = 0.8),
(ctx.fillStyle = this.getCluseColor(num)),
ctx.arc(size / 2, size / 2, size / 2 - 10, 0, 2 * Math.PI),
ctx.fill(),
(ctx.font = "20px alpht"),
(ctx.globalAlpha = 1),
(ctx.fillStyle = "rgb(255,255,255)");
let offset = size / 2 - (12 * num.toString().length) / 2;
ctx.fillText(num, offset, size / 2 + 7);
this.cluserCanvasImages[num] = canvas;
return canvas;
}
2.2 点聚合样式2
getCluserCanvasImage2(num) {
if (this.cluserCanvasImages[num]) {
return this.cluserCanvasImages[num];
}
//创建cavas
let canvas = document.createElement("canvas");
//canvas大小
let size = 12 * (num + "").length + 50;
canvas.width = canvas.height = size;
//获取canvas 上下文
let ctx = canvas.getContext("2d");
//绘制
ctx.translate(size / 2, size / 2);
ctx.fillStyle = this.getCluseColor(num);
const angle = Math.PI / 2;
const dltAngle = Math.PI / 6;
let startAngle = 0;
let endAngle = 0;
for (let i = 0; i < 3; i++) {
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.globalAlpha = 0.5;
startAngle = endAngle + dltAngle;
endAngle = startAngle + angle;
ctx.arc(0, 0, size / 2 - 5, startAngle, endAngle);
ctx.closePath();
ctx.fill();
}
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.globalAlpha = 1.0;
ctx.arc(0, 0, size / 4, 0, 2 * Math.PI), ctx.fill();
ctx.translate(-size / 2, -size / 2);
ctx.font = "20px alpht";
ctx.globalAlpha = 1;
ctx.fillStyle = "rgb(255,255,255)";
let offset = size / 2 - (12 * num.toString().length) / 2;
ctx.fillText(num, offset, size / 2 + 7);
this.cluserCanvasImages[num] = canvas;
return canvas;
}
3 ClusterLayer 类封装
ClusterLayer.js
/*
* @Description:
* @Author: maizi
* @Date: 2022-05-27 11:36:22
* @LastEditTime: 2024-07-23 16:48:10
* @LastEditors: maizi
*/
const merge = require('deepmerge')
import carIcon from "@/assets/img/car-icon.png";
import selectCarIcon from "@/assets/img/car-s-icon.png";
const defaultStyle = {
pixelRange: 40,
minimumClusterSize: 2,
image: carIcon,
selectImage: selectCarIcon,
scale: 0.7,
type: 0,
cluserColors: [
{
value: 40,
color: "rgb(255,0,0)"
},
{
value: 30,
color: "rgb(255,0,255)"
},
{
value: 20,
color: "rgb(255,255,0)"
},
{
value: 10,
color: "rgb(0,255,255)"
},
{
value: 1,
color: "rgb(0,255,0)"
}
]
};
class ClusterLayer {
constructor(options = {}) {
this.options = options;
this.style = merge(defaultStyle, this.options.style || {});
this.layer = null;
this.cluserCanvasImages = {};
this.init();
}
init() {
this.layer = new Cesium.CustomDataSource("clusterLayer");
this.layer.clustering.enabled = true;
this.layer.clustering.pixelRange = this.style.pixelRange;
this.layer.clustering.minimumClusterSize = this.style.minimumClusterSize;
this.setCanvasClusterEvent();
}
addDataToLayer(data) {
data.forEach(item => {
const entity = new Cesium.Entity({
id: item.id ? item.id : Math.random().toString(36).substring(2),
props: item,
position: Cesium.Cartesian3.fromDegrees(item.coords[0], item.coords[1]),
billboard: {
image: this.style.image,
scale: this.style.scale,
disableDepthTestDistance: Number.POSITIVE_INFINITY
}
});
entity.layer = this.layer;
this.layer.entities.add(entity);
});
}
setCanvasClusterEvent() {
this.layer.clustering.clusterEvent.addEventListener((clusteredEntities, feature) => {
feature.label.show = false;
feature.layer = this.layer;
feature.billboard.show = true;
feature.billboard.id = feature.label.id;
feature.billboard.verticalOrigin = Cesium.VerticalOrigin.BOTTOM;
feature.billboard.disableDepthTestDistance = Number.POSITIVE_INFINITY;
let image = null;
switch (this.style.type) {
case 0:
image = this.getCluserCanvasImage(clusteredEntities.length);
break;
case 1:
image = this.getCluserCanvasImage2(clusteredEntities.length);
break;
}
feature.billboard.image = image;
});
}
getCluserCanvasImage(num) {
if (this.cluserCanvasImages[num]) {
return this.cluserCanvasImages[num];
}
//创建cavas
let canvas = document.createElement("canvas");
//canvas大小
let size = 12 * (num + "").length + 50;
canvas.width = canvas.height = size;
//获取canvas 上下文
let ctx = canvas.getContext("2d");
//绘制
ctx.beginPath();
ctx.globalAlpha = 0.5;
(ctx.fillStyle = this.getCluseColor(num)),
ctx.arc(size / 2, size / 2, size / 2 - 5, 0, 2 * Math.PI),
ctx.fill(),
ctx.beginPath(),
(ctx.globalAlpha = 0.8),
(ctx.fillStyle = this.getCluseColor(num)),
ctx.arc(size / 2, size / 2, size / 2 - 10, 0, 2 * Math.PI),
ctx.fill(),
(ctx.font = "20px alpht"),
(ctx.globalAlpha = 1),
(ctx.fillStyle = "rgb(255,255,255)");
let offset = size / 2 - (12 * num.toString().length) / 2;
ctx.fillText(num, offset, size / 2 + 7);
this.cluserCanvasImages[num] = canvas;
return canvas;
}
getCluserCanvasImage2(num) {
if (this.cluserCanvasImages[num]) {
return this.cluserCanvasImages[num];
}
//创建cavas
let canvas = document.createElement("canvas");
//canvas大小
let size = 12 * (num + "").length + 50;
canvas.width = canvas.height = size;
//获取canvas 上下文
let ctx = canvas.getContext("2d");
//绘制
ctx.translate(size / 2, size / 2);
ctx.fillStyle = this.getCluseColor(num);
const angle = Math.PI / 2;
const dltAngle = Math.PI / 6;
let startAngle = 0;
let endAngle = 0;
for (let i = 0; i < 3; i++) {
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.globalAlpha = 0.5;
startAngle = endAngle + dltAngle;
endAngle = startAngle + angle;
ctx.arc(0, 0, size / 2 - 5, startAngle, endAngle);
ctx.closePath();
ctx.fill();
}
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.globalAlpha = 1.0;
ctx.arc(0, 0, size / 4, 0, 2 * Math.PI), ctx.fill();
ctx.translate(-size / 2, -size / 2);
ctx.font = "20px alpht";
ctx.globalAlpha = 1;
ctx.fillStyle = "rgb(255,255,255)";
let offset = size / 2 - (12 * num.toString().length) / 2;
ctx.fillText(num, offset, size / 2 + 7);
this.cluserCanvasImages[num] = canvas;
return canvas;
}
getCluseColor(num) {
for (let i = 0; i < this.style.cluserColors.length; i++) {
let item = this.style.cluserColors[i];
if (num >= item.value) {
return item.color;
}
}
}
updateStyle(style) {
this.style = merge(this.style, style);
this.layer.clustering.pixelRange = this.style.pixelRange;
this.layer.clustering.minimumClusterSize = this.style.minimumClusterSize;
}
setType(type) {
this.style.type = type;
}
setSelectById(id, enabled) {
if (this.layer) {
const entity = this.layer.entities.getById(id);
entity && (entity.billboard.image = enabled ? this.style.selectImage : this.style.image);
}
}
getEntityById(id) {
const entity = this.layer.entities.getById(id);
return entity;
}
clear() {
this.cluserCanvasImages = {};
if (this.layer) {
this.layer.entities.removeAll();
}
}
}
export { ClusterLayer };
4 完整示例代码
MapWorks.js
import GUI from 'lil-gui';
// 初始视图定位在中国
import { ClusterLayer } from './ClusterLayer'
Cesium.Camera.DEFAULT_VIEW_RECTANGLE = Cesium.Rectangle.fromDegrees(90, -20, 110, 90);
//天地图key
const key = '39673271636382067f0b0937ab9a9677'
const gui = new GUI();
const params = {
pixelRange: 40,
minimumClusterSize: 2
}
let viewer = null;
let clusterLayer = null
let eventHandler = null
let selectGraphic = null
function initMap(container) {
viewer = new Cesium.Viewer(container, {
animation: false,
baseLayerPicker: false,
fullscreenButton: false,
geocoder: false,
homeButton: false,
infoBox: false,
sceneModePicker: false,
selectionIndicator: false,
timeline: false,
navigationHelpButton: false,
scene3DOnly: true,
orderIndependentTranslucency: false,
contextOptions: {
webgl: {
alpha: true
}
}
})
viewer._cesiumWidget._creditContainer.style.display = 'none'
viewer.scene.fxaa = true
viewer.scene.postProcessStages.fxaa.enabled = true
if (Cesium.FeatureDetection.supportsImageRenderingPixelated()) {
// 判断是否支持图像渲染像素化处理
viewer.resolutionScale = window.devicePixelRatio
}
// 移除默认影像
removeAll()
// 地形深度测试
viewer.scene.globe.depthTestAgainstTerrain = true
// 背景色
viewer.scene.globe.baseColor = new Cesium.Color(0.0, 0.0, 0.0, 0)
// 太阳光照
viewer.scene.globe.enableLighting = true;
// 初始化图层
initLayer()
// 初始化鼠标事件
initClickEvent()
//gui面板
initGui()
//调试
window.viewer = viewer
}
function initGui() {
let layerFolder = gui.title('样式设置')
layerFolder.add(params, 'pixelRange', 20, 200).step(1).onChange(function (value) {
clusterLayer.updateStyle(params)
})
layerFolder.add(params, 'minimumClusterSize', 2, 10).step(1).onChange(function (value) {
clusterLayer.updateStyle(params)
})
}
function addTdtLayer(options) {
let url = `https://t{s}.tianditu.gov.cn/DataServer?T=${options.type}&x={x}&y={y}&l={z}&tk=${key}`
const layerProvider = new Cesium.UrlTemplateImageryProvider({
url: url,
subdomains: ['0','1','2','3','4','5','6','7'],
tilingScheme: new Cesium.WebMercatorTilingScheme(),
maximumLevel: 18
});
viewer.imageryLayers.addImageryProvider(layerProvider);
}
function initLayer() {
addTdtLayer({
type: 'img_w'
})
addTdtLayer({
type: 'cia_w'
})
clusterLayer = new ClusterLayer()
viewer.dataSources.add(clusterLayer.layer);
}
function loadClusterData(data, options) {
clusterLayer.clear()
clusterLayer.setType(options.type)
clusterLayer.addDataToLayer(data)
viewer.flyTo(clusterLayer.layer);
}
function initClickEvent() {
eventHandler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
initLeftClickEvent()
initMouseMoveEvent()
}
function initLeftClickEvent() {
eventHandler.setInputAction((e) => {
const pickedObj = viewer.scene.pick(e.position);
if (selectGraphic) {
clusterLayer.setSelectById(selectGraphic.id, false);
selectGraphic = null;
}
if (pickedObj && pickedObj.id) {
if (pickedObj.id instanceof Cesium.Entity) {
//选中单个
if (pickedObj.id.layer && pickedObj.id.layer.name === "clusterLayer") {
selectGraphic = pickedObj.id;
clusterLayer.setSelectById(selectGraphic.id, true);
}
} else if (pickedObj.id instanceof Array) {
// 选中聚合
if (pickedObj.id[0].layer && pickedObj.id[0].layer.name === "clusterLayer") {
console.log("聚合点位信息=>", pickedObj.id);
}
}
}
},Cesium.ScreenSpaceEventType.LEFT_CLICK)
}
function initMouseMoveEvent() {
eventHandler.setInputAction((e) => {
const pickedObj = viewer.scene.pick(e.endPosition);
if (pickedObj && pickedObj.id) {
if (pickedObj.id instanceof Cesium.Entity || pickedObj.id instanceof Array) {
// 改变鼠标状态
viewer.enableCursorStyle = false;
viewer._element.style.cursor = "";
document.body.style.cursor = "pointer";
}
} else {
// 改变鼠标状态
viewer.enableCursorStyle = true;
viewer._element.style.cursor = "";
document.body.style.cursor = "default";
}
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
}
function removeAll() {
viewer.imageryLayers.removeAll();
}
function destroy() {
viewer.entities.removeAll();
viewer.imageryLayers.removeAll();
viewer.destroy();
}
export {
initMap,
loadClusterData,
destroy
}
ClusterLayer.vue
<!--
* @Description:
* @Author: maizi
* @Date: 2023-04-07 17:03:50
* @LastEditTime: 2023-04-14 09:47:03
* @LastEditors: maizi
-->
<template>
<div id="container">
<div class="pane_container">
<el-button size="small" @click="changeType(0)">聚合样式1</el-button>
<el-button size="small" @click="changeType(1)">聚合样式2</el-button>
</div>
</div>
</template>
<script>
import * as MapWorks from './js/MapWorks'
export default {
name: 'ClusterLayer',
mounted() {
this.init();
},
methods:{
init(){
let container = document.getElementById("container");
MapWorks.initMap(container)
},
changeType(type) {
const data = [
{
coords: [104.06791968990645, 30.639931966652405],
id: "ulqg4wpkado1",
},
{
coords: [104.08286762001791, 30.651738427212116],
id: "ulqg4wpkado2",
},
{
coords: [104.05749321027527, 30.65012469539701],
id: "ulqg4wpkado3",
},
{
coords: [104.08726215231565, 30.64326919178345],
id: "ulqg4wpkado4",
},
{
coords: [104.05035209594628, 30.63767262782807],
id: "ulqg4wpkado5",
},
{
coords: [104.06333112463464, 30.65974843756529],
id: "ulqg4wpkado6",
},
{
coords: [104.05830951574102, 30.67804353001073],
id: "ulqg4wpkado7",
},
{
coords: [104.05817218349625, 30.669883306472084],
id: "ulqg4wpkado8",
},
{
coords: [104.05049441213879, 30.66347415161995],
id: "ulqg4wpkado9",
},
{
coords: [104.04463502177197, 30.676841811429426],
id: "ulqg4wpkado10",
},
{
coords: [104.03804322070602, 30.66933394719844],
id: "ulqg4wpkado11",
},
{
coords: [104.09535525896746, 30.657248119145248],
id: "ulqg4wpkado12",
},
{
coords: [104.08702274136078, 30.671537092255143],
id: "ulqg4wpkado13",
},
{
coords: [104.07828569455882, 30.65974271388589],
id: "ulqg4wpkado14",
},
{
coords: [104.07607075284544, 30.677677293114513],
id: "ulqg4wpkado15",
},
{
coords: [104.07594606111917, 30.670615773715657],
id: "ulqg4wpkado16",
}
]
MapWorks.loadClusterData(data, {
type: type
})
}
},
beforeDestroy(){
//实例被销毁前调用,页面关闭、路由跳转、v-if和改变key值
MapWorks.destroy();
}
}
</script>
<style lang="scss" scoped>
#container{
width: 100%;
height: 100%;
background: rgba(7, 12, 19, 1);
overflow: hidden;
background-size: 40px 40px, 40px 40px;
background-image: linear-gradient(hsla(0, 0%, 100%, 0.05) 1px, transparent 0), linear-gradient(90deg, hsla(0, 0%, 100%, 0.05) 1px, transparent 0);
.pane_container{
position: absolute;
left: 10px;
top: 50px;
padding: 10px 15px;
border-radius: 4px;
border: 1px solid rgba(128, 128, 128, 0.5);
color: #ffffff;
background: rgba(0, 0, 0, 0.4);
box-shadow: 0 3px 14px rgb(128 128 128 / 50%);
z-index: 2;
}
}
</style>