Cesium@1.138中实现标尺绘制功能

📅 2026/7/9 9:02:02 👁️ 阅读次数 📝 编程学习
Cesium@1.138中实现标尺绘制功能
/** * Cesium 标尺绘制功能类 * @param {Object} Cesium - 外部传入的 Cesium 命名空间对象 * @param {Cesium.Viewer} viewer - 外部传入的 Cesium Viewer 实例 */ export default class CesiumMeasureTool { constructor(Cesium, viewer) { if (!Cesium || !viewer) { throw new Error('CesiumMeasureTool 初始化失败:必须传入 Cesium 对象和 viewer 实例'); } this.Cesium = Cesium; this.viewer = viewer; this.handler = null; this.entities = []; this.activeShape = null; this.activeShapePoints = []; this.isDrawing = false; this.measureMode = null; } // ================= 核心公共方法 ================= /** 多点测距 */ startDistanceMeasure() { this.clearAll(); this.measureMode = 'distance'; this._startDrawing(); } /** 面积测量 */ startAreaMeasure() { this.clearAll(); this.measureMode = 'area'; this._startDrawing(); } /** * 清除所有绘制 (对外暴露的唯一清理接口) * 负责销毁事件、清理实体、重置状态 */ clearAll() { // 1. 首先移除动态预览实体,避免 CallbackProperty 引发错误 if (this.activeShape) { this.viewer.entities.remove(this.activeShape); this.activeShape = null; } // 2. 销毁事件监听 if (this.handler) { this.handler.destroy(); this.handler = null; } // 3. 使用倒序删除法清理所有追踪的 Entity if (this.entities && this.entities.length > 0) { for (let i = this.entities.length - 1; i >= 0; i--) { const entity = this.entities[i]; if (entity) { this.viewer.entities.remove(entity); } } this.entities = []; } // ✅ 核心修复:强制请求一次渲染,立即更新画面 // 解决因 Cesium 渲染缓存导致的“删除后不立即消失”的问题 this.viewer.scene.requestRender(); // 4. 重置内部状态 this.isDrawing = false; this.measureMode = null; this.activeShapePoints = []; this.viewer.container.style.cursor = ''; } // ================= 内部核心逻辑 ================= /** 初始化绘制状态与事件监听 */ _startDrawing() { this.isDrawing = true; this.activeShapePoints = []; this.viewer.container.style.cursor = 'crosshair'; this.handler = new this.Cesium.ScreenSpaceEventHandler(this.viewer.canvas); this.handler.setInputAction((event) => this._onLeftClick(event), this.Cesium.ScreenSpaceEventType.LEFT_CLICK); this.handler.setInputAction((event) => this._onMouseMove(event), this.Cesium.ScreenSpaceEventType.MOUSE_MOVE); this.handler.setInputAction(() => this._finishDrawing(), this.Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK); this.handler.setInputAction(() => this._finishDrawing(), this.Cesium.ScreenSpaceEventType.RIGHT_CLICK); } /** 左键点击处理 */ _onLeftClick(event) { const cartesian = this.viewer.camera.pickEllipsoid(event.position, this.viewer.scene.globe.ellipsoid); if (!cartesian) return; this.activeShapePoints.push(cartesian); const pointEntity = this.viewer.entities.add({ position: cartesian, point: { pixelSize: 8, color: this.Cesium.Color.YELLOW, outlineColor: this.Cesium.Color.BLACK, outlineWidth: 2 }, name: 'measure_vertex' }); this.entities.push(pointEntity); this._updateActiveShape(); } /** 鼠标移动处理 */ _onMouseMove(event) { if (!this.isDrawing || this.activeShapePoints.length === 0) return; const cartesian = this.viewer.camera.pickEllipsoid(event.endPosition, this.viewer.scene.globe.ellipsoid); if (!cartesian) return; if (this.activeShapePoints.length === 1) { this.activeShapePoints.push(cartesian); } else { this.activeShapePoints[this.activeShapePoints.length - 1] = cartesian; } this._updateActiveShape(); } /** 结束绘制 */ _finishDrawing() { if (!this.isDrawing) return; // 1. 移除动态预览图形 if (this.activeShape) { this.viewer.entities.remove(this.activeShape); this.activeShape = null; } // 2. 检查点数是否足够 if (this.measureMode === 'distance' && this.activeShapePoints.length < 2) { this.clearAll(); return; } if (this.measureMode === 'area' && this.activeShapePoints.length < 3) { this.clearAll(); return; } // 3. 绘制最终图形 this._drawFinalShape(); // 4. 重置绘制状态,准备下一次绘制 this.isDrawing = false; this.activeShapePoints = []; this.viewer.container.style.cursor = 'grab'; } // ================= 图形渲染与计算 ================= /** 动态更新绘制中的预览图形 */ _updateActiveShape() { if (this.activeShape) { this.viewer.entities.remove(this.activeShape); } if (this.measureMode === 'distance' && this.activeShapePoints.length >= 2) { this.activeShape = this.viewer.entities.add({ polyline: { positions: new this.Cesium.CallbackProperty(() => [...this.activeShapePoints], false), width: 2, material: new this.Cesium.PolylineDashMaterialProperty({ color: this.Cesium.Color.CYAN }), clampToGround: true }, name: 'measure_preview' }); } else if (this.measureMode === 'area' && this.activeShapePoints.length >= 3) { this.activeShape = this.viewer.entities.add({ polygon: { hierarchy: new this.Cesium.CallbackProperty(() => new this.Cesium.PolygonHierarchy(this.activeShapePoints), false), material: this.Cesium.Color.CYAN.withAlpha(0.1), outline: false, clampToGround: true }, polyline: { positions: new this.Cesium.CallbackProperty(() => { const pts = [...this.activeShapePoints]; if (pts.length > 0) pts.push(pts[0]); return pts; }, false), width: 2, material: new this.Cesium.PolylineDashMaterialProperty({ color: this.Cesium.Color.CYAN }), clampToGround: true }, name: 'measure_preview' }); } } /** 绘制最终保留的图形 */ _drawFinalShape() { const positions = [...this.activeShapePoints]; if (this.measureMode === 'distance') { const finalLine = this.viewer.entities.add({ polyline: { positions: positions, width: 2, material: new this.Cesium.PolylineDashMaterialProperty({ color: this.Cesium.Color.CYAN }), clampToGround: true }, name: 'measure_final_line' }); this.entities.push(finalLine); let totalDistance = 0; for (let i = 0; i < positions.length - 1; i++) { totalDistance += this.Cesium.Cartesian3.distance(positions[i], positions[i + 1]); } this._showDistanceTooltip(positions[positions.length - 1], totalDistance); } else if (this.measureMode === 'area') { const finalPolygon = this.viewer.entities.add({ polygon: { hierarchy: new this.Cesium.PolygonHierarchy(positions), material: this.Cesium.Color.CYAN.withAlpha(0.1), outline: false, clampToGround: true }, polyline: { positions: [...positions, positions[0]], width: 2, material: new this.Cesium.PolylineDashMaterialProperty({ color: this.Cesium.Color.CYAN }), clampToGround: true }, name: 'measure_final_area' }); this.entities.push(finalPolygon); const area = this._calculatePolygonArea(positions); this._showAreaLabel(positions, area); } } /** 计算多边形面积 */ _calculatePolygonArea(positions) { const geodesic = new this.Cesium.EllipsoidGeodesic(); let area = 0; for (let i = 0; i < positions.length; i++) { const nextIndex = (i + 1) % positions.length; const p1 = this.Cesium.Cartographic.fromCartesian(positions[i]); const p2 = this.Cesium.Cartographic.fromCartesian(positions[nextIndex]); geodesic.setEndPoints(p1, p2); area += (p2.longitude - p1.longitude) * (2 + Math.sin(p1.latitude) + Math.sin(p2.latitude)); } area = Math.abs(area * this.Cesium.Ellipsoid.WGS84.maximumRadius * this.Cesium.Ellipsoid.WGS84.maximumRadius / 2); return area; } // ================= UI 提示框处理 ================= _showDistanceTooltip(cartesian, distance) { const text = distance > 1000 ? `${(distance / 1000).toFixed(2)} km` : `${distance.toFixed(2)} m`; const existingLabel = this.entities.find(e => e.name === 'measure_tooltip_label'); if (existingLabel) { this.viewer.entities.remove(existingLabel); } const svgBg = this._createTooltipSVG(); const labelEntity = this.viewer.entities.add({ position: cartesian, label: { text: text, font: '16px sans-serif', fillColor: this.Cesium.Color.WHITE, showBackground: true, backgroundColor: new this.Cesium.Color(0, 0, 0, 1.0), backgroundPadding: new this.Cesium.Cartesian2(10, 10), backgroundImage: svgBg, verticalOrigin: this.Cesium.VerticalOrigin.BOTTOM, horizontalOrigin: this.Cesium.HorizontalOrigin.CENTER, pixelOffset: new this.Cesium.Cartesian2(0, -10), disableDepthTestDistance: Number.POSITIVE_INFINITY, }, name: 'measure_tooltip_label' }); this.entities.push(labelEntity); } _createTooltipSVG() { const width = 100; const height = 30; const arrowHeight = 6; const arrowWidth = 12; const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('width', width); svg.setAttribute('height', height + arrowHeight); svg.setAttribute('viewBox', `0 0 ${width} ${height + arrowHeight}`); const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); const d = ` M 0,0 H ${width} V ${height} L ${width/2 + arrowWidth/2},${height + arrowHeight} L ${width/2 - arrowWidth/2},${height + arrowHeight} V ${height} H 0 Z `; path.setAttribute('d', d); path.setAttribute('fill', 'rgba(0, 0, 0, 0.8)'); svg.appendChild(path); const serializer = new XMLSerializer(); const svgString = serializer.serializeToString(svg); const svgBlob = new Blob([svgString], {type: 'image/svg+xml;charset=utf-8'}); return URL.createObjectURL(svgBlob); } _showAreaLabel(positions, area) { const center = new this.Cesium.Cartesian3(0, 0, 0); positions.forEach(p => this.Cesium.Cartesian3.add(p, center, center)); this.Cesium.Cartesian3.divideByScalar(center, positions.length, center); let text = ''; if (area >= 1000000) text = `${(area / 1000000).toFixed(2)} km²`; else if (area >= 10000) text = `${(area / 10000).toFixed(2)} 公顷`; else text = `${area.toFixed(2)} m²`; const labelEntity = this.viewer.entities.add({ position: center, label: { text: text, font: '16px sans-serif', fillColor: this.Cesium.Color.YELLOW, outlineColor: this.Cesium.Color.BLACK, outlineWidth: 2, style: this.Cesium.LabelStyle.FILL_AND_OUTLINE, verticalOrigin: this.Cesium.VerticalOrigin.CENTER, horizontalOrigin: this.Cesium.HorizontalOrigin.CENTER, disableDepthTestDistance: Number.POSITIVE_INFINITY }, name: 'measure_area_label' }); this.entities.push(labelEntity); } }

使用

<template> <div ref="cesiumContainer" class="cesiumContainer"></div> <button id="addBtn">添加</button> <button id="editBtn">清除</button> </template> <script setup lang="ts"> import { ref, onMounted, onUnmounted, nextTick } from "vue"; import CesiumManager from "../utils/cesium-manager"; import type { CesiumInstance, CesiumViewer } from "../utils/cesium-manager"; import CesiumMeasureTool from "../utils/cesium-draw"; const cesiumContainer = ref<HTMLElement | null>(null); let Cesium: CesiumInstance | null = null; let viewer: CesiumViewer | null = null; const measureTool = ref<CesiumMeasureTool | null>(null); const cesiumManager = new CesiumManager(); onMounted(async () => { document.oncontextmenu = () => false; await nextTick(); if (!cesiumContainer.value) return; const { viewer: _viewer, Cesium: _Cesium } = await cesiumManager.initViewer( cesiumContainer.value, { targetFrameRate: 25 } ); viewer = _viewer; Cesium = _Cesium; // ✅ 仅初始化工具,不要在这里调用 startAreaMeasure() measureTool.value = new CesiumMeasureTool(Cesium, viewer); // 测试用:你可以把下面这行绑定到某个按钮上,而不是在 onMounted 里自动执行 // measureTool.value.startAreaMeasure(); const addBtn = document.getElementById("addBtn"); if (addBtn) { addBtn.addEventListener("click", () => { if (measureTool.value) { measureTool.value.startDistanceMeasure(); } }); } const editBtn = document.getElementById("editBtn"); if (editBtn) { editBtn.addEventListener("click", () => { console.log("清除按钮被点击"); if (measureTool.value) { measureTool.value.clearAll(); console.log("清除方法已执行"); } }); } else { console.error("未找到 ID 为 'editBtn' 的按钮元素"); } }); onUnmounted(() => { // 组件销毁时彻底清理 if (measureTool.value) { measureTool.value.clearAll(); } const editBtn = document.getElementById("editBtn"); if (editBtn) { editBtn.replaceWith(editBtn.cloneNode(true)); } }); </script> <style scoped> .cesiumContainer { width: 100vw; height: 100vh; margin: 0; padding: 0; position: absolute; overflow: hidden; z-index: 1; } #addBtn { position: fixed; top: 30px; left: 100px; z-index: 100000; cursor: pointer; } #editBtn { position: fixed; top: 30px; left: 30px; z-index: 100000; cursor: pointer; } </style>