Cesium 100K数据加载 支持弹窗 动态更改位置
- 前言:今天总结关于point、label、billboard海量数据加载。后续会研究下大量model加载以及大bim(几百G上T)模型记载
海量点加载
- 弹窗
加载点位时,不加载弹窗。点击点位时在加载弹窗,及有效的减少加载量,优化性能。
const handler = new Cesium.ScreenSpaceEventHandler();
handler.setInputAction(function (movement: any) {const pickedLabel = viewer.scene.pick(movement.position);if (Cesium.defined(pickedLabel)) {console.log(pickedLabel)staticPoins.some(i => {if (i.id === pickedLabel.id.id || i.id === pickedLabel.id) {changePopWins(i)return true}})}}, Cesium.ScreenSpaceEventType.LEFT_CLICK);function changePopWins(item: Point) {let flag = popWins.value.some(i => {if (i.id === item.id) {i.visible = !i.visiblereturn true}})if (!flag) {popWins.value.push({ ...item, visible: true })}
}
- dom-tag组件
<template><div class="cesium-domPoint" ref="element"><slot :data="props.data"></slot></div>
</template>
<script lang="ts" setup>
import * as Cesium from "cesium";
import { onMounted, onUnmounted, ref, PropType } from "vue";
import type { EalignX, EalignY } from "../typing";
const props = defineProps({eventClick: Function,data: Object,trackPos: {type: Object as PropType<Cesium.Cartesian3>,},trackEntity: {type: Object as PropType<Cesium.Entity>,},alignX: {type: String as PropType<EalignX>,},alignY: {type: String as PropType<EalignY>,},trackCursor: Boolean,
});
const { Viewer } = window;
let element = ref<HTMLDivElement | null>(null),mousePos: Cesium.Cartesian2,trackCursor = props.trackCursor;
const handler = new Cesium.ScreenSpaceEventHandler();
const onUpdate = () => {if (element == null) return;let screenPos;if (trackCursor) {screenPos = mousePos;} else if (props.trackEntity) {let pos =(props.trackEntity.position &&props.trackEntity.position.getValue(Cesium.JulianDate.now())) ||Cesium.Cartesian3.ZERO;screenPos = Cesium.SceneTransforms.wgs84ToWindowCoordinates(Viewer.scene,pos);} else if (props.trackPos) {screenPos = Cesium.SceneTransforms.wgs84ToWindowCoordinates(Viewer.scene,props.trackPos);}if (screenPos) {if (element.value) {switch (props.alignX) {case "left":element.value.style.left = screenPos.x + "px";break;case "center":element.value.style.left =screenPos.x + element.value.clientWidth * 0.5 + "px";break;case "right":default:element.value.style.left =screenPos.x - element.value.clientWidth * 0.5 + "px";}switch (props.alignY) {case "top":element.value.style.top =screenPos.y - element.value.clientHeight + "px";break;case "bottom":element.value.style.top = screenPos.y + "px";break;case "center":default:element.value.style.top =screenPos.y - element.value.clientHeight * 0.5 + "px";}if (parseFloat(element.value.style.top) < (-element.value.clientWidth) || parseFloat(element.value.style.left) < (-element.value.clientHeight) || parseFloat(element.value.style.left) > (document.body.clientWidth + element.value.clientWidth) || parseFloat(element.value.style.top) > (document.body.clientHeight + element.value.clientHeight)) {element.value.style.display = "none";}else {element.value.style.display = "";}}} else {if (element.value) element.value.style.display = "none";}
};
onMounted(() => {if (trackCursor) {handler.setInputAction((event: any) => {let offsetToLeftTop = Viewer.container.getBoundingClientRect();mousePos = Cesium.Cartesian2.subtract(event.endPosition,new Cesium.Cartesian2(offsetToLeftTop.left, offsetToLeftTop.top),new Cesium.Cartesian2());}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);}Viewer.scene.preUpdate.addEventListener(onUpdate);
});
onUnmounted(() => {if (Viewer && !Viewer.isDestroyed()) {Viewer.scene.preUpdate.removeEventListener(onUpdate);}handler && handler.destroy()
});
</script>
<style lang="less" scoped>
.cesium-domPoint {position: absolute;z-index: 0;
}
</style>
<template v-for="item in popWins" :key="item.id"><DomTag :trackPos="item.pos" v-if="item.visible"><PopWin :point="item" @tooglePopWin="tooglePopWin1" /></DomTag></template>
- 最常用的就是entity
不加载图片以及文字的时候20W的点都没问题,虽然帧数就10左右,但是流畅度还行
for (let i = 0; i < 500; i++) {for (let j = 0; j < 400; j++) {viewer.entities.add({point: {pixelSize: 5,color: Cesium.Color.BEIGE},position: Cesium.Cartesian3.fromDegrees(103 + i * 0.1, 30 + j * 0.1)}) }}
添加文字后(20K),虽然也是10侦左右但是会感到明显的卡顿
for (let i = 0; i < 50; i++) {for (let j = 0; j < 400; j++) {viewer.entities.add({point: {pixelSize: 5,color: Cesium.Color.BEIGE},label: {text: i+""},position: Cesium.Cartesian3.fromDegrees(103 + i * 0.1, 30 + j * 0.1)}) }
}
- 添加图片
图片较小只有179个字节。20k的数据流畅度可以,图片越大流畅度越低
viewer.entities.add({point: {pixelSize: 5,color: Cesium.Color.BEIGE},billboard: {image: 'icons/facility.gif'},// label: {// text: i+""// },position: Cesium.Cartesian3.fromDegrees(103 + i * 0.1, 30 + j * 0.1)})
- 总结:entity加载,图片越大流畅度越低,点越密集流畅度越低。层级越高流畅度越高(密度下降)。
entity聚合
添加(10k)聚合功能,首次加载时间延长,但是加载成功后流畅度显著提高,并且entity可以同时添加文字和图片
/*
本质上还是利用的是entity加载,还是慢加载时间长
加载完成后性能提升
*/
let staticPoins: Point[] = []for (let i = 0; i < 500; i++) {for (let j = 0; j < 200; j++) {let obj = {visible: false,id: i + '_' + j,onlinetime: "2023-02-21 11:32:22",accountname: `${i}_${j}`,differentialstate: 7,curH: 474.59999999999997,curB: 30 + i * 0.1,curL: 104 + 0.1 * j,satellitenumber: 50,dopvalue: 0.6000000238418579,delay: 0,reserver1: "153403.172851563",reserver2: "NTRIP GNSSInternetRadio/1.4.5",pos: Cesium.Cartesian3.fromDegrees(104.06657490833334, 30.63132543),}obj.pos = Cesium.Cartesian3.fromDegrees(obj.curL, obj.curB)staticPoins.push(obj)}}staticPoins.forEach((i, j) => {if (j < 10000)dataSource.entities.add({id: i.id,point: {pixelSize: 0},label: {text: i.accountname,font: '20px sans-serif',showBackground: true,// verticalOrigin: Cesium.VerticalOrigin.TOP,pixelOffset: new Cesium.Cartesian2(0, -65),distanceDisplayCondition: new Cesium.DistanceDisplayCondition(0, 10e5),//根据Label与相机的距离来获取或设置Label的近和远像素偏移缩放比例属性// eyeOffset: new Cesium.Cartesian3(0, 7.2, 0)heightReference: Cesium.HeightReference.RELATIVE_TO_GROUND},billboard: {image: 'icons/facility.gif',scaleByDistance: new Cesium.NearFarScalar(1.5e2, 1, 1.5e7, 0),//根据相机距离缩放(下限、下限的值、上限、上限的值)width: 16,height: 16,heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,horizontalOrigin: Cesium.HorizontalOrigin.CENTER, // //相对于对象的原点(注意是原点的位置)的水平位置verticalOrigin: Cesium.VerticalOrigin.BOTTOM,},position: i.pos})})const dataSourcePromise = viewer.dataSources.add(dataSource);dataSourcePromise.then(function (dataSource) {const pixelRange = 15;const minimumClusterSize = 3;const enabled = true;dataSource.clustering.enabled = enabled; //是否聚合dataSource.clustering.pixelRange = pixelRange;dataSource.clustering.minimumClusterSize = minimumClusterSize;const pinBuilder = new Cesium.PinBuilder();const pin1000 = pinBuilder.fromText("1000+", Cesium.Color.RED, 48).toDataURL();const pin500 = pinBuilder.fromText("100+", Cesium.Color.RED, 48).toDataURL();const pin100 = pinBuilder.fromText("100+", Cesium.Color.RED, 48).toDataURL();const pin50 = pinBuilder.fromText("50+", Cesium.Color.RED, 48).toDataURL();const pin40 = pinBuilder.fromText("40+", Cesium.Color.ORANGE, 48).toDataURL();const pin30 = pinBuilder.fromText("30+", Cesium.Color.YELLOW, 48).toDataURL();const pin20 = pinBuilder.fromText("20+", Cesium.Color.GREEN, 48).toDataURL();const pin10 = pinBuilder.fromText("10+", Cesium.Color.BLUE, 48).toDataURL();const singleDigitPins = new Array(8);for (let i = 0; i < singleDigitPins.length; ++i) {singleDigitPins[i] = pinBuilder.fromText(`${i + 2}`, Cesium.Color.VIOLET, 48).toDataURL();}function customStyle() {if (Cesium.defined(removeListener)) {removeListener && removeListener();removeListener = undefined;} else {removeListener = dataSource.clustering.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 >= 1000) {cluster.billboard.image = pin1000;} else if (clusteredEntities.length >= 500) {cluster.billboard.image = pin500;} else if (clusteredEntities.length >= 100) {cluster.billboard.image = pin100;} else 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];}});}const pixelRange = dataSource.clustering.pixelRange;dataSource.clustering.pixelRange = 0;dataSource.clustering.pixelRange = pixelRange;}customStyle();})
dom加载
利用坐标点的变化时候改变dom元素的位置
优点:自由度高
缺点:适合几百以内的数据。
<template v-for="item in points" :key="item.id"><DomTag :trackPos="item.pos"><Dev :point="item" @tooglePopWin="tooglePopWin" /></DomTag>
</template>
- PointPrimitiveCollection
加载PointPrimitiveCollection点集合,速度快且流畅,也是推荐的方式。
缺点只能加载点
let pointPrimitives: Cesium.PointPrimitiveCollection;
pointPrimitives = scene.primitives.add(new Cesium.PointPrimitiveCollection()
);
for(let i = 0; i < 100000; i++) {
pointPrimitives.add({id: i.id,pixelSize: 10,color: color,outlineColor: outlineColor,outlineWidth: 0,label: {text: 222},distanceDisplayCondition: new Cesium.DistanceDisplayCondition(5.5e3),position: i.pos,
});
}
- 点位移动
这个方法,移动十万个点位,很流畅
viewer.scene.preUpdate.addEventListener(animateBillboards);
function animateBillboards() {const moveAmount = new Cesium.Cartesian3(100, 0.0, 0.0);const positionScratch = new Cesium.Cartesian3();// @ts-ignoreconst billboards = pointPrimitives._pointPrimitives;const length = billboards.length;for (let i = 0; i < length; ++i) {const billboard = billboards[i];Cesium.Cartesian3.clone(billboard.position, positionScratch);Cesium.Cartesian3.add(positionScratch, moveAmount, positionScratch);billboard.position = positionScratch;}
}
- billoardPrimitives
同上,只能加载图片。
let billoardPrimitives: Cesium.BillboardCollection
billoardPrimitives = viewer.scene.primitives.add(new Cesium.BillboardCollection({scene: scene,}));
for(let i = 0; i < 100000; i++) {
billoardPrimitives.add({id: i.id,image: 'icons/facility.gif',width: 10,height: 10,position: i.pos
});
}
- Primitive和聚合搭配着使用
cesium官方未能提供primitive的聚合方法,但是可以用entity的聚合搭配着使用。
- CommomSiteTookit
import * as Cesium from "cesium";
import { defaultValue } from 'cesium'type Option = {delay?:number;enabled?: boolean;pixelRange?:number;minimumClusterSize?:number;
}
/* @_v 引入外部创建的Viewer实例(new Cesium.Viewer(...))* @myPrimitives 原语集合,可以包含页面显示的pointPrimitiveCollection、billboardCollection、labelCollection、primitiveCollection、primitiveCluster* @myPrimitiveCluster 自定义原语集群* @myBillboardCollection 广告牌集合(站点显示的内容数据) @desc 使用primitiveCollection原语集合与primitiveCluster原语集群,处理地图界面显示广告牌billboard数量 > 10w 级时,界面卡顿,浏览器崩溃等问题*/
class CommomSiteTookit {static _v: Cesium.Viewer;myPrimitives: Cesium.PrimitiveCollection | null = nullmyPrimitiveCluster:any = null;myBillboardCollection:Cesium.BillboardCollection|null = null;constructor() {}/* @desc 使用commomSiteTookit实例前,必须先初始化该实例的_v对象*/init(viewer: Cesium.Viewer) {CommomSiteTookit._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:Option = {}) {let billboardCollection = new Cesium.BillboardCollection();let labelboardCollection = new Cesium.LabelCollection();if (Cesium.defined(this.myPrimitives)) {CommomSiteTookit._v.scene.primitives.remove(this.myPrimitives);}this.myPrimitives = CommomSiteTookit._v.scene.primitives.add(new Cesium.PrimitiveCollection());//@ts-ignoreconst primitiveCluster = new Cesium.PrimitiveCluster();this.myPrimitives && 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(CommomSiteTookit._v.scene);let removeListener:any;let pinBuilder = new Cesium.PinBuilder();/* 定义广告牌 fromText(显示文字,颜色,大小) */const pin1000 = pinBuilder.fromText("1000+", Cesium.Color.RED, 48).toDataURL();const pin500 = pinBuilder.fromText("500+", Cesium.Color.RED, 48).toDataURL();const pin100 = pinBuilder.fromText("100+", Cesium.Color.RED, 48).toDataURL();const pin50 = pinBuilder.fromText("50+", Cesium.Color.RED, 48).toDataURL();const pin40 = pinBuilder.fromText("40+", Cesium.Color.ORANGE, 48).toDataURL();const pin30 = pinBuilder.fromText("30+", Cesium.Color.YELLOW, 48).toDataURL();const pin20 = pinBuilder.fromText("20+", Cesium.Color.GREEN, 48).toDataURL();const pin10 = pinBuilder.fromText("10+", Cesium.Color.BLUE, 48).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:any, cluster:any) {cluster.label.show = false;cluster.billboard.show = true;cluster.billboard.id = cluster.label.id;cluster.billboard.verticalOrigin = Cesium.VerticalOrigin.BOTTOM;/* 根据站点(参数)的数量给予对应的广告牌 */if (clusteredEntities.length >= 1000) {cluster.billboard.image = pin1000;} else if (clusteredEntities.length >= 500) {cluster.billboard.image = pin500;} else if (clusteredEntities.length >= 100) {cluster.billboard.image = pin100;} else 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 stylinglet pixelRange = primitiveCluster.pixelRange;primitiveCluster.pixelRange = 0;primitiveCluster.pixelRange = pixelRange;_.myPrimitiveCluster = primitiveCluster;}this.myBillboardCollection = billboardCollection;// start with custom stylecustomStyle();return billboardCollection;}/* @params enable bool值控制开启或关闭集群* @desc 控制集群生效与否*/enableCluster(enable:boolean) {if (Cesium.defined(this.myPrimitiveCluster)) {this.myPrimitiveCluster!.enabled = enable;}}/* @params id 站点ID* @return 返回可操作的广告牌[siteBillboard.image = 'xxxx']* @desc 根据id在集合中获取指定站点广告牌*/getSiteBillboardById(id: string) {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)) {CommomSiteTookit._v.scene.primitives.remove(this.myPrimitives);}}/* @params show bool值 控制显示或隐藏* @desc 隐藏或显示所有站点广告牌*/showStatus(show = true) {this.myPrimitives!.show = show;}/* @desc 根据id删除指定站点广告牌*/remove(id:string) {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)const data = CommomSiteTookit.load({enabled: true,delay: 1200,pixelRange: 20});for(let i = 0; i < 100000; i++) {data.add({image: 'icons/facility.gif',scaleByDistance: new Cesium.NearFarScalar(1.5e2, 1, 1.5e7, 0.2),width: 16,height: 16,position: i.pos,id: i.id});
}```