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

需求背景

需要实现一个上百点批量同时存在的 popup 弹框,为了提高用户体验

1.重叠的弹框,需要隐藏下一层级的 popup

2.为了让用户尽可能看到较全的弹框,需要做弹框的自动避让

解决效果

index.vue

javascript 复制代码
<!--/**
* @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 (!model.showTip) return
  // 碰撞检测
  const {left, top, bottom, right} = viewer.container.getBoundingClientRect()
  model.listData.forEach(async (item, index) => {
    const curIndex = model.listData.findIndex(x => x.name === item.name)
    let width, height, area
    if (!item.AABB) {
      const dom = document.querySelector(`.sectionEntityDom${curIndex}`)
      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 longitude = (item.ctr_points_lonlat[0][0] + item.ctr_points_lonlat[1][0]) / 2
    const latitude = (item.ctr_points_lonlat[0][1] + item.ctr_points_lonlat[1][1]) / 2
    const curPosition = Cesium.Cartesian3.fromDegrees(longitude, latitude, item.heightZ);
    const {x, y} = viewer.scene.cartesianToCanvasCoordinates(curPosition)
    if (index === 0) {
      item.offsetPopupBoxType = "top";
      item.AABB.offsetX = x + offsetPopupBoxOptions["top"] * width
      item.AABB.offsetY = y + offsetPopupBoxOptions["top"] * height
    }
    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
      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
    }
    model.listData[curIndex] = item
  })
}
</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>
相关推荐
智方科技8 小时前
3DTiles之使用customShader调整风格
3d·智慧城市·cesium
山海鲸可视化10 小时前
GIS融合之路(八)-如何用Cesium直接加载OSGB文件(不用转换成3dtiles)
3d·gis·数字孪生·cesium·倾斜摄影·osgb
智方科技14 小时前
如何编译 Cesium 源码
3d·智慧城市·cesium·tilesbuilder
柳晓黑胡椒1 天前
cesiusm实现 多图例展示+点聚合(base64图标)
css3·canvas·base64·cesium·animation
智方科技小智1 天前
3DTiles之使用customShader调整风格
cesium
智方科技小智1 天前
3D Tiles 之 Geometry Error 介绍
cesium
一介青烟小生2 天前
cesium渲染3DTiles模型和glb模型
3d·cesium
undefined&&懒洋洋9 天前
Cesium使用flyToBoundingSphere实现倾斜相机视角观察物体
前端·javascript·cesium·webgis
GIS瞧葩菜12 天前
GeoSever发布图层(保姆姬)
wms·geoserver·cesium
新中地GIS开发老师13 天前
地理信息科学专业想搞GIS开发:学前端还是后端?
前端·javascript·arcgis·前端框架·cesium