vue3+cesium测量功能(距离 面积 点位 高度差)

直接放到vue项目即可运行
 

<template>
  <div class="measure-demo">
    <div class="cesium-container" ref="viewerContainer" id="cesiumContainer"></div>
    <div class="control-panel">
      <h2>测量工具</h2>
      <div class="button-group">
        <button 
          :class="{ active: activeTool === 'distance' }"
          @click="toggleTool('distance')"
        >
          距离测量
        </button>
        <button 
          :class="{ active: activeTool === 'area' }"
          @click="toggleTool('area')"
        >
          面积测量
        </button>
        <button 
          :class="{ active: activeTool === 'height' }"
          @click="toggleTool('height')"
        >
          高度测量
        </button>
        <button 
          :class="{ active: activeTool === 'point' }"
          @click="toggleTool('point')"
        >
          点位测量
        </button>
        <button @click="clearMeasurements">清除测量</button>
        <button @click="resetView">重置视角</button>
      </div>
      <div class="instruction-panel">
        <h3>使用说明</h3>
        <div v-if="activeTool === 'distance'">
          <p>距离测量:</p>
          <ol>
            <li>点击地图上的起点</li>
            <li>继续点击添加途经点</li>
            <li>双击结束测量</li>
          </ol>
        </div>
        <div v-else-if="activeTool === 'area'">
          <p>面积测量:</p>
          <ol>
            <li>点击地图绘制多边形顶点</li>
            <li>双击闭合多边形完成测量</li>
          </ol>
        </div>
        <div v-else-if="activeTool === 'height'">
          <p>高度测量:</p>
          <ol>
            <li>点击选择起点</li>
            <li>点击选择终点</li>
            <li>显示两点间的高度差</li>
          </ol>
        </div>
        <div v-else-if="activeTool === 'point'">
          <p>点位测量:</p>
          <ol>
            <li>点击地图任意位置</li>
            <li>显示该点的经纬度坐标</li>
          </ol>
        </div>
        <div v-else>
          <p>请选择上方的测量工具开始测量</p>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import * as Cesium from 'cesium'

const viewerContainer = ref(null)
const viewer = ref(null)
const activeTool = ref('')
const measureEntities = ref([])
let handler = null

// 初始化Cesium viewer
const initViewer = async () => {
  if (!viewerContainer.value) return
  const terrainProvider = await Cesium.createWorldTerrainAsync();
  viewer.value = new Cesium.Viewer(viewerContainer.value, {
    terrainProvider:terrainProvider,
    timeline: false,
    animation: false,
    baseLayerPicker: false,
    geocoder: false,
    homeButton: false,
    sceneModePicker: false,
    navigationHelpButton: false,
    fullscreenButton: false
  })

  // 设置默认视角
  viewer.value.camera.setView({
    destination: Cesium.Cartesian3.fromDegrees(116.391, 39.901, 15000.0)
  })

  handler = new Cesium.ScreenSpaceEventHandler(viewer.value.scene.canvas)
}

// 显示错误提示
const showErrorMessage = (message) => {
  if (!viewer.value) return
  
  const container = document.createElement('div')
  container.className = 'error-message'
  container.innerHTML = message
  document.body.appendChild(container)
  
  setTimeout(() => {
    document.body.removeChild(container)
  }, 3000)
}

// 切换测量工具
const toggleTool = (tool) => {
  if (activeTool.value === tool) {
    activeTool.value = ''
    removeHandler()
  } else {
    activeTool.value = tool
    setupHandler(tool)
  }
}

// 设置事件处理器
const setupHandler = (tool) => {
  removeHandler()
  if (!viewer.value || !handler) return

  switch (tool) {
    case 'distance':
      setupDistanceMeasurement()
      break
    case 'area':
      setupAreaMeasurement()
      break
    case 'height':
      setupHeightMeasurement()
      break
    case 'point':
      setupPointMeasurement()
      break
  }
}

