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

发布时间:2026/7/9 8:47:13
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/svgxml;charsetutf-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 refcesiumContainer classcesiumContainer/div button idaddBtn添加/button button ideditBtn清除/button /template script setup langts 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 refHTMLElement | null(null); let Cesium: CesiumInstance | null null; let viewer: CesiumViewer | null null; const measureTool refCesiumMeasureTool | 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

相关新闻