cesium 实现批量divpoint气泡,及气泡碰撞测试与自动避让

需求背景

需要实现一个上百点批量同时存在的 popup 弹框,为了提高用户体验
1.重叠的弹框,需要隐藏下一层级的 popup
2.为了让用户尽可能看到较全的弹框,需要做弹框的自动避让

解决效果

index.vue

<!--/**
* @author: liuk
* @date: 2024-08-20
* @describe:数值
*/-->
<template>
  <div class="numericalValue-wrap">
    <teleport to="body">
      <ul v-show="showTip && item.visible"
          v-for="(item,index) in listData" :key="index"
          :class="['surveyStation-popup','sectionEntityDom'+index,'section-popup',item.offsetPopupBoxType,
          item?.levelOverflow >= 0.01 ? 'waterlevel-overflow' : ''
          ]"
          :style="{
            transform: `translate(${item.AABB?.offsetX || 0}px, ${item.AABB?.offsetY ||0}px)`}">
        <li>名称:<span class="label">{{ index }}</span></li>
        <li>编号:<span class="label">{{ index }}</span></li>
        <li>
          水位:
          <span class="num">{{ item.waterLevel }}</span>m
          <span style="color:red" v-if="item.levelOverflow>= 0.01">{{ item.levelOverflow.toFixed(2) }}</span>
        </li>
        <li>流量:<span class="num">{{ item.flow }}</span> mm</li>
      </ul>
    </teleport>
  </div>
</template>

<script lang="ts" setup>
import {onMounted, onUnmounted, reactive, toRefs} from "vue";

const model = reactive({
  showTip: true,
  listData: [],
  popupPoss: [],
  curId: "",
  dialogVisible: false
})
const {showTip, showGrid, popupPoss, listData, curId, dialogVisible} = toRefs(model)