// 距离测量实现
const setupDistanceMeasurement = () => {
  if (!viewer.value || !handler) return

  const positions = []
  let activeShape = null

  // 获取鼠标点击位置
  const getPosition = (position) => {
    let earthPosition
    // 球面
    if (viewer.value?.terrainProvider instanceof Cesium.EllipsoidTerrainProvider) {
      earthPosition = viewer.value.scene.camera.pickEllipsoid(position)
    }
    // 地形
    else {
      const ray = viewer.value?.camera.getPickRay(position)
      if (ray) {
        earthPosition = viewer.value?.scene.globe.pick(ray, viewer.value.scene)
      }
    }
    return earthPosition
  }

  // 绘制线条
  const drawLine = (positionData) => {
    const entity = viewer.value?.entities.add({
      polyline: {
        positions: positionData,
        clampToGround: true,
        width: 3,
        material: new Cesium.PolylineDashMaterialProperty({
          color: Cesium.Color.YELLOW,
          dashLength: 16.0
        })
      }
    })
    return entity || null
  }

  handler.setInputAction((event) => {
    const earthPosition = getPosition(event.position)
    if (!earthPosition) return

    if (positions.length === 0) {
      positions.push(earthPosition)
      const dynamicPositions = new Cesium.CallbackProperty(() => {
        return positions
      }, false)
      activeShape = drawLine(dynamicPositions)
    }
    positions.push(earthPosition)
  }, Cesium.ScreenSpaceEventType.LEFT_CLICK)

  handler.setInputAction((event) => {
    if (positions.length >= 2) {
      const earthPosition = getPosition(event.endPosition)    
      if (Cesium.defined(earthPosition)) {
        positions.pop()
        positions.push(earthPosition)
      }
    }
  }, Cesium.ScreenSpaceEventType.MOUSE_MOVE)

  handler.setInputAction(() => {
    if (positions.length < 2) {
      showErrorMessage('请至少绘制两个点')
      return
    }

    const distance = calculateDistance(positions)
    
    // 创建最终的线条
    const finalPolyline = drawLine(positions)
    if (finalPolyline) measureEntities.value.push(finalPolyline)
    
    // 添加距离标签
    const labelEntity = viewer.value?.entities.add({
      position: positions[positions.length - 1],
      billboard: {
        image: createDistanceLabel(distance),
        verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
        horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
        pixelOffset: new Cesium.Cartesian2(0, -10)
      }
    })
    if (labelEntity) measureEntities.value.push(labelEntity)
    // 移除动态线条
    if (activeShape) {
      viewer.value?.entities.remove(activeShape)
      activeShape = null
    }
    //取消测量和监听
    removeHandler()
  }, Cesium.ScreenSpaceEventType.RIGHT_CLICK)
}

// 面积测量实现
const setupAreaMeasurement = () => {
  if (!viewer.value || !handler) return

  const positions = []
  let activeShape = null

  // 获取鼠标点击位置
  const getPosition = (position) => {
    let earthPosition
    // 球面
    if (viewer.value?.terrainProvider instanceof Cesium.EllipsoidTerrainProvider) {
      earthPosition = viewer.value.scene.camera.pickEllipsoid(position)
    }
    // 地形
    else {
      const ray = viewer.value?.camera.getPickRay(position)
      if (ray) {
        earthPosition = viewer.value?.scene.globe.pick(ray, viewer.value.scene)
      }
    }
    return earthPosition
  }

  // 绘制多边形
  const drawPolygon = (positionData) => {
    const entity = viewer.value?.entities.add({
      polygon: {
        hierarchy: positionData,
        material: Cesium.Color.YELLOW.withAlpha(0.3),
        outline: true,
        outlineColor: Cesium.Color.WHITE,
        outlineWidth: 2
      }
    })
    return entity || null
  }

  handler.setInputAction((event) => {
    const earthPosition = getPosition(event.position)
    if (!earthPosition) return

    if (positions.length === 0) {
      positions.push(earthPosition)
      const dynamicPositions = new Cesium.CallbackProperty(() => {
        return new Cesium.PolygonHierarchy(positions)
      }, false)
      activeShape = drawPolygon(dynamicPositions)
    }
    positions.push(earthPosition)
  }, Cesium.ScreenSpaceEventType.LEFT_CLICK)

  handler.setInputAction((event) => {
    if (positions.length >= 2) {
      const earthPosition = getPosition(event.endPosition)
      if (Cesium.defined(earthPosition)) {
        positions.pop()
        positions.push(earthPosition)
      }
    }
  }, Cesium.ScreenSpaceEventType.MOUSE_MOVE)

  handler.setInputAction(() => {
    if (positions.length < 3) {
      showErrorMessage('请至少绘制三个点')
      return
    }

    positions.pop()
    const area = calculateArea(positions)
    
    // 创建最终的多边形
    const finalPolygon = drawPolygon(positions)
    if (finalPolygon) measureEntities.value.push(finalPolygon)
    
    // 添加面积标签
    const labelEntity = viewer.value?.entities.add({
      position: positions[0],
      billboard: {
        image: createAreaLabel(area),
        verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
        horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
        pixelOffset: new Cesium.Cartesian2(0, -10)
      }
    })
    if (labelEntity) measureEntities.value.push(labelEntity)
     // 移除动态多边形
     if (activeShape) {   
          viewer.value?.entities.remove(activeShape)
          activeShape = null
        }
    //取消测量和监听
    removeHandler()
  }, Cesium.ScreenSpaceEventType.RIGHT_CLICK)
}

