cesiusm实现 多图例展示+点聚合(base64图标)

需求背景

1.需要展示多个站点图例的图表及闪烁效果

2.需要考虑层级高时,多图例的点聚合效果,且点聚合显示需要采用设计的圆形图标

解决思路

闪烁效果:采用 css3的 animation动画属性

圆形图标:采用canvas的toDataURL方法,生产base64图片

解决效果

index.vue

javascript 复制代码
/**
* @author: liuk
* @date: 2024-11-13 16:40:51
* @describe:图例展示+点聚合(base64图标)
*/
<template>
  <div class="legendShow-wrap">
    <div class="legendBox">
      <div class="lengendBtn" @click='isFlat = !isFlat' :class="{select: isFlat === true}">
        <span>图例</span>
        <el-icon v-show="isFlat">
          <ArrowDown/>
        </el-icon>
        <el-icon v-show="!isFlat">
          <ArrowUp/>
        </el-icon>
      </div>
      <ul v-if="isFlat">
        <li v-for="item of legendImgList" :key='item.id' @click="updateProject(item)"
            :class="item.typeArr[0] === curId ? 'select':''">
          <div class="imgBox">
            <img :src="item.img" alt="">
          </div>
          <p>{{ item.typeArr[0] }}</p>
        </li>
      </ul>
    </div>
    <ul class="animation-list" ref="animationListRef"></ul>
    <!-- 默认加载天地图-->
    <Tdt_img_d/>
  </div>
</template>

<script lang="ts" setup>
// Component
import Tdt_img_d from "@/views/cesium/component/controlPanel/layerManagement/basicMap/tdt_img_d.vue"
import {onMounted, onUnmounted, reactive, toRefs, ref} from "vue";
import jsonData from "./data.json"
import {usemapStore} from "@/store/modules/cesiumMap";
// Refs
const animationListRef = ref(null)

const mapStore = usemapStore()
const model = reactive({
  isFlat: false,// 是否展开
  curId: '',//当前选中图例
})
const {isFlat, curId} = toRefs(model)

onMounted(() => {
  mapResetCamera()
  viewer.dataSources.add(lengendShowDatasource);
  viewer.scene.postRender.addEventListener(showPopupBox);
  lengendShowDatasource.clustering.clusterEvent.addEventListener(clusteredFn)
  addEntity()
})

onUnmounted(() => {
  lengendShowDatasource.clustering.clusterEvent.removeEventListener(clusteredFn)
  lengendShowDatasource?.entities?.removeAll()
  viewer.dataSources.remove(lengendShowDatasource);
  viewer.scene.postRender.removeEventListener(showPopupBox);
})


const updateProject = (row) => {
  if (model.curId === row.typeArr[0]) {
    lengendShowDatasource.entities.values.forEach(entity => entity.show = true)
    model.curId = ""
    return
  }
  lengendShowDatasource.entities.values.forEach(entity => {
    entity.show = row.typeArr.includes(entity.data.state)
  })
  model.curId = row.typeArr[0]
}

// 地图逻辑
const viewer = mapStore.getCesiumViewer()
const lengendShowDatasource = new Cesium.CustomDataSource("lengendShow");
lengendShowDatasource.clustering.pixelRange = 2; //多少像素矩形范围内聚合
lengendShowDatasource.clustering.minimumClusterSize = 3;
lengendShowDatasource.clustering.enabled = true;

const addEntity = () => {
  jsonData.forEach(item => {
    const {longitude, latitude, state, level} = item
    const legendData = legendImgList.find(x => x.typeArr.includes(state))
    lengendShowDatasource.entities.add({
      customType: "lengendShow",
      position: Cesium.Cartesian3.fromDegrees(longitude, latitude),
      data: item,
      billboard: {
        image: legendData.img,
        scale: 0.5,
        horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
        verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
        scaleByDistance: new Cesium.NearFarScalar(1.5e2, 1.0, 8.0e6, 0.2),
        disableDepthTestDistance: Number.POSITIVE_INFINITY, //解决遮挡问题
        heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
        // distanceDisplayCondition: new Cesium.DistanceDisplayCondition(0, 5 * 1e5),
      },
    })
    const li = document.createElement('li') // 后面加载dom,不在组件的scope里面
    li.longitude = longitude
    li.latitude = latitude
    li.typeArr = legendData.typeArr
    li.className = 'animation-point'
    li.style.color = getColor(level)
    animationListRef.value.appendChild(li)
  })
}

