📋 文章目錄
- 引言
- 功能概述
- 環境準備
- 核心實現步驟
- 地圖初始化
- 多邊形繪制
- 頂點編輯功能
- 顏色與透明度自定義
- 面積計算與顯示
- 常見問題解決方案
- 多邊形顏色顯示異常
- 面積標簽不可見
- 控制臺alpha類型錯誤
- 地圖交互無法恢復
- 完整代碼
- 總結與擴展
引言
Cesium作為一款強大的3D地理信息可視化庫,在WebGIS開發中有著廣泛的應用。本文將詳細介紹如何在Vue框架下基于Cesium實現交互式多邊形繪制與編輯功能,包括顏色自定義、透明度調整以及面積實時計算等實用功能,并解決開發過程中遇到的各種常見問題。
無論是GIS應用、智慧城市還是工程測量系統,多邊形繪制都是核心功能之一。通過本文的實現方案,你將能夠快速集成專業的多邊形編輯工具到自己的Cesium項目中,并掌握Cesium事件處理和狀態管理的最佳實踐。
功能概述
本文實現的多邊形工具具有以下特性:
? 交互式繪制:左鍵點擊添加頂點,右鍵結束繪制 ? 頂點編輯:拖拽頂點實時調整多邊形形狀 ? 樣式自定義:支持顏色選擇和透明度調整 ? 面積計算:自動計算并顯示多邊形面積(平方公里) ? 交互控制:繪制/編輯時禁用地圖默認交互,完成后恢復
環境準備
在開始之前,請確保你的項目中已安裝以下依賴:
# 安裝Cesium
npm install cesium --save# 安裝Turf.js用于面積計算
npm install @turf/turf --save
核心實現步驟
1. 地圖初始化
首先,我們需要初始化Cesium地圖實例。在Vue組件的mounted鉤子中完成地圖的加載:
import initMap from '@/config/initMap.js';
import { mapConfig } from '@/config/mapConfig';export default {data() {return {viewer: null,// 繪制狀態管理isDrawing: false,isEditing: false,currentPolygon: null,polygonPoints: [],dynamicPoints: [],vertexEntities: [],vertexHandlers: [], // 頂點事件處理器數組handler: null,polygonColor: '#0000FF', // 默認藍色polygonAlpha: 0.5, // 默認透明度areaLabel: null // 面積標簽實體};},mounted() {// 初始化地圖this.viewer = initMap(mapConfig.gaode.url3, false);// 初始化繪制處理器this.initDrawHandler();}
}
2. 多邊形繪制
多邊形繪制是通過監聽鼠標事件實現的,主要分為三個階段:
- 左鍵點擊:添加頂點并更新多邊形
- 鼠標移動:動態顯示多邊形輪廓
- 右鍵點擊:結束繪制并創建最終多邊形
核心代碼實現:
// 開始繪制多邊形
startDrawPolygon() {if (this.isEditing) this.stopEditPolygon();this.isDrawing = true;this.polygonPoints = [];this.dynamicPoints = [];this.disableMapInteraction();// 左鍵點擊添加頂點this.handler.setInputAction(this.handleLeftClick, Cesium.ScreenSpaceEventType.LEFT_CLICK);// 鼠標移動更新動態輪廓this.handler.setInputAction(this.handleMouseMove, Cesium.ScreenSpaceEventType.MOUSE_MOVE);// 右鍵結束繪制this.handler.setInputAction(this.handleRightClick, Cesium.ScreenSpaceEventType.RIGHT_CLICK);// 創建動態多邊形實體this.createDynamicPolygon();
},// 創建動態多邊形(繪制過程中的臨時多邊形)
createDynamicPolygon() {// 移除已存在的動態多邊形if (this.currentPolygon) {this.viewer.entities.remove(this.currentPolygon);}this.currentPolygon = this.viewer.entities.add({polygon: {hierarchy: new Cesium.CallbackProperty(() => {return new Cesium.PolygonHierarchy(this.dynamicPoints);}, false),material: Cesium.Color.RED.withAlpha(0.3),outline: true,outlineColor: Cesium.Color.RED}});
}
3. 頂點編輯功能
編輯功能允許用戶通過拖拽頂點來調整多邊形形狀,實現思路是為每個頂點創建可交互的點實體,并監聽其拖拽事件:
// 為每個頂點添加拖拽事件
addVertexDragHandler(vertexEntity, index) {const vertexHandler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas);// 將處理器存儲到數組,以便后續銷毀this.vertexHandlers.push(vertexHandler);vertexHandler.setInputAction((event) => {const pick = this.viewer.scene.pick(event.position);if (Cesium.defined(pick) && pick.id === vertexEntity) {// 開始拖拽const moveHandler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas);moveHandler.setInputAction((moveEvent) => {const newCartesian = this.getCartesianFromMouse(moveEvent.endPosition);if (newCartesian) {// 更新頂點位置this.polygonPoints[index] = newCartesian;vertexEntity.position.setValue(newCartesian);// 更新多邊形this.currentPolygon.polygon.hierarchy.setValue(new Cesium.PolygonHierarchy(this.polygonPoints));// 更新面積顯示this.calculateAndShowArea();}}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);// 結束拖拽vertexHandler.setInputAction(() => {moveHandler.destroy();}, Cesium.ScreenSpaceEventType.LEFT_UP);}}, Cesium.ScreenSpaceEventType.LEFT_DOWN);
}
4. 顏色與透明度自定義
通過添加顏色選擇器和透明度滑塊,允許用戶自定義多邊形樣式:
// 更新多邊形樣式
updatePolygonStyle() {if (!this.currentPolygon) return;try {const color = Cesium.Color.fromCssColorString(this.polygonColor).withAlpha(this.polygonAlpha);this.currentPolygon.polygon.material = color;} catch (e) {console.error('顏色解析錯誤:', e);// 使用默認顏色作為fallbackthis.currentPolygon.polygon.material = Cesium.Color.BLUE.withAlpha(0.5);}
}
5. 面積計算與顯示
使用Turf.js庫計算多邊形面積,并在多邊形中心顯示面積標簽:
// 計算并顯示多邊形面積
calculateAndShowArea() {try {if (this.polygonPoints.length < 3) return;// 將笛卡爾坐標轉換為經緯度const coordinates = this.polygonPoints.map(cartesian => {const cartographic = Cesium.Cartographic.fromCartesian(cartesian);return [Cesium.Math.toDegrees(cartographic.longitude),Cesium.Math.toDegrees(cartographic.latitude)];});// 閉合多邊形coordinates.push(coordinates[0]);// 使用Turf.js計算面積const polygon = turf.polygon([coordinates]);const areaSquareMeters = turf.area(polygon);const areaSquareKm = areaSquareMeters / 1000000;const areaText = `面積: ${areaSquareKm.toFixed(4)} 平方公里`;// 顯示面積標簽this.showAreaLabel(areaText);} catch (e) {console.error('面積計算錯誤:', e);this.showAreaLabel('面積計算失敗');}
},// 在多邊形中心顯示面積標簽
showAreaLabel(text) {// 移除舊標簽if (this.areaLabel) {this.viewer.entities.remove(this.areaLabel);}// 計算多邊形中心點const center = this.calculatePolygonCenter();// 創建新標簽this.areaLabel = this.viewer.entities.add({position: center,label: {text: text,font: '16px sans-serif',fillColor: Cesium.Color.YELLOW,backgroundColor: Cesium.Color.BLACK.withAlpha(0.7),padding: new Cesium.Cartesian2(12, 8),horizontalOrigin: Cesium.HorizontalOrigin.CENTER,verticalOrigin: Cesium.VerticalOrigin.CENTER,disableDepthTestDistance: Number.POSITIVE_INFINITY,outline: true,outlineColor: Cesium.Color.WHITE,outlineWidth: 1}});
}
常見問題解決方案
在開發過程中,我們可能會遇到以下問題:
1. 多邊形顏色顯示異常
問題:設置顏色后多邊形顯示為白色或不生效。
解決方案:
- 確保顏色值是有效的CSS顏色字符串
- 檢查透明度值是否為數字類型
- 添加錯誤處理和默認顏色 fallback
// 安全的顏色設置方法
try {const color = Cesium.Color.fromCssColorString(this.polygonColor).withAlpha(Number(this.polygonAlpha) || 0.5); // 確保alpha為數字this.currentPolygon.polygon.material = color;
} catch (e) {console.error('顏色解析錯誤:', e);// 使用默認顏色作為fallbackthis.currentPolygon.polygon.material = Cesium.Color.BLUE.withAlpha(0.5);
}
2. 面積標簽不可見
問題:面積標簽不顯示或被地形遮擋。
解決方案:
- 提升標簽高度,避免被地形遮擋
- 使用醒目顏色和背景提高可見性
- 設置
disableDepthTestDistance
確保標簽始終顯示在最前面
// 計算中心點時提升高度
calculatePolygonCenter() {// ... 經緯度計算邏輯 ...// 將中心點轉換回笛卡爾坐標,并提升高度return Cesium.Cartesian3.fromDegrees(centerLon, centerLat, 50); // 提升50米高度
}
3. 控制臺alpha類型錯誤
問題:Expected alpha to be typeof number, actual typeof was string
解決方案:
- 確保透明度值為數字類型
- 在Vue中使用
.number
修飾符或手動轉換類型
<!-- Vue模板中 -->
<input type="range" v-model.number="polygonAlpha" min="0" max="1" step="0.1">
4. 地圖交互無法恢復
問題:結束繪制或編輯后,地圖依然不能縮放或移動。
解決方案:
- 增強交互恢復方法,確保所有控制參數被正確設置
- 管理所有事件處理器的生命周期,確保完全銷毀
- 在所有退出路徑調用交互恢復方法
// 恢復地圖交互(增強版)
enableMapInteraction() {const controller = this.viewer.scene.screenSpaceCameraController;// 確保所有輸入被啟用controller.enableInputs = true;// 恢復所有相機控制controller.enableRotate = true;controller.enableZoom = true;controller.enableTranslate = true;controller.enableTilt = true;controller.enableLook = true;console.log("地圖交互已完全恢復");
},// 停止編輯多邊形(修復版)
stopEditPolygon() {this.isEditing = false;// 銷毀所有頂點拖拽事件處理器this.vertexHandlers.forEach(handler => {if (handler && !handler.isDestroyed()) {handler.destroy();}});this.vertexHandlers = []; // 清空處理器數組this.enableMapInteraction();
}
完整代碼
Vue組件代碼
<template><div id="cesiumContainer" style="width: 100%; height: 100vh"><div class="polygon-controls"><label>多邊形顏色:</label><input type="color" v-model="polygonColor" @input="updatePolygonStyle" /><label>透明度:</label><inputtype="range"min="0"max="1"step="0.1"v-model.number="polygonAlpha"@input="updatePolygonStyle"/></div><div class="">結束繪制</div></div>
</template><script>
import initMap from '@/config/initMap.js';
import { mapConfig } from '@/config/mapConfig';
// import * as Cesium from 'cesium';
import * as turf from '@turf/turf';export default {data() {return {viewer: null,// 繪制狀態管理isDrawing: false,isEditing: false,currentPolygon: null,polygonPoints: [],dynamicPoints: [],vertexEntities: [],handler: null,polygonColor: '#0000FF', // 默認藍色polygonAlpha: 0.5, // 默認透明度areaLabel: null, // 面積標簽實體vertexHandlers: [], // 新增:存儲頂點拖拽事件處理器};},mounted() {this.viewer = initMap(mapConfig.gaode.url3, false);this.initDrawHandler();},methods: {// 初始化繪制事件處理器initDrawHandler() {this.handler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas);this.startDrawPolygon();},// 開始繪制多邊形startDrawPolygon() {console.log('開始繪制多邊形');if (this.isEditing) this.stopEditPolygon();this.isDrawing = true;this.polygonPoints = [];this.dynamicPoints = [];this.disableMapInteraction();// 左鍵點擊添加頂點this.handler.setInputAction(this.handleLeftClick,Cesium.ScreenSpaceEventType.LEFT_CLICK);// 鼠標移動更新動態輪廓this.handler.setInputAction(this.handleMouseMove,Cesium.ScreenSpaceEventType.MOUSE_MOVE);// 右鍵結束繪制this.handler.setInputAction(this.handleRightClick,Cesium.ScreenSpaceEventType.RIGHT_CLICK);// 創建動態多邊形實體this.createDynamicPolygon();},// 處理左鍵點擊添加頂點handleLeftClick(event) {const cartesian = this.getCartesianFromMouse(event.position);if (!cartesian) return;this.polygonPoints.push(cartesian);this.dynamicPoints.push(cartesian);// 添加頂點標記this.addVertexMarker(cartesian);},// 處理鼠標移動更新輪廓handleMouseMove(event) {if (this.polygonPoints.length === 0 || !this.isDrawing) return;const cartesian = this.getCartesianFromMouse(event.endPosition);if (!cartesian) return;// 更新動態點if (this.dynamicPoints.length > this.polygonPoints.length) {this.dynamicPoints.pop();}this.dynamicPoints.push(cartesian);},// 處理右鍵結束繪制handleRightClick() {if (this.polygonPoints.length < 3) {alert('至少需要3個頂點才能形成多邊形');this.clearDrawing();return;}// 移除動態點this.dynamicPoints.pop();this.isDrawing = false;// 保存最終多邊形this.saveFinalPolygon();// 清除臨時事件this.handler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK);this.handler.removeInputAction(Cesium.ScreenSpaceEventType.MOUSE_MOVE);this.handler.removeInputAction(Cesium.ScreenSpaceEventType.RIGHT_CLICK);// 開啟編輯模式this.startEditPolygon();// 確保交互已恢復(雙重保險)setTimeout(() => {this.enableMapInteraction();}, 100);},// 開始編輯多邊形startEditPolygon() {this.isEditing = true;this.disableMapInteraction();// 為每個頂點添加拖拽事件this.vertexEntities.forEach((vertex, index) => {this.addVertexDragHandler(vertex, index);});},// 添加頂點拖拽事件addVertexDragHandler(vertexEntity, index) {const vertexHandler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas);// 將處理器存儲到數組,以便后續銷毀this.vertexHandlers.push(vertexHandler);vertexHandler.setInputAction((event) => {const pick = this.viewer.scene.pick(event.position);if (Cesium.defined(pick) && pick.id === vertexEntity) {// 開始拖拽const moveHandler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas);moveHandler.setInputAction((moveEvent) => {const newCartesian = this.getCartesianFromMouse(moveEvent.endPosition);if (newCartesian) {// 更新頂點位置this.polygonPoints[index] = newCartesian;vertexEntity.position.setValue(newCartesian);// 更新多邊形this.currentPolygon.polygon.hierarchy.setValue(new Cesium.PolygonHierarchy(this.polygonPoints));// 更新面積顯示this.calculateAndShowArea();}}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);// 結束拖拽vertexHandler.setInputAction(() => {moveHandler.destroy();}, Cesium.ScreenSpaceEventType.LEFT_UP);}}, Cesium.ScreenSpaceEventType.LEFT_DOWN);},// 停止編輯多邊形stopEditPolygon() {this.isEditing = false;// 銷毀所有頂點拖拽事件處理器this.vertexHandlers.forEach((handler) => {if (handler && !handler.isDestroyed()) {handler.destroy();}});this.vertexHandlers = []; // 清空處理器數組this.enableMapInteraction();},// 創建動態多邊形createDynamicPolygon() {// 移除已存在的動態多邊形if (this.currentPolygon) {this.viewer.entities.remove(this.currentPolygon);}this.currentPolygon = this.viewer.entities.add({polygon: {hierarchy: new Cesium.CallbackProperty(() => {return new Cesium.PolygonHierarchy(this.dynamicPoints);}, false),material: Cesium.Color.RED.withAlpha(0.3), // 繪制過程中使用紅色outline: true,outlineColor: Cesium.Color.RED,},});},// 保存最終多邊形(修復重復定義問題)saveFinalPolygon() {// 移除動態多邊形this.viewer.entities.remove(this.currentPolygon);try {// 驗證顏色和透明度console.log('當前顏色值:',this.polygonColor,'透明度:',this.polygonAlpha);const color = Cesium.Color.fromCssColorString(this.polygonColor).withAlpha(Number(this.polygonAlpha) || 0.5);// 創建最終多邊形,應用自定義顏色和透明度this.currentPolygon = this.viewer.entities.add({polygon: {hierarchy: new Cesium.PolygonHierarchy(this.polygonPoints),material: color,outline: true,outlineColor: Cesium.Color.BLACK,outlineWidth: 2,},});// 計算并顯示面積this.calculateAndShowArea();} catch (e) {console.error('創建多邊形失敗:', e);// 使用默認顏色作為fallbackthis.currentPolygon = this.viewer.entities.add({polygon: {hierarchy: new Cesium.PolygonHierarchy(this.polygonPoints),material: Cesium.Color.BLUE.withAlpha(0.5),outline: true,outlineColor: Cesium.Color.BLACK,},});}},// 添加頂點標記addVertexMarker(position) {const vertexEntity = this.viewer.entities.add({position: position,point: {pixelSize: 10,color: Cesium.Color.YELLOW,outlineColor: Cesium.Color.BLACK,outlineWidth: 2,disableDepthTestDistance: Number.POSITIVE_INFINITY,},});this.vertexEntities.push(vertexEntity);},// 從鼠標位置獲取笛卡爾坐標getCartesianFromMouse(position) {const ray = this.viewer.camera.getPickRay(position);if (!ray) return null;return this.viewer.scene.globe.pick(ray, this.viewer.scene);},// 清除繪制狀態clearDrawing() {this.isDrawing = false;this.polygonPoints = [];this.dynamicPoints = [];if (this.currentPolygon) {this.viewer.entities.remove(this.currentPolygon);}this.vertexEntities.forEach((vertex) =>this.viewer.entities.remove(vertex));this.vertexEntities = [];if (this.areaLabel) {this.viewer.entities.remove(this.areaLabel);this.areaLabel = null;}this.enableMapInteraction();},// 更新多邊形樣式updatePolygonStyle() {if (!this.currentPolygon) return;try {const color = Cesium.Color.fromCssColorString(this.polygonColor).withAlpha(Number(this.polygonAlpha) || 0.5);this.currentPolygon.polygon.material = color;console.log('樣式更新成功:', color);} catch (e) {console.error('顏色解析錯誤:', e);this.currentPolygon.polygon.material = Cesium.Color.BLUE.withAlpha(0.5);}},// 計算并顯示多邊形面積calculateAndShowArea() {try {if (this.polygonPoints.length < 3) return;// 將笛卡爾坐標轉換為經緯度const coordinates = this.polygonPoints.map((cartesian) => {const cartographic = Cesium.Cartographic.fromCartesian(cartesian);return [Cesium.Math.toDegrees(cartographic.longitude),Cesium.Math.toDegrees(cartographic.latitude),];});// 確保多邊形閉合if (coordinates.length > 0 &&!(coordinates[0][0] === coordinates[coordinates.length - 1][0] &&coordinates[0][1] === coordinates[coordinates.length - 1][1])) {coordinates.push([...coordinates[0]]);}// 使用Turf.js計算面積const polygon = turf.polygon([coordinates]);const areaSquareMeters = turf.area(polygon);const areaSquareKm = areaSquareMeters / 1000000;const areaText = `面積: ${areaSquareKm.toFixed(4)} 平方公里`;console.log('計算面積:', areaText);// 顯示面積標簽this.showAreaLabel(areaText);} catch (e) {console.error('面積計算錯誤:', e);this.showAreaLabel('面積計算失敗');}},// 在多邊形中心顯示面積標簽(優化版本)showAreaLabel(text) {// 移除舊標簽if (this.areaLabel) {this.viewer.entities.remove(this.areaLabel);}// 計算多邊形中心點(優化算法)const center = this.calculatePolygonCenter();// 創建新標簽,增強可見性this.areaLabel = this.viewer.entities.add({position: center,label: {text: text,font: '16px sans-serif',fillColor: Cesium.Color.YELLOW, // 使用醒目顏色backgroundColor: Cesium.Color.BLACK.withAlpha(0.8), // 增強對比度padding: new Cesium.Cartesian2(12, 8),horizontalOrigin: Cesium.HorizontalOrigin.CENTER,verticalOrigin: Cesium.VerticalOrigin.CENTER,disableDepthTestDistance: Number.POSITIVE_INFINITY, // 始終顯示在最前面pixelOffset: new Cesium.Cartesian2(0, 0),outline: true,outlineColor: Cesium.Color.WHITE,outlineWidth: 1,},});},// 計算多邊形中心點(更可靠的方法)calculatePolygonCenter() {if (this.polygonPoints.length === 0) return Cesium.Cartesian3.ZERO;// 計算經緯度平均值let totalLon = 0,totalLat = 0;const cartographics = this.polygonPoints.map((cartesian) =>Cesium.Cartographic.fromCartesian(cartesian));cartographics.forEach((cartographic) => {totalLon += Cesium.Math.toDegrees(cartographic.longitude);totalLat += Cesium.Math.toDegrees(cartographic.latitude);});const centerLon = totalLon / cartographics.length;const centerLat = totalLat / cartographics.length;// 將中心點轉換回笛卡爾坐標,并提升高度避免被地形遮擋return Cesium.Cartesian3.fromDegrees(centerLon, centerLat, 50);},// 禁用地圖交互disableMapInteraction() {const controller = this.viewer.scene.screenSpaceCameraController;controller.enableRotate = false;controller.enableZoom = false;controller.enableTranslate = false;controller.enableTilt = false;controller.enableLook = false;},// 恢復地圖交互enableMapInteraction() {const controller = this.viewer.scene.screenSpaceCameraController;// 確保所有輸入被啟用controller.enableInputs = true;// 恢復所有相機控制controller.enableRotate = true;controller.enableZoom = true;controller.enableTranslate = true;controller.enableTilt = true;controller.enableLook = true;// 重置鼠標事件處理if (this.handler) {this.handler.destroy();this.handler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas);}console.log('地圖交互已完全恢復');},},beforeDestroy() {if (this.viewer) {this.viewer.destroy();}if (this.handler) {this.handler.destroy();}// 移除控制面板const controlPanel = document.querySelector('.polygon-controls');if (controlPanel) {controlPanel.remove();}},
};
</script>
CSS樣式
<style lang="scss" scoped>
#cesiumContainer {width: 100%;height: 100vh;touch-action: none;.polygon-controls {position: absolute;top: 20px;right: 20px;z-index: 1000;background: white;padding: 15px;border-radius: 5px;box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);}.polygon-controls label {display: block;margin: 10px 0 5px;font-weight: bold;}.polygon-controls input {width: 100%;margin-bottom: 10px;}
}
</style>
總結與擴展
本文詳細介紹了如何基于Cesium和Vue實現交互式多邊形繪制與編輯功能,包括核心功能實現、樣式自定義和面積計算等關鍵技術點。特別解決了開發過程中常見的顏色顯示異常、標簽不可見、類型錯誤和交互無法恢復等問題。
通過事件監聽、動態屬性更新和地理空間計算,我們構建了一個功能完善的多邊形編輯工具。重點強調了事件處理器生命周期管理和狀態控制的最佳實踐,這些經驗對于開發復雜Cesium交互功能具有普遍參考價值。
功能擴展方向
- 添加刪除功能:允許用戶刪除多邊形或單個頂點
- 支持多個多邊形:管理多個多邊形圖層
- 導入導出:支持GeoJSON格式導入導出
- 測量工具:添加距離測量、角度測量等功能
- 樣式庫:預設多種多邊形樣式供選擇
- 撤銷/重做:實現操作歷史記錄功能
希望本文能幫助你快速掌握Cesium多邊形繪制技術,如果有任何問題或建議,歡迎在評論區留言討論!
原創不易,轉載請注明出處
如果本文對你有幫助,別忘了點贊和關注哦!