// 高度测量实现
const setupHeightMeasurement = () => {
  if (!viewer.value || !handler) return

  const positions = []
  let activeShape = null

  // 获取鼠标点击位置
  const getPosition = (position) => {
    let earthPosition
    // 球面
    if (viewer.value?.terrainProvider instanceof Cesium.EllipsoidTerrainProvider) {
      earthPosition = viewer.value.scene.camera.pickEllipsoid(position)
    }
    // 地形
    else {
      const ray = viewer.value?.camera.getPickRay(position)
      if (ray) {
        earthPosition = viewer.value?.scene.globe.pick(ray, viewer.value.scene)
      }
    }
    return earthPosition
  }

  // 绘制线条
  const drawLine = (positionData) => {
    const entity = viewer.value?.entities.add({
      polyline: {
        positions: positionData,
        clampToGround: false,
        width: 3,
        material: Cesium.Color.YELLOW
      }
    })
    return entity || null
  }

  handler.setInputAction((event) => {
    const earthPosition = getPosition(event.position)
    if (!earthPosition) return

    if (positions.length === 0) {
      positions.push(earthPosition)
      const dynamicPositions = new Cesium.CallbackProperty(() => {
        return positions
      }, false)
      activeShape = drawLine(dynamicPositions)
    } else if (positions.length === 1) {
      positions.push(earthPosition)
      const height = calculateHeight(positions[0], positions[1])
      
      // 创建最终的线条
      const finalPolyline = drawLine(positions)
      if (finalPolyline) measureEntities.value.push(finalPolyline)
      
      // 添加高度标签
      const labelEntity = viewer.value?.entities.add({
        position: positions[positions.length - 1],
        billboard: {
          image: createHeightLabel(height),
          verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
          horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
          pixelOffset: new Cesium.Cartesian2(0, -10)
        }
      })
      if (labelEntity) measureEntities.value.push(labelEntity)
      
      // 移除动态线条
      if (activeShape) {
        viewer.value?.entities.remove(activeShape)
        activeShape = null
      }
      //取消测量和监听
      removeHandler()
    }
  }, Cesium.ScreenSpaceEventType.LEFT_CLICK)

  handler.setInputAction((event) => {
    if (positions.length >= 2) {
      const earthPosition = getPosition(event.endPosition)    
      if (Cesium.defined(earthPosition)) {
        positions.pop()
        positions.push(earthPosition)
      }
    }
  }, Cesium.ScreenSpaceEventType.MOUSE_MOVE)
}

// 点位测量实现
const setupPointMeasurement = () => {
  if (!viewer.value || !handler) return

  handler.setInputAction((event) => {
    const earthPosition = viewer.value?.scene.pickPosition(event.position)
    if (!earthPosition) return

    const cartographic = Cesium.Cartographic.fromCartesian(earthPosition)
    const longitude = Cesium.Math.toDegrees(cartographic.longitude)
    const latitude = Cesium.Math.toDegrees(cartographic.latitude)
    const height = cartographic.height

    const point = viewer.value?.entities.add({
      position: earthPosition,
      point: {
        pixelSize: 5,
        color: Cesium.Color.RED,
        outlineColor: Cesium.Color.WHITE,
        outlineWidth: 2
      },
      label: {
        text: `经度: ${longitude.toFixed(6)}\n纬度: ${latitude.toFixed(6)}\n高度: ${height.toFixed(2)}米`,
        font: '14px sans-serif',
        fillColor: Cesium.Color.WHITE,
        outlineColor: Cesium.Color.BLACK,
        outlineWidth: 2,
        style: Cesium.LabelStyle.FILL_AND_OUTLINE,
        verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
        pixelOffset: new Cesium.Cartesian2(0, -10)
      }
    })
    if (point) measureEntities.value.push(point)
  }, Cesium.ScreenSpaceEventType.LEFT_CLICK)
}