const showPopupBox = () => {
  const cameraPosition = viewer.camera.positionWC; // 相机世界坐标
  ;[...animationListRef.value.children].forEach(dom => {
    const {longitude, latitude, typeArr} = dom
    const curPosition = Cesium.Cartesian3.fromDegrees(longitude, latitude, 0);
    const {x, y} = viewer.scene.cartesianToCanvasCoordinates(curPosition)
    dom.style.left = x + "px"
    dom.style.top = y + "px"
    const distance = Cesium.Cartesian3.distance(cameraPosition, curPosition);
    if ((model.curId && !typeArr.includes(model.curId)) || distance < 0 || distance > 1e6) {
      dom.style.display = "none"
    } else {
      dom.style.display = "block"
    }
  })
}

const clusteredFn = (clusteredEntities, cluster) => {
  cluster.label.show = false;
  cluster.billboard.show = true;
  cluster.billboard.scale = 0.5; // 解决数字文本锯齿
  cluster.billboard.image = getCircleImage(clusteredEntities.length);
  cluster.billboard.disableDepthTestDistance = Number.POSITIVE_INFINITY //解决遮挡问题
}

import png1 from "@/assets/images/legendShow/lx.png"
import png2 from "@/assets/images/legendShow/sg.png"
import png3 from "@/assets/images/legendShow/fh.png"
import png4 from "@/assets/images/legendShow/zj.png"
import png5 from "@/assets/images/legendShow/cy.png"
import png6 from "@/assets/images/legendShow/zy.png"
import png7 from "@/assets/images/legendShow/rk.png"
import png8 from "@/assets/images/legendShow/gh.png"

const legendImgList = [// 图例数据
  {img: png1, typeArr: ["立项"]},
  {img: png2, typeArr: ["施工"]},
  {img: png3, typeArr: ["复核"]},
  {img: png4, typeArr: ["自验", "未自验", "已自验", "初验驳回", "自验"],},
  {img: png5, typeArr: ["初验", "待初验", "初验通过", "终验驳回", "市级同意终验驳回"]},
  {img: png6, typeArr: ["终验", "申请终验中", "申请发起核验", "申请开展验收", "申请终验驳回", "市级同意核验", "市级同意终验", "核验中", "核验完成", "终验通过",],},
  {img: png7, typeArr: ["入库", "申请入库中", "县级核定完成"]},
  {img: png8, typeArr: ["管护", "市级同意入库", "管护"]},
]

const getColor = (level) => {
  switch (level) {
    case 1:
      return "#eaff56";
    case 2:
      return "#ff461f";
    case 3:
      return "#f20c00";
  }
}
// 生产圆形图标 base64格式
const getCircleImage = (count, option = {}) => {
  let {clr, clr2, font, fontColor} = option
  const options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
  const circleImageRadius = 28;
  const circleImageRadius2 = 28 - 5
  if (!options.color) {
    if (count < 10) {
      clr = "rgba(181, 226, 140, 0.6)";
      clr2 = "rgba(110, 204, 57, 0.5)";
    } else if (count < 100) {
      clr = "rgba(241, 211, 87, 0.6)";
      clr2 = "rgba(240, 194, 12, 0.5)";
    } else {
      clr = "rgba(253, 156, 115, 0.6)";
      clr2 = "rgba(241, 128, 23, 0.5)";
    }
  }

  const thisSize = circleImageRadius * 2;

  const circleCanvas = document.createElement("canvas");
  circleCanvas.width = thisSize;
  circleCanvas.height = thisSize;
  const circleCtx = circleCanvas.getContext("2d");

  circleCtx.fillStyle = "#ffffff00";
  circleCtx.globalAlpha = 0.0;
  circleCtx.fillRect(0, 0, thisSize, thisSize);

  //圆形底色 (外圈)
  if (clr) {
    circleCtx.globalAlpha = 1.0;
    circleCtx.beginPath();
    circleCtx.arc(circleImageRadius, circleImageRadius, circleImageRadius, 0, Math.PI * 2, true);
    circleCtx.closePath();
    circleCtx.fillStyle = clr;
    circleCtx.fill();
  }

  //圆形底色(内圈)
  if (clr2) {
    circleCtx.globalAlpha = 1.0;
    circleCtx.beginPath();
    circleCtx.arc(circleImageRadius, circleImageRadius, circleImageRadius2, 0, Math.PI * 2, true);
    circleCtx.closePath();
    circleCtx.fillStyle = clr2;
    circleCtx.fill();
  }

  //数字文字
  circleCtx.font = font || circleImageRadius2 * 0.9 + "px bold normal";
  circleCtx.fillStyle = fontColor || "#ffffff";
  circleCtx.textAlign = "center";
  circleCtx.textBaseline = "middle";
  circleCtx.fillText(count, circleImageRadius, circleImageRadius);

  return circleCanvas.toDataURL();
};

const mapResetCamera = () => {
  viewer.camera.flyTo({
    destination: Cesium.Cartesian3.fromDegrees(106.487115, 21.464166, 290064.99),
    orientation: {
      heading: Cesium.Math.toRadians(360.0),
      pitch: Cesium.Math.toRadians(-48.9),
      roll: 0.0
    }
  });
}
</script>

