封装Cesium中dom元素跟随实体移动的弹窗类

在项目中经常会遇到一个需求就是一个弹窗/面板跟随实体移动。最开始的方法是将html转成图片,然后放到实体的广告板上,但是html转图片会有点卡而且清晰度不够,所以打算换一种思路。
大体思路如下

  1. 在cesium的图层之上创建一个dom元素,设置绝对定位,
  2. 利用postRender不断监听cesium的变化,获取实体的实时位置,
  3. 通过计算得到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;

不足之处欢迎指出🙏

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Cesium点位弹窗跟随点位移动是指在Cesium地图上,当我们点击一个具体的点位时,会弹出一个弹窗,同时该弹窗会随着点位的移动而保持在该点位附近。这个功能在展示地理信息的应用非常常用。 实现这个功能,我们可以借助Cesium实体(Entity)功能。首先,我们需要创建一个实体来表示点位。实体可以包含很多属性,比如位置、图标、标签等。接下来,在点击该点位时,我们可以通过监听该实体的点击事件来触发弹窗的显示和隐藏。同时,我们还需要监听实体移动事件,以便在点位移动时,及时更新弹窗的位置。 具体地,我们可以使用Cesium的Viewer对象来创建地图,然后创建一个Cesium.Entity对象来表示我们的点位。接着,我们可以通过添加监听事件来实现点击弹窗跟随移动的功能。当点击该点位时,我们可以通过设置弹窗的CSS样式来实现显示,同时设置弹窗的位置为当前点位的位置。当点位移动时,我们可以监听实体的位置属性的改变,通过更新弹窗的位置属性来实现跟随移动,即弹窗的位置始终保持在点位的附近。 总结来说,实现Cesium点位弹窗跟随点位移动的功能,需要通过监听实体的点击事件来显示和隐藏弹窗,同时通过监听实体的位置属性的改变来更新弹窗的位置。这样就可以实现一个点位弹窗,在点位移动时保持在点位的附近随着点位移动的效果。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值