// 计算距离
const calculateDistance = (positions) => {
  let distance = 0
  for (let i = 0; i < positions.length - 1; i++) {
    distance += Cesium.Cartesian3.distance(positions[i], positions[i + 1])
  }
  return distance
}

// 计算面积
const calculateArea = (positions) => {
  const coordinates = positions.map(position => {
    const cartographic = Cesium.Cartographic.fromCartesian(position)
    return [
      Cesium.Math.toDegrees(cartographic.longitude),
      Cesium.Math.toDegrees(cartographic.latitude)
    ]
  })
  coordinates.push(coordinates[0]) // 闭合多边形

  let area = 0
  for (let i = 0; i < coordinates.length - 1; i++) {
    area += coordinates[i][0] * coordinates[i + 1][1]
    area -= coordinates[i][1] * coordinates[i + 1][0]
  }
  area = Math.abs(area) * 0.5 * 111319.9 * 111319.9
  return area
}

// 计算高度差
const calculateHeight = (start, end) => {
  const startCartographic = Cesium.Cartographic.fromCartesian(start)
  const endCartographic = Cesium.Cartographic.fromCartesian(end)
  return Math.abs(endCartographic.height - startCartographic.height)
}

// 创建距离标签
const createDistanceLabel = (distance) => {
  const canvas = document.createElement('canvas')
  canvas.width = 200
  canvas.height = 100
  const ctx = canvas.getContext('2d')
  if (!ctx) return ''

  // 绘制背景
  ctx.fillStyle = 'rgba(40, 44, 52, 0.8)'
  ctx.strokeStyle = '#007acc'
  ctx.lineWidth = 2
  roundRect(ctx, 0, 0, 180, 60, 8)
  ctx.fill()
  ctx.stroke()

  // 计算文本宽度以实现居中
  ctx.font = 'bold 16px Arial'
  const titleWidth = ctx.measureText('距离').width
  const titleX = (180 - titleWidth) / 2

  ctx.font = '14px Arial'
  const valueText = `${distance.toFixed(2)} 米`
  const valueWidth = ctx.measureText(valueText).width
  const valueX = (180 - valueWidth) / 2

  // 绘制居中的文本
  ctx.fillStyle = 'white'
  ctx.font = 'bold 16px Arial'
  ctx.fillText('距离', titleX, 25)
  ctx.font = '14px Arial'
  ctx.fillText(valueText, valueX, 45)

  return canvas.toDataURL()
}

// 创建面积标签
const createAreaLabel = (area) => {
  const canvas = document.createElement('canvas')
  canvas.width = 200
  canvas.height = 100
  const ctx = canvas.getContext('2d')
  if (!ctx) return ''

  // 绘制背景
  ctx.fillStyle = 'rgba(40, 44, 52, 0.8)'
  ctx.strokeStyle = '#007acc'
  ctx.lineWidth = 2
  roundRect(ctx, 0, 0, 180, 60, 8)
  ctx.fill()
  ctx.stroke()

  // 计算文本宽度以实现居中
  ctx.font = 'bold 16px Arial'
  const titleWidth = ctx.measureText('面积').width
  const titleX = (180 - titleWidth) / 2

  ctx.font = '14px Arial'
  const valueText = `${area.toFixed(2)} 平方米`
  const valueWidth = ctx.measureText(valueText).width
  const valueX = (180 - valueWidth) / 2

  // 绘制居中的文本
  ctx.fillStyle = 'white'
  ctx.font = 'bold 16px Arial'
  ctx.fillText('面积', titleX, 25)
  ctx.font = '14px Arial'
  ctx.fillText(valueText, valueX, 45)

  return canvas.toDataURL()
}

// 绘制圆角矩形
const roundRect = (ctx, x, y, width, height, radius) => {
  ctx.beginPath()
  ctx.moveTo(x + radius, y)
  ctx.lineTo(x + width - radius, y)
  ctx.quadraticCurveTo(x + width, y, x + width, y + radius)
  ctx.lineTo(x + width, y + height - radius)
  ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height)
  ctx.lineTo(x + radius, y + height)
  ctx.quadraticCurveTo(x, y + height, x, y + height - radius)
  ctx.lineTo(x, y + radius)
  ctx.quadraticCurveTo(x, y, x + radius, y)
  ctx.closePath()
}

