Vue 中使用 Cesium 实现可拖拽点标记及坐标实时显示功能
在 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 的事件处理机制实现拖拽交互,并通过坐标转换实现提示框跟随效果。
这种实现方式具有较好的性能表现,适用于需要在地图上进行点位调整和标记的场景。你可以根据实际需求扩展功能,如添加拖拽范围限制、保存历史位置、添加更多样式的视觉反馈等。