<style lang="scss" scoped>
.legendShow-wrap {
  .legendBox {
    position: fixed;
    left: 70px;
    bottom: 35px;
    width: 115px;
    border-radius: 4px;
    background: rgba(29, 40, 57, 0.6);
    backdrop-filter: blur(4px);
    pointer-events: auto;

    .lengendBtn {
      width: calc(100% - 10px);
      height: 35px;
      margin: 5px auto 5px;
      line-height: 35px;
      text-align: center;
      border: 1px solid #7588aab0;
      border-radius: 4px;
      color: rgba(255, 255, 255, 0.8);
      background: rgba(23, 40, 53, 0.4);
      backdrop-filter: blur(4px);
      box-shadow: rgb(7 98 255 / 30%) 0px 0px 2px 1px;
      cursor: pointer;

      &:hover {
        border-color: #4086ffb0;
        color: rgb(255, 255, 255);
        text-shadow: rgb(7 98 255 / 50%) 0px 0px 8px;
      }

      &.select {
        border: 1px solid #4086ffb0;
        color: rgb(255, 255, 255);
        background: rgba(25, 56, 111, 0.6);
        text-shadow: rgb(7 98 255 / 50%) 0px 0px 8px;
      }

      i {
        margin-left: 5px;
      }
    }

    ul {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      justify-content: center;
      width: 100%;
      height: 100%;
      color: rgba(255, 255, 255, 0.856);
      transition: all 3s linear 1s; //过渡为什么没用


      li {
        display: flex;
        justify-content: center;
        width: 100%;
        margin: 3px 10px;
        opacity: 0.8;
        cursor: pointer;

        &:hover {
          opacity: 1;
        }

        &.select {
          opacity: 1;
          color: #4086ffb0;
          font-weight: 600;
        }

        .imgBox {
          width: 20px;
          margin-right: 15px;

          img {
            width: 100%;
            height: 100%;
          }
        }
      }
    }
  }
}
</style>
<style lang="scss">
.animation-point {
  position: fixed;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  border: 1px solid hsla(0, 0%, 100%, .5);
  cursor: pointer;
  color: #0ff;
  background: currentColor;
  transform: translate(-50%, 50%);
  box-shadow: 0 0 2em currentColor, 0 0 .5em currentColor;

  &::after, &::before {
    content: "";
    position: absolute;
    width: 0;
    height: 0;
    left: 50%;
    top: 50%;
    border: 1px solid;
    border-radius: 50%;
    transform: translate(-50%, -50%);
    animation: mapAni 1s ease infinite
  }
}

@keyframes mapAni {
  0% {
    width: 0;
    height: 0;
    opacity: 1;
    filter: alpha(opacity=1)
  }
  25% {
    width: 12px;
    height: 12px;
    opacity: .7;
    filter: alpha(opacity=70)
  }
  50% {
    width: 20px;
    height: 20px;
    opacity: .5;
    filter: alpha(opacity=50)
  }
  75% {
    width: 30px;
    height: 30px;
    opacity: .2;
    filter: alpha(opacity=20)
  }
  to {
    width: 40px;
    height: 40px;
    opacity: 0;
    filter: alpha(opacity=0)
  }
}

</style>
相关推荐
汪洪墩4 小时前
【Mars3d】设置backgroundImage、map.scene.skyBox、backgroundImage来回切换
开发语言·javascript·python·ecmascript·webgl·cesium
supermapsupport9 小时前
iClent3D for Cesium 实现无人机巡检飞行效果
gis·cesium·supermap·webgis
不浪brown11 小时前
仅需3行代码!带你做一个3D卫星轨道飞行动画
cesium
安冬的码畜日常11 小时前
【CSS in Depth 2 精译_088】第五部分:添加动效概述 + 第 15 章:CSS 过渡特效概述 + 15.1:状态间的由此及彼
前端·css·css3·html5·css过渡
桃园码工20 小时前
4_使用 HTML5 Canvas API (3) --[HTML5 API 学习之旅]
前端·html5·canvas
孤留光乩21 小时前
从零搭建纯前端飞机大战游戏(附源码)
前端·javascript·游戏·html·css3
东离与糖宝2 天前
说说你对 css3 display:flex 弹性盒模型 的理解
前端·css·css3
WebDesign_Mu2 天前
HTML5+CSS3+JS制作精美的宠物主题网站(内附源码,含5个页面)
前端·javascript·css·html·css3·html5·宠物
白嫖叫上我2 天前
Cesium 无人机航线规划(航点航线)
无人机·cesium
荆州克莱3 天前
什么是微端游戏?微端应该选择什么配置的服务器?
spring boot·spring·spring cloud·css3·技术