// 创建高度标签
const createHeightLabel=(height)=>{
  const canvas = document.createElement('canvas')
  canvas.width = 200
  canvas.height = 100
  const ctx = canvas.getContext('2d')
  if (!ctx) return ''
  // 绘制背景
  ctx.fillStyle = 'rgba(40, 44, 52, 0.8)'
  ctx.strokeStyle = '#007acc'
  ctx.lineWidth = 2
  roundRect(ctx, 0, 0, 180, 60, 8)
  ctx.fill()
  ctx.stroke()
  // 计算文本宽度以实现居中
  ctx.font = 'bold 16px Arial'
  const titleWidth = ctx.measureText('高度').width
  const titleX = (180 - titleWidth) / 2

  ctx.font = '14px Arial'
  const valueText = `${height.toFixed(2)} 米`
  const valueWidth = ctx.measureText(valueText).width
  const valueX = (180 - valueWidth) / 2
  // 绘制居中的文本
  ctx.fillStyle = 'white'
  ctx.font = 'bold 16px Arial'
  ctx.fillText('高度', titleX, 25)
  ctx.font = '14px Arial'
  ctx.fillText(valueText, valueX, 45)
  return canvas.toDataURL()
}

// 清除测量结果
const clearMeasurements = () => {
  if (!viewer.value) return

  measureEntities.value.forEach(entity => {
    viewer.value?.entities.remove(entity)
  })
  measureEntities.value = []
  activeTool.value = ''
  removeHandler()
}

// 重置视角
const resetView = () => {
  if (!viewer.value) return

  viewer.value.camera.setView({
    destination: Cesium.Cartesian3.fromDegrees(116.391, 39.901, 15000.0)
  })
}

// 移除事件处理器
const removeHandler = () => {
  if (!handler) return

  handler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK)
  handler.removeInputAction(Cesium.ScreenSpaceEventType.MOUSE_MOVE)
  handler.removeInputAction(Cesium.ScreenSpaceEventType.RIGHT_CLICK)
}

onMounted(async () => {
  await initViewer()
})

onBeforeUnmount(() => {
  if (handler) {
    handler.destroy()
    handler = null
  }
  if (viewer.value) {
    viewer.value.destroy()
    viewer.value = null
  }
})
</script>

<style lang="scss" scoped>
.measure-demo {
  width: 100%;
  height: 100%;
  display: flex;
  position: relative;

  .cesium-container {
    flex: 1;
    height: 100%;
  }

  .control-panel {
    width: 300px;
    height: 100%;
    background-color: rgba(40, 44, 52, 0.9);
    padding: 20px;
    color: white;
    overflow-y: auto;
    z-index: 1000;
    h2 {
      margin-bottom: 20px;
      text-align: center;
    }

    .button-group {
      display: flex;
      flex-direction: column;
      gap: 10px;
      margin-bottom: 20px;

      button {
        padding: 10px;
        border: none;
        border-radius: 4px;
        background-color: #4a4f57;
        color: white;
        cursor: pointer;
        transition: all 0.3s ease;

        &:hover {
          background-color: #5a6069;
        }

        &.active {
          background-color: #007acc;
        }
      }
    }

    .instruction-panel {
      background-color: rgba(255, 255, 255, 0.1);
      padding: 15px;
      border-radius: 4px;

      h3 {
        margin-bottom: 10px;
      }

      p {
        margin-bottom: 8px;
      }

      ol {
        padding-left: 20px;
        
        li {
          margin-bottom: 5px;
        }
      }
    }
  }
}
</style>

<style>
.error-message {
  position: fixed;
  top: 20px;
  left: 50%;
  transform: translateX(-50%);
  background-color: rgba(255, 59, 48, 0.9);
  color: white;
  padding: 12px 24px;
  border-radius: 4px;
  font-size: 14px;
  z-index: 1000;
  animation: fadeInOut 3s ease-in-out;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}

@keyframes fadeInOut {
  0% {
    opacity: 0;
    transform: translate(-50%, -20px);
  }
  10% {
    opacity: 1;
    transform: translate(-50%, 0);
  }
  90% {
    opacity: 1;
    transform: translate(-50%, 0);
  }
  100% {
    opacity: 0;
    transform: translate(-50%, -20px);
  }
}
</style>

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值