前言:
项目之前的设计,billboard 广告牌是绑在 entityCollection 集合下的,为了能够在获取单个实体(entity)时能够获取更多数据信息(entity 能够注入除了它本身的属性之外的属性信息)
entityCollection 集合加上集群聚合功能,数据量临界点在 3w~4w 左右,就会出现界面卡顿。fps 低于 20 并且波动很大,延迟保持在 100ms 左右。数据量低于临界点时,entity 的方式呈现页面还是比较奈斯的
当数据量大于 10w+时,基本上 fps 处于 0-5,延迟大于 200ms,加载数据时延迟直接飙升几千都可能出现,同时(entityCollection 的)数据量过大直接导致浏览器崩溃无法加载
注:
尝试过很多优化的方法,抛开后台接口数据传递处理的优化,只针对前端 cesium 界面的所有优化方法中,记录我找到的效果最好,并且在衔接后续已经完成的其他功能方面开销最小的优化方式
该方法也适合界面显示大量的 pointPrimitiveCollection(点集合)、labelCollection(label 集合)造成的界面卡顿同理可得嘛
我使用的是显示 billboardCollection 广告牌集合
前往 primitives 原语集合官网文档
前往 官网相关例子
前往 实现 primitiveCluster 原语集群参考博客
在不需要聚合集群这一项功能的情况下,只使用 primitiveCollection 其实就能够完美的解决广告牌 10w+造成的界面卡顿崩溃等问题。建议是不需要聚合功能时,就不要添加 primitiveCluster 原语集群来处理优化。因为在聚合的方法,会监听摄像机的改变事件时刻改变聚合数量状态,反而会出现卡顿情况。应项目需求,添加聚合功能!针对聚合卡顿我做了加定时器的优化处理下面也会贴出来
primitiveCollection 使用例子
广告牌集合添加代码(如下),其他的集合如 point、label,官方有文档都大同小异
const billboardCollection = viewer.scene.primitives.add(
new Cesium.BillboardCollection()
);
billboardCollection.add({
position: Cesium.Cartesian3.fromDegrees(114.49, 41.23, 0),
width: 38,
height: 38,
image: "xxxxx"
});
此时往 billboardCollection 中添加的 billboard 就会直接呈现在界面上,并且能够轻松应对 10w+的数据量。对比之前的添加方式(如下)效果很明显
// 之前的添加方式
const entityCollection = new Cesium.EntityCollection();
const billboard = new Cesium.BillboardGraphics({
width: 38,
height: 38,
image: "xxxxx"
});
const entity = new Cesium.Entity({
position: Cesium.Cartesian3.fromDegrees(114.49, 41.23, 0),
billboard: billboard,
s1: "xxx",
s2: "xxx",
s3: "xxx",
s4: "xxx",
s5: "xxx"
});
entityCollection.add(entity);
如果你只需要优化数据量大导致界面卡顿崩溃等问题,不用实现聚合功能,到此就完全 OK 了
primitiveCollection 的聚合功能原生官方并没有提供,在官方文档中只提供了 EntityCluster 方法,来对 entityCollection 集合进行聚合操作。通过 EntityCluster 方法聚合时需要配合 datasource 对象使用,因为在原生的 datasource 对象自身有 clustering 属性(跳转 之前我写的 EntityCluster 聚合博客)。
由于我们直接使用的 Primitive 方式将 billboard 添加到地图中,就跳过了 datasource 的步骤。因此我们需要自己来定义一个 PrimitiveCluster 方法来创建一个 cluster 对象,针对原语集合进行聚合,结合其他博主文档提供的方法,PrimitiveCluster.js 具体实现方法总结如下
往 cesium 的包文件或者依赖文件中添加 PrimitiveCluster 方法
1.添加的路径:
1:npm 包中----- node_modules\cesium\Source\DataSources\PrimitiveCluster.js
2:引入外部文件方式 ---- Source\DataSources\PrimitiveCluster.js
2.复制同目录下 EntityCluster.js 内容到 PrimitiveCluster.js 中
3.文件内全局修改名称,EntityCluster -> PrimitiveCluster、 entityCluster -> primitiveCluster
4.屏蔽大概在 191 行左右 getScreenSpacePositions 方法中的代码块(EntityCluster 中 item.id 指向的就是 entity 实体对象,在 primitiveCollection 中 item.id 为 undefined 会包错)
/* var canClusterLabels =
primitiveCluster._clusterLabels && defined(item._labelCollection);
var canClusterBillboards =
primitiveCluster._clusterBillboards && defined(item.id._billboard);
var canClusterPoints =
primitiveCluster._clusterPoints && defined(item.id._point);
if (canClusterLabels && (canClusterPoints || canClusterBillboards)) {
continue;
} */
第 4 步时 如果你的业务需求在添加广告牌时需要为广告牌添加唯一的标识 id(如下添加方式),则可以不用屏蔽源代码,添加的 id 能够规避此处报错
billboardCollection.add({
id: "xxx",
position: Cesium.Cartesian3.fromDegrees(114.49, 41.23, 0),
width: 38,
height: 38,
image: "xxxxx"
});
5.在 PrimitiveCluster.js 的上级目录(node_modules\cesium\Source\Cesium.js)中找到入口文件 cesium.js,导入 PrimitiveCluster 方法
export { default as PrimitiveCluster } from "./DataSources/PrimitiveCluster.js";
至此 PrimitiveCluster 方法就添加完成,可以直接通过 new Cesium.PrimitiveCluster()的方式来调用
PrimitiveCluster 方法来实现聚合
1.往 scene.primitives 中添加一个用作‘根’的原语集合 primitives
2.创建一个空 billboardCollection 广告牌集合
3.通过 PrimitiveCluster 方法创建一个 cluster 实例对象 primitiveCluster
4.将 primitiveCluster 添加到原语集合 primitives 中
5.配置 primitiveCluster 对象的基本参数(可以不配置有提供默认参数)
6.(重要*)将空 billboardCollection 广告牌集合赋予 primitiveCluster._billboardCollection,手动添加聚合内容
提一下: label、point 集合添加方式一致
primitiveCluster._labelCollection;
primitiveCluster._pointCollection;
7.(重要*)调用_initialize 方法初始化 cluster 实例的事件监听
8.之后就与 datasource 聚合方式的.then 方法一致,只需要将 dataSource.clustering.clusterEvent.addEventListener 换成 primitiveCluster.clusterEvent.addEventListener
如下:
const primitives = viewer.scene.primitives.add(
new Cesium.PrimitiveCollection()
);
const billboardCollection = new Cesium.BillboardCollection();
const primitiveCluster = new Cesium.PrimitiveCluster();
primitives.add(primitiveCluster);
primitiveCluster.enabled = true; //开启聚合功能
primitiveCluster.pixelRange = 15; //范围
primitiveCluster.minimumClusterSize = 2; //最小聚合数量
primitiveCluster._billboardCollection = billboardCollection;
primitiveCluster._initialize(viewer.scene);
primitiveCluster.clusterEvent.addEventListener(function(
clusteredEntities,
cluster
) {
// ... 处理聚合显示广告牌代码块与dataSource处理方式一致
});
按照上面的方式完成聚合后,往 billboardCollection 集合中添加 billboard 广告牌就会在页面呈现出来并且聚合显示。但是数据量 10w+的情况下,在处理摄像机视角改变的监听事件时会出现卡顿问题。下面贴一下简单优化的方法
优化 PrimitiveCluster 卡顿问题
在 PrimitiveCluster.js 的_initialize 方法中,可以看到原方法使用 createDeclutterCallback 方法创建了一个回调方法,并将这个回调方法添加到了 scene.camera.changed 监听中。因此只要 scene.camera 视角改变,就会执行聚合的处理逻辑方法返回两个参数 clusteredEntities 与 cluster
primitiveCluster.clusterEvent.addEventListener(function(
clusteredEntities,
cluster
) {
// ... 处理聚合显示广告牌代码块与dataSource处理方式一致
});
所以只需要在_initialize 方法加一个防抖的定时器,让它事件处理频率降低就能达到优化的效果。同时暴露 delay 时间参数可以在实例化后进行配置改变
//1.PrimitiveCluster构造函数中添加_delay参数
this._delay = defaultValue(options.delay, 800)
//2.在PrimitiveCluster.prototype拦截器Object.defineProperties方法中添加_delay的访问以及设置方法
delay: {
get: function () {
return this._delay;
},
set: function (value) {
this._delay = value;
},
},
// 3._initialize方法改造
PrimitiveCluster.prototype._initialize = function(scene) {
this._scene = scene;
var cluster = createDeclutterCallback(this);
this._cluster = cluster;
var _t = null;
const _self = this;
this._removeEventListener = scene.camera.changed.addEventListener(function(amount) {
if (_t) {
clearTimeout(_t);
_t = null;
}
_t = setTimeout(() => {
cluster(amount);
}, _self._delay);
});
};
到此上文的内容就是关于优化 cesium 界面广告牌(billboard)数据量大于 10w +时,地图加载缓慢、卡顿、加载完成后浏览器严重卡顿等问题的方法思路,下面贴代码记录。
import * as Cesium from "cesium/Cesium";
import defaultValue from "./core/defaultValue";
/**
* @_v 引入外部创建的Viewer实例(new Cesium.Viewer(...))
* @myPrimitives 原语集合,可以包含页面显示的pointPrimitiveCollection、billboardCollection、labelCollection、primitiveCollection、primitiveCluster
* @myPrimitiveCluster 自定义原语集群
* @myBillboardCollection 广告牌集合(站点显示的内容数据)
*
* @desc 使用primitiveCollection原语集合与primitiveCluster原语集群,处理地图界面显示广告牌billboard数量 > 10w 级时,界面卡顿,浏览器崩溃等问题
*/
class CommomSiteTookit {
static _v = null;
myPrimitives = null;
myPrimitiveCluster = null;
myBillboardCollection = null;
constructor() {}
/**
* @desc 使用commomSiteTookit实例前,必须先初始化该实例的_v对象
*/
init(viewer) {
this._v = viewer;
}
/**
* @param [options] 具有以下属性的对象
* @param [options.delay=800] 防抖处理定时器的time
* @param [options.enabled=true] 是否启用集群
* @param [options.pixelRange=15] 用于扩展屏幕空间包围框的像素范围
* @param [options.minimumClusterSize=2] 可集群的屏幕空间对象的最小数量
*
* @desc 处理原语集合,并实现聚合集群功能方法
* @return billboardCollection集合,可直接往集合里添加广告牌billboard,呈现在页面上
*/
load(options = {}) {
let billboardCollection = new Cesium.BillboardCollection();
if (Cesium.defined(this.myPrimitives)) {
this._v.scene.primitives.remove(this.myPrimitives);
}
this.myPrimitives = this._v.scene.primitives.add(
new Cesium.PrimitiveCollection()
);
const primitiveCluster = new Cesium.PrimitiveCluster();
this.myPrimitives.add(primitiveCluster);
primitiveCluster.delay = defaultValue(options.delay, 800);
primitiveCluster.enabled = defaultValue(options.enabled, true);
primitiveCluster.pixelRange = defaultValue(options.pixelRange, 15);
primitiveCluster.minimumClusterSize = defaultValue(
options.minimumClusterSize,
2
);
primitiveCluster._billboardCollection = billboardCollection;
primitiveCluster._initialize(this._v.scene);
let removeListener;
let pinBuilder = new Cesium.PinBuilder();
/* 定义广告牌 fromText(显示文字,颜色,大小) */
let pin50 = pinBuilder.fromText("50+", Cesium.Color.RED, 40).toDataURL();
let pin40 = pinBuilder.fromText("40+", Cesium.Color.ORANGE, 40).toDataURL();
let pin30 = pinBuilder.fromText("30+", Cesium.Color.YELLOW, 40).toDataURL();
let pin20 = pinBuilder.fromText("20+", Cesium.Color.GREEN, 40).toDataURL();
let pin10 = pinBuilder.fromText("10+", Cesium.Color.BLUE, 40).toDataURL();
/* 数量小于十个的聚合广告牌 */
let singleDigitPins = new Array(8);
for (let i = 0; i < singleDigitPins.length; ++i) {
singleDigitPins[i] = pinBuilder
.fromText("" + (i + 2), Cesium.Color.VIOLET, 40)
.toDataURL();
}
const _ = this;
function customStyle() {
if (Cesium.defined(removeListener)) {
removeListener();
removeListener = undefined;
} else {
removeListener = primitiveCluster.clusterEvent.addEventListener(
function(clusteredEntities, cluster) {
cluster.label.show = false;
cluster.billboard.show = true;
cluster.billboard.id = cluster.label.id;
cluster.billboard.verticalOrigin = Cesium.VerticalOrigin.BOTTOM;
/* 根据站点(参数)的数量给予对应的广告牌 */
if (clusteredEntities.length >= 50) {
cluster.billboard.image = pin50;
} else if (clusteredEntities.length >= 40) {
cluster.billboard.image = pin40;
} else if (clusteredEntities.length >= 30) {
cluster.billboard.image = pin30;
} else if (clusteredEntities.length >= 20) {
cluster.billboard.image = pin20;
} else if (clusteredEntities.length >= 10) {
cluster.billboard.image = pin10;
} else {
cluster.billboard.image =
singleDigitPins[clusteredEntities.length - 2];
}
}
);
}
// force a re-cluster with the new styling
let pixelRange = primitiveCluster.pixelRange;
primitiveCluster.pixelRange = 0;
primitiveCluster.pixelRange = pixelRange;
_.myPrimitiveCluster = primitiveCluster;
}
this.myBillboardCollection = billboardCollection;
// start with custom style
customStyle();
return billboardCollection;
}
/**
* @params enable bool值控制开启或关闭集群
* @desc 控制集群生效与否
*/
enableCluster(enable) {
if (Cesium.defined(this.myPrimitiveCluster)) {
this.myPrimitiveCluster.enabled = enable;
}
}
/**
* @params id 站点ID
* @return 返回可操作的广告牌[siteBillboard.image = 'xxxx']
* @desc 根据id在集合中获取指定站点广告牌
*/
getSiteBillboardById(id) {
if (!Cesium.defined(this.myBillboardCollection)) return undefined;
const _b = this.myBillboardCollection;
const l = _b.length;
let siteBillboard = undefined;
for (let i = 0; i < l; i++) {
if (id == _b.get(i).id) {
siteBillboard = _b.get(i);
break;
}
}
return siteBillboard;
}
/**
* @desc 删除所有站点广告牌
*/
removeAll() {
if (Cesium.defined(this.myPrimitives)) {
this._v.scene.primitives.remove(this.myPrimitives);
}
}
/**
* @params show bool值 控制显示或隐藏
* @desc 隐藏或显示所有站点广告牌
*/
showStatus(show = true) {
this.myPrimitives.show = show;
}
/**
* @desc 根据id删除指定站点广告牌
*/
remove(id) {
const billboard = this.getSiteBillboardById(id);
billboard && this.myBillboardCollection.remove(billboard);
}
/**
* @desc 销毁(目前退出页面时直接viewer销毁)
*/
destroy() {
this.myPrimitives = null;
this.myPrimitiveCluster = null;
this.myBillboardCollection = null;
// this._v.scene.primitives.destroy()
}
}
export default new CommomSiteTookit();
在执行commomSiteTookit.init(viewer)后,加载数据主要的操作在 load 方法中,load 返回的 billboardCollection,可以动态的添加 billboard 数据,直接呈现在界面,代码如下。
const list = ['10w+数据']
const l = list.length
const data = commomSiteTookit.load({
enabled: true,
delay: 1200,
pixelRange: 20
});
for (let i = 0; i < l; i++) {
data.add({
image: `xxxx`,
scaleByDistance: new Cesium.NearFarScalar(1.5e2, 1, 1.5e7, 0.2),
width: 38, // default: undefined
height: 38, // default: undefined
position: Cesium.Cartesian3.fromDegrees(
list[i].longitude,
list[i].latitude,
0
),
id: list[i].id + ""
});
}