在 Cesium 地圖開發中,實現點標記的拖拽交互并實時顯示坐標信息是一個常見的需求。本文將詳細介紹如何在 Vue 框架中使用 Cesium 的 Primitive 方式創建點標記,并實現拖拽功能及坐標提示框跟隨效果。
先看效果圖
功能實現概述
我們將實現的功能包括:
- 使用 Primitive 方式創建可交互的點標記
- 實現點標記的拖拽功能(點擊選中、跟隨鼠標移動、釋放確定位置)
- 拖拽過程中實時顯示坐標信息提示框
- 拖拽時禁用地圖默認交互,提升操作體驗
核心技術點解析
1. 創建可拖拽點標記
使用PointPrimitiveCollection
創建點集合,添加點標記并設置基本樣式:
createPoint_primitives() {// 創建點集合const pointCollection = new Cesium.PointPrimitiveCollection();// 添加點this.draggablePoint = pointCollection.add({position: Cesium.Cartesian3.fromDegrees(116.4074, 39.9042, 0), // 初始位置(北京坐標)color: Cesium.Color.RED, // 點顏色pixelSize: 30, // 點大小outlineColor: Cesium.Color.WHITE, // 輪廓顏色outlineWidth: 2, // 輪廓寬度id: 'draggable-point', // 唯一標識});// 將點集合添加到場景圖元中this.viewer.scene.primitives.add(pointCollection);// 設置點的拖動功能this.setupPointDragging();
}
2. 實現拖拽交互邏輯
拖拽功能主要通過監聽鼠標的三個事件實現:左鍵按下、鼠標移動和左鍵釋放。
左鍵按下事件
// 左鍵按下事件 - 開始拖動
handler.setInputAction((click) => {// 拾取鼠標點擊位置的對象const pickedObject = this.viewer.scene.pick(click.position);// 檢查是否拾取到了我們創建的點if (Cesium.defined(pickedObject) && // 確保拾取對象存在pickedObject.primitive === this.draggablePoint // 確認是目標點) {isDragging = true;currentPoint = pickedObject.primitive;currentPoint.color = Cesium.Color.BLUE; // 改變顏色表示選中狀態// 顯示坐標提示框并更新位置this.updateTooltip(currentPoint.position);this.showCoordinates = true;this.disableMapInteraction(); // 禁用地圖默認交互}
}, Cesium.ScreenSpaceEventType.LEFT_DOWN);
這里的Cesium.defined(pickedObject)
是 Cesium 提供的工具函數,用于檢查一個值是否 "已定義且非空",避免后續操作出現空指針錯誤。
鼠標移動事件
在鼠標移動時,實時更新點的位置和坐標提示框:
// 鼠標移動事件 - 更新點位置
handler.setInputAction((movement) => {if (!isDragging || !currentPoint) return;// 將鼠標位置轉換為地理坐標const ray = this.viewer.camera.getPickRay(movement.endPosition);const position = this.viewer.scene.globe.pick(ray, this.viewer.scene);if (Cesium.defined(position)) {// 更新點的位置currentPoint.position = position;// 移動時實時更新提示框位置this.updateTooltip(position);}
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
左鍵釋放事件
拖拽結束時,恢復點的樣式并保存最終位置:
// 左鍵釋放事件 - 結束拖動
handler.setInputAction(() => {if (isDragging && currentPoint) {// 恢復點的原始顏色currentPoint.color = Cesium.Color.RED;// 獲取最終位置的經緯度const cartographic = Cesium.Cartographic.fromCartesian(currentPoint.position);const longitude = Cesium.Math.toDegrees(cartographic.longitude);const latitude = Cesium.Math.toDegrees(cartographic.latitude);console.log(`點位置已更新至: 經度 ${longitude.toFixed(4)}, 緯度 ${latitude.toFixed(4)}`);this.savePointPosition(longitude, latitude);}// 恢復地圖的默認鼠標交互this.enableMapInteraction();isDragging = false;currentPoint = null;
}, Cesium.ScreenSpaceEventType.LEFT_UP);
3. 坐標提示框跟隨功能
實現坐標提示框跟隨點移動的核心是updateTooltip
方法,該方法完成兩件事:將三維坐標轉換為經緯度,以及將三維坐標轉換為屏幕坐標用于定位提示框。
updateTooltip(position) {// 1. 計算經緯度信息const cartographic = Cesium.Cartographic.fromCartesian(position);this.coordinate = {lng: Cesium.Math.toDegrees(cartographic.longitude),lat: Cesium.Math.toDegrees(cartographic.latitude),};// 2. 計算屏幕坐標(兼容不同Cesium版本)const screenPosition = Cesium.SceneTransforms.wgs84ToWindowCoordinates? Cesium.SceneTransforms.wgs84ToWindowCoordinates(this.viewer.scene,position): Cesium.SceneTransforms.worldToWindowCoordinates(this.viewer.scene,position);// 3. 設置提示框位置(偏移20px避免遮擋點)if (screenPosition) {this.tooltipPosition = {x: screenPosition.x + 20,y: screenPosition.y - 20,};}
}
4. 地圖交互控制
為了提升拖拽體驗,在拖拽過程中需要禁用地圖的默認交互,拖拽結束后再恢復:
// 禁用地圖交互的方法
disableMapInteraction() {// 禁用鼠標左鍵拖動地圖this.viewer.scene.screenSpaceCameraController.enableRotate = false;// 禁用鼠標右鍵縮放this.viewer.scene.screenSpaceCameraController.enableZoom = false;// 禁用中鍵平移this.viewer.scene.screenSpaceCameraController.enableTranslate = false;// 禁用傾斜this.viewer.scene.screenSpaceCameraController.enableTilt = false;// 禁用旋轉this.viewer.scene.screenSpaceCameraController.enableLook = false;
}// 恢復地圖交互的方法
enableMapInteraction() {// 恢復所有鼠標交互this.viewer.scene.screenSpaceCameraController.enableRotate = true;this.viewer.scene.screenSpaceCameraController.enableZoom = true;this.viewer.scene.screenSpaceCameraController.enableTranslate = true;this.viewer.scene.screenSpaceCameraController.enableTilt = true;this.viewer.scene.screenSpaceCameraController.enableLook = true;
}
完整代碼實現
下面是完整的 Vue 組件代碼,包含了上述所有功能:
高德地圖三個地址
export const mapConfig = {gaode: {url1: 'http://webst02.is.autonavi.com/appmaptile?x={x}&y={y}&z={z}&lang=zh_cn&size=1&scale=1&style=8', //'高德路網中文注記'url2: 'https://webst02.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}', //高德影像url3: 'http://webrd02.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}', //高德矢量},
};
?封裝初始化地圖實例,組件直接引用
/*** 初始化地圖實例* @param {string} url - 地圖瓦片服務的URL模板* @param {boolean} is3d - 是否啟用3D地圖模式*/
export default function initMap(url, is3d) {// 初始化地圖// 創建一個Cesium Viewer實例,綁定到ID為'cesiumContainer'的DOM元素const viewer = new Cesium.Viewer('cesiumContainer', {animation: false, // 隱藏動畫控件baseLayerPicker: false, // 隱藏基礎圖層選擇器fullscreenButton: false, // 隱藏全屏按鈕geocoder: false, // 隱藏地理編碼搜索框homeButton: false, // 隱藏主頁按鈕infoBox: false, // 隱藏信息框sceneModePicker: false, // 隱藏場景模式選擇器scene3DOnly: is3d ? true : false, // 是否啟用3D-only模式優化性能,若is3d為true則啟用sceneMode: is3d ? Cesium.SceneMode.SCENE3D : Cesium.SceneMode.SCENE2D, // 場景模式,is3d為true時使用3D場景,否則使用2D場景selectionIndicator: false, // 隱藏選擇指示器timeline: false, // 隱藏時間軸navigationHelpButton: false, // 隱藏導航幫助按鈕navigationInstructionsInitiallyVisible: false, // 初始時不顯示導航說明shouldAnimate: true, // 啟用場景動畫projection: new Cesium.WebMercatorProjection(), // 地圖投影,使用Web墨卡托投影});// 移除默認影像圖層viewer.scene.imageryLayers.remove(viewer.scene.imageryLayers.get(0));// 去除版權信息viewer._cesiumWidget._creditContainer.style.display = 'none';// 向地圖的影像圖層添加一個自定義的瓦片影像服務viewer.imageryLayers.addImageryProvider(new Cesium.UrlTemplateImageryProvider({url: url, // 瓦片服務的URL模板subdomains: ['0', '1', '2', '3'], // 子域名列表maximumLevel: 18, // 最大縮放級別}));// 初始定位viewer.camera.setView({destination: Cesium.Cartesian3.fromDegrees(118.006, 39.7128, 1500000), // (lng, lat, height)});return viewer;
}
地圖組件代碼
<template><div id="cesiumContainer" style="width: 100%; height: 100vh"><!-- 坐標信息提示框 --><divv-if="showCoordinates"class="coordinate-tooltip":style="{ left: tooltipPosition.x + 'px', top: tooltipPosition.y + 'px' }">經度: {{ coordinate.lng.toFixed(6) }}<br />緯度: {{ coordinate.lat.toFixed(6) }}</div></div>
</template><script>
import initMap from '@/config/initMap.js';
import { mapConfig } from '@/config/mapConfig';
export default {data() {return {viewer: null,showCoordinates: false,coordinate: { lng: 0, lat: 0 },tooltipPosition: { x: 0, y: 0 },};},mounted() {this.viewer = initMap(mapConfig.gaode.url3, false);this.$nextTick(async () => {await this.createPoint_primitives(); // primitives方式創建});},methods: {// 創建點標記createPoint_primitives() {// 創建點集合const pointCollection = new Cesium.PointPrimitiveCollection();// 添加點this.draggablePoint = pointCollection.add({position: Cesium.Cartesian3.fromDegrees(116.4074, 39.9042, 0),color: Cesium.Color.RED,pixelSize: 30,outlineColor: Cesium.Color.WHITE,outlineWidth: 2,id: 'draggable-point',});// 將點集合添加到場景圖元中this.viewer.scene.primitives.add(pointCollection);// 設置點的拖動功能this.setupPointDragging();},setupPointDragging() {const handler = new Cesium.ScreenSpaceEventHandler(this.viewer.canvas);let isDragging = false;let currentPoint = null;// 左鍵按下事件 - 開始拖動handler.setInputAction((click) => {const pickedObject = this.viewer.scene.pick(click.position); // 拾取鼠標點擊位置的對象// 檢查是否拾取到了目標點if (Cesium.defined(pickedObject) && // 確保拾取對象存在pickedObject.primitive === this.draggablePoint // 確認是我們創建的點) {isDragging = true;currentPoint = pickedObject.primitive;currentPoint.color = Cesium.Color.BLUE; // 改變點的外觀以指示正在拖動// 初始顯示提示框并更新位置this.updateTooltip(currentPoint.position);this.showCoordinates = true;this.disableMapInteraction(); // 禁用地圖的默認鼠標交互}}, Cesium.ScreenSpaceEventType.LEFT_DOWN);// 鼠標移動事件 - 更新點位置handler.setInputAction((movement) => {if (!isDragging || !currentPoint) return;// 將鼠標位置轉換為地理坐標const ray = this.viewer.camera.getPickRay(movement.endPosition);const position = this.viewer.scene.globe.pick(ray, this.viewer.scene);if (Cesium.defined(position)) {// 更新點的位置currentPoint.position = position;// 移動時實時更新提示框位置this.updateTooltip(position);}}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);// 左鍵釋放事件 - 結束拖動handler.setInputAction(() => {if (isDragging && currentPoint) {// 恢復點的原始顏色currentPoint.color = Cesium.Color.RED;// 獲取最終位置的經緯度const cartographic = Cesium.Cartographic.fromCartesian(currentPoint.position);const longitude = Cesium.Math.toDegrees(cartographic.longitude);const latitude = Cesium.Math.toDegrees(cartographic.latitude);console.log(`點位置已更新至: 經度 ${longitude.toFixed(4)}, 緯度 ${latitude.toFixed(4)}`);this.savePointPosition(longitude, latitude);}// 恢復地圖的默認鼠標交互this.enableMapInteraction();isDragging = false;currentPoint = null;}, Cesium.ScreenSpaceEventType.LEFT_UP);},// 更新提示框位置和坐標信息updateTooltip(position) {// 1. 計算經緯度信息const cartographic = Cesium.Cartographic.fromCartesian(position);this.coordinate = {lng: Cesium.Math.toDegrees(cartographic.longitude),lat: Cesium.Math.toDegrees(cartographic.latitude),};// 2. 計算屏幕坐標(兼容不同Cesium版本)const screenPosition = Cesium.SceneTransforms.wgs84ToWindowCoordinates? Cesium.SceneTransforms.wgs84ToWindowCoordinates(this.viewer.scene,position): Cesium.SceneTransforms.worldToWindowCoordinates(this.viewer.scene,position);// 3. 設置提示框位置(偏移20px避免遮擋點)if (screenPosition) {this.tooltipPosition = {x: screenPosition.x + 20,y: screenPosition.y - 20,};}},// 禁用地圖交互的方法disableMapInteraction() {// 禁用鼠標左鍵拖動地圖this.viewer.scene.screenSpaceCameraController.enableRotate = false;// 禁用鼠標右鍵縮放this.viewer.scene.screenSpaceCameraController.enableZoom = false;// 禁用中鍵平移this.viewer.scene.screenSpaceCameraController.enableTranslate = false;// 禁用傾斜this.viewer.scene.screenSpaceCameraController.enableTilt = false;// 禁用旋轉this.viewer.scene.screenSpaceCameraController.enableLook = false;},// 恢復地圖交互的方法enableMapInteraction() {// 恢復所有鼠標交互this.viewer.scene.screenSpaceCameraController.enableRotate = true;this.viewer.scene.screenSpaceCameraController.enableZoom = true;this.viewer.scene.screenSpaceCameraController.enableTranslate = true;this.viewer.scene.screenSpaceCameraController.enableTilt = true;this.viewer.scene.screenSpaceCameraController.enableLook = true;},// 保存點位置的方法(可根據實際需求實現)savePointPosition(longitude, latitude) {// 示例:可以將位置發送到服務器或保存到本地存儲console.log(`保存點位置: 經度 ${longitude}, 緯度 ${latitude}`);// 實際應用中可能會調用API// fetch('/api/save-point', {// method: 'POST',// body: JSON.stringify({ longitude, latitude }),// });},},beforeDestroy() {// 組件銷毀時釋放資源if (this.viewer) {this.viewer.destroy();}},
};
</script><style lang="scss" scoped>
#cesiumContainer {width: 100%;height: 100vh;touch-action: none; /* 優化移動端交互 */position: relative; /* 確保提示框定位正確 */overflow: hidden;
}
.coordinate-tooltip {position: absolute;background-color: rgba(0, 0, 0, 0.7);color: white;padding: 8px 12px;border-radius: 4px;font-size: 12px;pointer-events: none; /* 確保鼠標事件能穿透提示框 */z-index: 1000; /* 確保提示框顯示在最上層 */animation: fadeIn 0.3s ease-out; /* 淡入動畫 */
}@keyframes fadeIn {from {opacity: 0;transform: translateY(10px);}to {opacity: 1;transform: translateY(0);}
}
</style>
總結
本文詳細介紹了在 Vue 中使用 Cesium 實現可拖拽點標記及坐標實時顯示的功能。通過 Primitive 方式創建點標記,結合 Cesium 的事件處理機制實現拖拽交互,并通過坐標轉換實現提示框跟隨效果。
這種實現方式具有較好的性能表現,適用于需要在地圖上進行點位調整和標記的場景。你可以根據實際需求擴展功能,如添加拖拽范圍限制、保存歷史位置、添加更多樣式的視覺反饋等。