onMounted(() => {
  getlist()
  viewer.dataSources.add(sectionDatasource);
  handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
  handler.setInputAction(onMouseMove, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
  handler.setInputAction(onMouseClick, Cesium.ScreenSpaceEventType.LEFT_CLICK);
  viewer.camera.percentageChanged = 0;
  viewer.scene.camera.changed.addEventListener(showPopupBox);
})

onUnmounted(() => {
  sectionDatasource.entities.removeAll()
  handler.destroy()
  viewer.dataSources.remove(sectionDatasource);
  viewer.scene.camera.changed.removeEventListener(showPopupBox);
})

const getlist = () => {
  const data = [
    {
      "ctr_points_lonlat": [
        [113.04510386306632,25.748247970488464],
        [113.04619931039747,25.746722270257674]
      ],
    },
    /* ... */
  ]
  setTimeout(() => {
    model.listData = data || []
    model.popupPoss = new Array(data.length).fill("").map(() => ({}))
    addTip(data)
  }, 500)
}

// 地图逻辑
import {usemapStore} from "@/store/modules/cesiumMap";
import mittBus from "@/utils/mittBus";

const sectionDatasource = new Cesium.CustomDataSource("section");

const mapStore = usemapStore()
let handler, PreSelEntity
const viewer = mapStore.getCesiumViewer();
const addTip = (data) => {
  data.forEach(item => {
    sectionDatasource.entities.add({
      customType: "sectionEntity",
      id: item.label,
      data: item,
      polyline: {
        positions: Cesium.Cartesian3.fromDegreesArray(item.ctr_points_lonlat.flat()),
        material: Cesium.Color.fromCssColorString("yellow").withAlpha(1),
        width: 5,
      }
    })
  })
}

const onMouseMove = (movement) => {
  if (PreSelEntity) {
    PreSelEntity.polyline.material = Cesium.Color.fromCssColorString("yellow").withAlpha(1)
    PreSelEntity = null
  }
  const pickedObject = viewer.scene.pick(movement.endPosition);
  if (!Cesium.defined(pickedObject) || !Cesium.defined(pickedObject.id)) return
  const entity = pickedObject.id;
  if (!(entity instanceof Cesium.Entity) || entity.customType !== "sectionEntity") return
  entity.polyline.material = Cesium.Color.fromCssColorString("red").withAlpha(1)
  if (entity !== PreSelEntity) PreSelEntity = entity;
}

const onMouseClick = (movement) => {
  const pickedObject = viewer.scene.pick(movement.position);
  if (!Cesium.defined(pickedObject) || !Cesium.defined(pickedObject.id)) return
  const entity = pickedObject.id;
  if (!(entity instanceof Cesium.Entity) || entity.customType !== "sectionEntity") return
  model.curId = entity.id
}

const offsetPopupBoxOptions = {
  top: [-0.5, -1],
  bottom: [-0.5, 0],
  right: [0, -0.5],
  left: [-1, -0.5],
}
const showPopupBox = () => {
  if (!showTip.value) return
  // 碰撞检测
  const {left, top, bottom, right} = viewer.container.getBoundingClientRect()
  const CanvasWidth = parseInt(getComputedStyle(viewer.container).width)
  model.listData.forEach(async (item, index) => {
    let width, height, area
    if (!item.AABB) {
      const dom = document.querySelector(`.sectionEntityDom${props.isDualMap ? 'DualMap' : ''}${index}`)
      width = parseInt(getComputedStyle(dom).width) + 2 * parseInt(getComputedStyle(dom).padding.split(" ")[1])
      height = parseInt(getComputedStyle(dom).height) + 2 * parseInt(getComputedStyle(dom).padding.split(" ")[0])
      area = width * height
      item.AABB = {width, height, area: width * height}
    } else {
      width = item.AABB.width
      height = item.AABB.height
      area = item.AABB.area
    }
    const curPosition = Cesium.Cartesian3.fromDegrees(item.longitude, item.latitude, 0);
    let x, y;
    try {
      const obj = viewer.scene.cartesianToCanvasCoordinates(curPosition)
      x = obj.x
      y = obj.y
    } catch (e) {
      item.visible = false
    }
    if (!x) return
    if (index === 0) {
      item.offsetPopupBoxType = "top";
      item.AABB.offsetX = x + offsetPopupBoxOptions["top"][0] * width + (props.isDualMap ? CanvasWidth : 0)
      item.AABB.offsetY = y + offsetPopupBoxOptions["top"][1] * height
      item.visible = true
      return
    }
    const offsetPopupBoxKeys = Object.keys(offsetPopupBoxOptions)
    const toChecks = model.listData.slice(0, index) // 需要测试碰撞的单位
    offsetPopupBoxKeys.some((type) => {
      item.offsetPopupBoxType = ""
      item.AABB.offsetX = x + offsetPopupBoxOptions[type][0] * width + (props.isDualMap ? CanvasWidth : 0)
      item.AABB.offsetY = y + offsetPopupBoxOptions[type][1] * height
      const check = toChecks.every(checkItem => {
        const box1 = checkItem.AABB
        const box2 = item.AABB
        let intersectionArea = 0 // 相交面积
        // 计算在每个轴上的重叠部分
        const overlapX = Math.min(box1.offsetX + box1.width, box2.offsetX + box2.width) - Math.max(box1.offsetX, box2.offsetX);
        const overlapY = Math.min(box1.offsetY + box1.height, box2.offsetY + box2.height) - Math.max(box1.offsetY, box2.offsetY);
        // 如果在两个轴上都有重叠,则计算相交区域的面积
        if (overlapX > 0 && overlapY > 0) intersectionArea = overlapX * overlapY;
        return intersectionArea <= area * 0.05;
      });
      if (check) {
        item.offsetPopupBoxType = type
      }
      return check
    })
    switch (true) { // 屏幕边界限制
      case item.AABB.offsetX + width <= right && item.AABB.offsetX >= left && item.AABB.offsetY >= top && item.AABB.offsetY + height <= bottom:
        item.visible = !!item.offsetPopupBoxType;
        break
      default:
        item.visible = false;
        break
    }
  })
}
</script>

<style lang="scss">
.surveyStation-popup {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 3;
  margin: 0;
  padding: 7px 15px;
  list-style: none;
  background: rgba(5, 9, 9, 0.6);
  border-radius: 4px;
  font-size: 14px;
  color: #fff;
  cursor: default;
  --w: 24px;
  --h: 10px;

  &::before {
    content: "";
    background-color: rgba(0, 0, 0, 0.7);
    position: absolute;
    bottom: 0;
    left: 50%;
    width: var(--w);
    height: var(--h);
    transform: translate(-50%, 100%) translateY(-0.5px);
    clip-path: polygon(50% 100%, 0 0, 100% 0);
  }
  &.ponint-list::before{
    display: none;
  }

  &.map2d {
    margin-left: -15px; // 二维图片底座尺寸大小
    margin-top: -50px;
  }

  &.map3d {
    margin-left: -15px; // 三维图片底座尺寸大小
    margin-top: -100px;
  }

  .ponint-list-li {
    cursor: pointer;

    &:hover {
      background: rgba(204, 204, 204, .6);
    }
  }
}

.section-popup {
  --w: 24px;
  --h: 10px;
  width: 150px;
  height: 80px;
  margin-top: -10px;

  &::before {
    content: "";
    background-color: rgba(0, 0, 0, 0.7);
    position: absolute;
    bottom: 0;
    left: 50%;
    width: 24px;
    height: 10px;
    transform: translate(-50%, 100%) translateY(-0.5px);
    clip-path: polygon(50% 100%, 0 0, 100% 0);
  }

  &.top {
    margin-top: calc(var(--h) * -1);

    &::before {
      top: auto;
      bottom: 0;
      right: auto;
      left: 50%;
      width: var(--w);
      height: var(--h);
      transform: translate(-50%, 100%) translateY(-0.5px);
      clip-path: polygon(50% 100%, 0 0, 100% 0);
    }
  }

  &.bottom {
    margin-top: var(--h);

    &::before {
      top: 0;
      bottom: auto;
      right: auto;
      left: 50%;
      width: var(--w);
      height: var(--h);
      transform: translate(-50%, -100%) translateY(0.5px);
      clip-path: polygon(50% 0, 0 100%, 100% 100%);
    }
  }

  &.right {
    margin-left: var(--h);

    &::before {
      top: 50%;
      bottom: auto;
      right: auto;
      left: 0;
      width: var(--h);
      height: var(--w);
      transform: translate(-100%, -50%) translateX(0.5px);
      clip-path: polygon(100% 0, 0 50%, 100% 100%);
    }
  }

  &.left {
    margin-left: calc(var(--h) * -1);

    &::before {
      top: 50%;
      bottom: auto;
      right: 0;
      left: auto;
      width: var(--h);
      height: var(--w);
      transform: translate(100%, -50%) translateX(-0.5px);
      clip-path: polygon(0 100%, 0 0, 100% 50%);
    }
  }

  &.waterlevel-overflow {
    animation: dm-yj-breathe 800ms ease-in-out infinite;
    animation-direction: alternate;
  }

  .label {
    color: #00ff00;
  }

  .num {
    color: orange;
  }
}
</style>
  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Cesium是一个用于构建地理空间应用的开源JavaScript库。借助Cesium,可以通过在虚拟地球上加入各种元素,包括气泡框。 要实现一个气泡框,首先需要通过Cesium创建一个场景,并在场景中添加需要显示的实体或标记。可以使用Cesium的Entity对象或Marker对象来表示这些元素。然后,可以通过为每个实体或标记添加描述信息,将气泡框链接到相应的元素上。 在添加实体或标记时,可以为每个元素指定位置信息,例如经纬度或笛卡尔坐标。这样,当用户与虚拟地球交互时,可以根据元素的位置在相应的位置显示气泡框。 为了实现气泡框的显示效果,可以使用Cesium的Popup对象或自定义HTML元素来创建一个浮动的信息窗口。可以使用CSS样式定义气泡框的外观,例如背景颜色、边框和文本样式等。而且,Cesium提供了丰富的API来控制气泡框的显示位置、大小和内容。 要显示一个气泡框,可以在用户与元素交互时触发事件,例如当鼠标悬停在元素上或用户点击元素时。在事件处理程序中,可以根据元素的位置和描述信息,动态创建气泡框,并将其添加到场景中。在气泡框中,可以显示元素的详细信息,例如名称、描述、图标等。 总之,使用Cesium可以很方便地实现气泡框的效果。通过添加实体或标记,并使用Popup对象或自定义HTML元素,可以在虚拟地球上显示具有交互性的气泡框,以展示元素的详细信息。无论是构建地图应用还是可视化地理数据,Cesium都是一个强大的工具。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柳晓黑胡椒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值