直接放到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>