在项目中经常会遇到一个需求就是一个弹窗/面板跟随实体移动。最开始的方法是将html转成图片,然后放到实体的广告板上,但是html转图片会有点卡而且清晰度不够,所以打算换一种思路。
大体思路如下
- 在cesium的图层之上创建一个dom元素,设置绝对定位,
- 利用
postRender
不断监听cesium的变化,获取实体的实时位置, - 通过计算得到dom的位置,使用left、top设置dom位置
如此一来,便可以达到弹窗跟随实体移动的效果,具体实现不难,请看代码注释。
效果演示:
CesiumPopup
因为最近在学习ts,所以就顺便写成ts练练手了。关键代码如下:
// 枚举类型,实体点对于弹窗的位置
enum PLACEMENT {
TOP_LEFT, // 实体中心点在弹窗的左上角
TOP_RIGHT, // 实体中心点在弹窗的右上角
BOTTOM_LEFT, // 实体中心点在弹窗的左下角
BOTTOM_RIGHT, // 实体中心点在弹窗的右下角
CENTER, // 实体中心点在弹窗的中心
}
interface PopupOptions {
placement: PLACEMENT; // 实体点对于弹窗的位置
isAdaptive: boolean; // 是否自适应
offsetLeft: number;
offsetTop: number;
}
/**
* CesiumPopup 类,用于创建跟随实体移动的弹窗
* @example
* const popup = new CesiumPopup(domElement, viewer, entity, {
* placement: CesiumPopup.PLACEMENT.TopLeft,
* isAdaptive: true,
* });
*/
class CesiumPopup {
static PLACEMENT = PLACEMENT;
private dom: HTMLElement;
private viewer: Cesium.Viewer;
private entity: Cesium.Entity;
private placement: PLACEMENT;
private isAdaptive: boolean;
private offsetLeft: number;
private offsetTop: number;
// 默认配置项
private defaultOptions = {
placement: PLACEMENT.BOTTOM_RIGHT,
isAdaptive: false,
offsetLeft: 0,
offsetTop: 0,
};
/**
* 创建一个 CesiumPopup 实例
* @param {HTMLElement} dom - 弹窗的 DOM 元素
* @param {Cesium.Viewer} viewer - Cesium Viewer 实例
* @param {Cesium.Entity} entity - 跟随的实体
* @param {PopupOptions} [options={}] - 其他配置项
*/
constructor(
dom: HTMLElement,
viewer: Cesium.Viewer,
entity: Cesium.Entity,
options: PopupOptions = this.defaultOptions
) {
// 使用对象解构和扩展运算符合并默认值和传入的配置项
const { placement, isAdaptive, offsetLeft, offsetTop } = { ...this.defaultOptions, ...options };
this.dom = dom;
this.viewer = viewer;
this.entity = entity;
this.placement = placement;
this.isAdaptive = isAdaptive;
this.offsetLeft = offsetLeft;
this.offsetTop = offsetTop;
}
/**
* 获取弹窗的宽度
* @returns {number} 弹窗的宽度
*/
getWidth(): number {
return this.dom.clientWidth;
}
/**
* 获取弹窗的高度
* @returns {number} 弹窗的高度
*/
getHeight(): number {
return this.dom.clientHeight;
}
/**
* 设置弹窗位置
* @param {Object} pos - 位置对象
* @param {number} pos.left - 弹窗的左边距
* @param {number} pos.top - 弹窗的上边距
*/
setPosition(pos: { left: number; top: number }) {
const elStyle = this.dom.style;
elStyle.left = `${pos.left + this.offsetLeft}px`;
elStyle.top = `${pos.top + this.offsetTop}px`;
}
/**
* 设置弹窗的可见性
* @param {boolean} visible - 是否可见
*/
setVisible(visible: boolean) {
const elStyle = this.dom.style;
elStyle.display = visible ? 'block' : 'none';
}
/**
* 检查弹窗是否显示
* @returns {boolean} 是否显示
*/
isVisible(): boolean {
return this.dom.style.display === 'block';
}
/**
* 更新弹窗位置
* @param {Object} position - 实体的屏幕位置
* @param {number} position.left - 屏幕位置的左边距
* @param {number} position.top - 屏幕位置的上边距
*/
updatePosition(position: { left: number; top: number }) {
// 获取元素宽高
const elWidth = this.getWidth();
const elHeight = this.getHeight();
// 获取弹窗位置
let left = position.left;
let top = position.top;
// 根据位置和方向计算弹窗位置
switch (this.placement) {
case PLACEMENT.TOP_LEFT:
left -= elWidth;
top -= elHeight;
break;
case PLACEMENT.TOP_RIGHT:
top -= elHeight;
break;
case PLACEMENT.BOTTOM_LEFT:
left -= elWidth;
break;
case PLACEMENT.CENTER:
left -= elWidth / 2;
top -= elHeight / 2;
break;
case PLACEMENT.BOTTOM_RIGHT:
default:
break;
}
// 弹窗位置自适应,尽量保证弹窗在视口内
if (this.isAdaptive && this.placement !== PLACEMENT.CENTER) {
if (left < 0) {
left = position.left; // 水平镜像处理
} else if (
this.placement !== PLACEMENT.TOP_LEFT &&
this.placement !== PLACEMENT.BOTTOM_LEFT &&
left + elWidth > window.innerWidth
) {
left -= elWidth; // 水平镜像处理
}
if (top < 0) {
top = position.top; // 垂直镜像处理
} else if (
this.placement !== PLACEMENT.TOP_LEFT &&
this.placement !== PLACEMENT.TOP_RIGHT &&
top + elHeight > window.innerHeight
) {
top -= elHeight; // 垂直镜像处理
}
}
this.setPosition({
left: left,
top: top,
});
}
/**
* 弹窗跟随实体移动
* @returns {Function} 返回移除监听的方法
*/
listenPostRender(): () => void {
// 在外部定义 handlePostRender 函数以便能够移除监听
const handlePostRender = () => {
// 检查实体和其位置是否已定义
if (this.entity && this.entity.position) {
// 获取实体的位置(Cartesian3)
let cartesian3: Cesium.Cartesian3 | undefined | any;
// 如果getValue可用,则调用getValue方法获取当前时间对应的位置,否则直接使用position属性
if (this.entity.position.getValue) {
cartesian3 = this.entity.position.getValue(
this.viewer.clock.currentTime,
);
} else {
cartesian3 = this.entity.position;
}
// 如果位置未定义,则不往下执行
if (!cartesian3) return;
// 判断实体是否在地球背面
const isVisible: Boolean = new Cesium.EllipsoidalOccluder(
Cesium.Ellipsoid.WGS84,
this.viewer.camera.position,
).isPointVisible(cartesian3);
// 如果实体在地球背面,则隐藏弹窗,不往下执行
if (!isVisible) {
this.setVisible(false);
return;
}
this.setVisible(true);
// 将位置转换为屏幕坐标
const windowPos = Cesium.SceneTransforms.wgs84ToWindowCoordinates(
this.viewer.scene,
cartesian3,
);
// 更新弹窗位置
this.updatePosition({
left: windowPos.x,
top: windowPos.y,
});
}
};
// 移除监听的方法
this.removePostRenderListener = () => {
this.viewer.scene.postRender.removeEventListener(handlePostRender);
};
// 先更新位置
handlePostRender();
// 移除之前的监听
this.removePostRenderListener();
// 添加事件监听
this.viewer.scene.postRender.addEventListener(handlePostRender);
// 返回移除监听的方法
return this.removePostRenderListener;
}
/**
* 移除监听
*/
removePostRenderListener(){}
}
export default CesiumPopup;
不足之处欢迎指出🙏