You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
3234 lines
124 KiB
3234 lines
124 KiB
<template>
|
|
<div class="cesium-container">
|
|
<div id="cesiumViewer" ref="cesiumViewer"></div>
|
|
|
|
<drawing-toolbar
|
|
:draw-dom-click="drawDomClick"
|
|
:drawing-mode="drawingMode"
|
|
:has-entities="allEntities.length > 0"
|
|
:tool-mode="toolMode"
|
|
@toggle-drawing="toggleDrawing"
|
|
@clear-all="clearAll"
|
|
@export-data="exportData"
|
|
@import-data="importData"
|
|
@locate="handleLocate"
|
|
/>
|
|
<measurement-panel
|
|
v-if="measurementResult"
|
|
:result="measurementResult"
|
|
@close="measurementResult = null"
|
|
/>
|
|
<hover-tooltip
|
|
v-if="hoverTooltip.visible"
|
|
:visible="hoverTooltip.visible"
|
|
:content="hoverTooltip.content"
|
|
:style="{
|
|
left: hoverTooltip.position.x + 'px',
|
|
top: hoverTooltip.position.y + 'px'
|
|
}"
|
|
/>
|
|
<context-menu
|
|
v-if="contextMenu.visible"
|
|
:visible="contextMenu.visible"
|
|
:position="contextMenu.position"
|
|
:entity-data="contextMenu.entityData"
|
|
@delete="deleteEntityFromContextMenu"
|
|
@update-property="updateEntityProperty"
|
|
/>
|
|
|
|
<!-- 定位弹窗 -->
|
|
<locate-dialog
|
|
:visible="locateDialogVisible"
|
|
@update:visible="locateDialogVisible = $event"
|
|
@confirm="handleLocateConfirm"
|
|
@cancel="handleLocateCancel"
|
|
/>
|
|
|
|
<!-- 地图右下角:比例尺 + 经纬度 -->
|
|
<div class="map-info-panel">
|
|
<div class="scale-bar">
|
|
<span class="scale-bar-text">{{ scaleBarText }}</span>
|
|
<div class="scale-bar-line" :style="{ width: scaleBarWidthPx + 'px' }">
|
|
<span class="scale-bar-tick scale-bar-tick-left"></span>
|
|
<span class="scale-bar-tick scale-bar-tick-right"></span>
|
|
</div>
|
|
</div>
|
|
<div class="coordinates-display">
|
|
{{ coordinatesText }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<script>
|
|
import * as Cesium from 'cesium'
|
|
import 'cesium/Build/Cesium/Widgets/widgets.css'
|
|
import DrawingToolbar from './DrawingToolbar.vue'
|
|
import MeasurementPanel from './MeasurementPanel.vue'
|
|
import HoverTooltip from './HoverTooltip.vue'
|
|
import ContextMenu from './ContextMenu.vue'
|
|
import LocateDialog from './LocateDialog.vue'
|
|
import axios from 'axios'
|
|
import request from '@/utils/request'
|
|
import { getToken } from '@/utils/auth'
|
|
export default {
|
|
name: 'CesiumMap',
|
|
props: {
|
|
drawDomClick: {
|
|
type: Boolean,
|
|
default: false,
|
|
// 增加 props 类型校验,方便调试
|
|
validator(val) {
|
|
const isBoolean = typeof val === 'boolean'
|
|
if (!isBoolean) {
|
|
console.error('drawDomClick 必须是布尔值,当前值:', val, '类型:', typeof val)
|
|
}
|
|
return isBoolean
|
|
}
|
|
},
|
|
toolMode: {
|
|
type: String,
|
|
default: 'airspace' // 'airspace' or 'ranging'
|
|
},
|
|
},
|
|
watch: {
|
|
drawDomClick: {
|
|
immediate: true, // 组件初始化时立即执行一次
|
|
handler(newVal, oldVal) {
|
|
// 可选:如果需要在值变化时执行额外逻辑(比如初始化地图)
|
|
if (newVal) {
|
|
// this.initMap()
|
|
}
|
|
}
|
|
}
|
|
},
|
|
data() {
|
|
return {
|
|
viewer: null,
|
|
// 绘制相关
|
|
drawingMode: null, // 'point', 'line', 'polygon', 'rectangle', 'circle'
|
|
drawingHandler: null,
|
|
tempEntity: null, // 最终实体
|
|
tempPreviewEntity: null, // 预览实体(新增)
|
|
drawingPoints: [],
|
|
drawingPointEntities: [], // 存储线绘制时的点实体
|
|
drawingStartPoint: null,
|
|
isDrawing: false,
|
|
activeCursorPosition: null, // 实时鼠标位置
|
|
// 实体管理
|
|
allEntities: [], // 所有绘制的实体
|
|
entityCounter: 0,
|
|
selectedEntity: null, // 当前选中的实体
|
|
// 测量结果
|
|
measurementResult: null,
|
|
// 悬停提示
|
|
hoverTooltip: {
|
|
visible: false,
|
|
content: '',
|
|
position: { x: 0, y: 0 }
|
|
},
|
|
// 右键菜单
|
|
contextMenu: {
|
|
visible: false,
|
|
position: { x: 0, y: 0 },
|
|
entityData: null
|
|
},
|
|
// 默认样式
|
|
defaultStyles: {
|
|
point: { color: '#FF0000', size: 12 },
|
|
line: { color: '#00FF00', width: 3 },
|
|
polygon: { color: '#0000FF', opacity: 0.5, width: 2 },
|
|
rectangle: { color: '#FFA500', opacity: 0.3, width: 2 },
|
|
circle: { color: '#800080', opacity: 0.4, width: 2 },
|
|
sector: { color: '#FF6347', opacity: 0.5, width: 2 },
|
|
arrow: { color: '#FF0000', width: 6 },
|
|
text: { color: '#000000', font: '48px Microsoft YaHei, PingFang SC, sans-serif', backgroundColor: 'rgba(255, 255, 255, 0.8)' },
|
|
image: { width: 150, height: 150 }
|
|
},
|
|
// 鼠标经纬度
|
|
coordinatesText: '经度: --, 纬度: --',
|
|
// 比例尺(高德风格)
|
|
scaleBarText: '--',
|
|
scaleBarWidthPx: 80,
|
|
// 定位相关
|
|
locateDialogVisible: false
|
|
}
|
|
},
|
|
components: {
|
|
DrawingToolbar,
|
|
MeasurementPanel,
|
|
HoverTooltip,
|
|
ContextMenu,
|
|
LocateDialog
|
|
},
|
|
mounted() {
|
|
console.log(this.drawDomClick,999999)
|
|
// this.initMap()
|
|
this.checkCesiumLoaded()
|
|
},
|
|
beforeDestroy() {
|
|
// 销毁鼠标悬停事件处理器
|
|
if (this.hoverHandler) {
|
|
this.hoverHandler.destroy();
|
|
this.hoverHandler = null;
|
|
}
|
|
this.destroyViewer()
|
|
},
|
|
methods: {
|
|
preventContextMenu(e) {
|
|
e.preventDefault();
|
|
},
|
|
clearAllWaypoints() {
|
|
console.log(">>> [执行深度清理] 正在清除所有航迹残留...");
|
|
// 清除所有带业务标记的实体(点和线)
|
|
const entities = this.viewer.entities.values;
|
|
for (let i = entities.length - 1; i >= 0; i--) {
|
|
const entity = entities[i];
|
|
const props = entity.properties;
|
|
// 航点 (isMissionWaypoint)
|
|
const isWp = props && props.isMissionWaypoint;
|
|
// 航线 (isMissionRouteLine)
|
|
const isLine = props && props.isMissionRouteLine;
|
|
// 绘制时的临时 ID
|
|
const isTemp = entity.id && String(entity.id).includes('temp_wp_');
|
|
|
|
if (isWp || isLine || isTemp) {
|
|
this.viewer.entities.remove(entity);
|
|
}
|
|
}
|
|
if (this.tempPreviewEntity) {
|
|
this.viewer.entities.remove(this.tempPreviewEntity);
|
|
this.tempPreviewEntity = null;
|
|
}
|
|
// 清理手动维护的记录数组,只移除与航线相关的实体
|
|
if (this.allEntities) {
|
|
// 过滤掉与航线相关的实体,保留用户绘制的线段等实体
|
|
this.allEntities = this.allEntities.filter(item => {
|
|
// 检查实体是否与航线相关
|
|
const isRouteRelated = item.type === 'route' || item.routeId ||
|
|
(item.entity && item.entity.properties &&
|
|
(item.entity.properties.isMissionWaypoint || item.entity.properties.isMissionRouteLine));
|
|
|
|
// 如果是航线相关实体,从地图中移除
|
|
if (isRouteRelated && item.entity) {
|
|
this.viewer.entities.remove(item.entity);
|
|
return false;
|
|
}
|
|
|
|
// 保留非航线相关实体
|
|
return true;
|
|
});
|
|
}
|
|
// 重置坐标缓存
|
|
if (this.drawingPoints) {
|
|
this.drawingPoints = [];
|
|
}
|
|
|
|
console.log(">>> [清理完毕] 航迹已清空,用户绘制的实体已保留。");
|
|
},
|
|
updateWaypointGraphicById(dbId, newName) {
|
|
const entities = this.viewer.entities.values;
|
|
|
|
// 1. 精确查找
|
|
const target = entities.find(e => {
|
|
// 必须确保实体有 properties 且包含 dbId
|
|
if (e.properties && e.properties.dbId) {
|
|
// 【核心修复】使用 .getValue() 获取包装内的原始值
|
|
// 同时使用 == (非严格等于) 兼容字符串与数字对比
|
|
return e.properties.dbId.getValue() === dbId;
|
|
}
|
|
return false;
|
|
});
|
|
|
|
if (target) {
|
|
// 2. 同步修改实体的属性与 Label 文字
|
|
target.name = newName;
|
|
if (target.label) {
|
|
target.label.text = newName;
|
|
}
|
|
console.log(`>>> [地图同步成功] ID:${dbId} 已重命名为: ${newName}`);
|
|
} else {
|
|
// 调试辅助:打印当前地图上所有可用的 ID,看看数据到底对不对
|
|
const availableIds = entities
|
|
.filter(e => e.properties && e.properties.dbId)
|
|
.map(e => e.properties.dbId.getValue());
|
|
console.warn(`>>> [地图同步失败] 尝试匹配 ID: ${dbId}, 但地图现有 ID 列表为:`, availableIds);
|
|
}
|
|
},
|
|
// 新建航线绘制
|
|
startMissionRouteDrawing() {
|
|
this.stopDrawing(); // 停止其他可能存在的绘制
|
|
this.drawingPoints = [];
|
|
let activeCursorPosition = null;
|
|
this.isDrawing = true;
|
|
this.viewer.canvas.style.cursor = 'crosshair';
|
|
this.drawingHandler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas);
|
|
window.addEventListener('contextmenu', this.preventContextMenu, true);
|
|
// 鼠标移动预览逻辑
|
|
this.drawingHandler.setInputAction((movement) => {
|
|
const newPosition = this.getClickPosition(movement.endPosition);
|
|
if (newPosition) {
|
|
activeCursorPosition = newPosition;
|
|
}
|
|
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
|
|
// 左键点击逻辑
|
|
this.drawingHandler.setInputAction((click) => {
|
|
const position = this.getClickPosition(click.position);
|
|
if (!position) return;
|
|
this.drawingPoints.push(position);
|
|
const wpIndex = this.drawingPoints.length;
|
|
// 绘制业务航点
|
|
this.viewer.entities.add({
|
|
id: `temp_wp_${wpIndex}`,
|
|
name: `WP${wpIndex}`,
|
|
position: position,
|
|
properties: {
|
|
isMissionWaypoint: true, // 这是一个永久的业务标记
|
|
originalIndex: wpIndex, // 存下它是第几个点
|
|
temp: true
|
|
},
|
|
point: {
|
|
pixelSize: 10,
|
|
color: Cesium.Color.WHITE,
|
|
outlineColor: Cesium.Color.fromCssColorString('#0078FF'),
|
|
outlineWidth: 3,
|
|
disableDepthTestDistance: Number.POSITIVE_INFINITY // 保证不被地形遮挡
|
|
},
|
|
label: {
|
|
text: `WP${wpIndex}`,
|
|
font: '12px MicroSoft YaHei',
|
|
pixelOffset: new Cesium.Cartesian2(0, -20),
|
|
fillColor: Cesium.Color.WHITE,
|
|
outlineColor: Cesium.Color.BLACK,
|
|
outlineWidth: 2,
|
|
style: Cesium.LabelStyle.FILL_AND_OUTLINE
|
|
}
|
|
});
|
|
// 第一次点击后,创建动态黑白斑马线
|
|
if (this.drawingPoints.length === 1) {
|
|
this.tempPreviewEntity = this.viewer.entities.add({
|
|
polyline: {
|
|
positions: new Cesium.CallbackProperty(() => {
|
|
if (this.drawingPoints.length > 0 && activeCursorPosition) {
|
|
return [...this.drawingPoints, activeCursorPosition];
|
|
}
|
|
return this.drawingPoints;
|
|
}, false),
|
|
width: 4,
|
|
// 黑白斑马材质
|
|
material: new Cesium.PolylineDashMaterialProperty({
|
|
color: Cesium.Color.WHITE, // 主色:白
|
|
gapColor: Cesium.Color.BLACK, // 间隙色:黑
|
|
dashLength: 20.0 // 斑马纹长度
|
|
}),
|
|
clampToGround: true
|
|
}
|
|
});
|
|
}
|
|
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
|
// 右键点击逻辑(结束绘制、抛出数据、恢复右键)
|
|
this.drawingHandler.setInputAction(() => {
|
|
if (this.drawingPoints.length > 1) {
|
|
// 转换坐标并传回给 childRoom/index.vue
|
|
const latLngPoints = this.drawingPoints.map((p, index) => {
|
|
const coords = this.cartesianToLatLng(p);
|
|
return {
|
|
id: index + 1,
|
|
name: `WP${index + 1}`,
|
|
lat: coords.lat,
|
|
lng: coords.lng,
|
|
alt: 5000,
|
|
speed: 800
|
|
};
|
|
});
|
|
this.$emit('draw-complete', latLngPoints);
|
|
} else {
|
|
this.$message.info('点数不足,航线已取消');
|
|
}
|
|
// 清理并恢复环境
|
|
this.stopDrawing();
|
|
setTimeout(() => {
|
|
window.removeEventListener('contextmenu', this.preventContextMenu, true);
|
|
}, 200);
|
|
}, Cesium.ScreenSpaceEventType.RIGHT_CLICK);
|
|
},
|
|
// 格式化平台图标 URL(与 RouteEditDialog / RightPanel 一致)
|
|
formatPlatformIconUrl(url) {
|
|
if (!url) return '';
|
|
const cleanPath = (url + '').replace(/\/+/g, '/');
|
|
const backendUrl = process.env.VUE_APP_BACKEND_URL || '';
|
|
return backendUrl + cleanPath;
|
|
},
|
|
//正式航线渲染函数(style 可选:waypoint { pixelSize, color, outlineColor, outlineWidth },line { style, width, color, gapColor, dashLength })
|
|
renderRouteWaypoints(waypoints, routeId = 'default', platformId, platform, style) {
|
|
if (!waypoints || waypoints.length < 1) return;
|
|
// 清理旧线
|
|
const lineId = `route-line-${routeId}`;
|
|
const existingLine = this.viewer.entities.getById(lineId);
|
|
if (existingLine) {
|
|
this.viewer.entities.remove(existingLine);
|
|
}
|
|
// 清理所有属于该航线的旧点
|
|
waypoints.forEach((wp,index) => {
|
|
const waypointEntityId = `wp_${routeId}_${wp.id}`;
|
|
const existingWaypoint = this.viewer.entities.getById(waypointEntityId);
|
|
if (existingWaypoint) {
|
|
this.viewer.entities.remove(existingWaypoint);
|
|
}
|
|
const arcId = `arc-line-${routeId}-${index}`;
|
|
const existingArc = this.viewer.entities.getById(arcId);
|
|
if (existingArc) {
|
|
this.viewer.entities.remove(existingArc);
|
|
}
|
|
});
|
|
const wpStyle = (style && style.waypoint) ? style.waypoint : {};
|
|
const lineStyle = (style && style.line) ? style.line : {};
|
|
const pixelSize = wpStyle.pixelSize != null ? wpStyle.pixelSize : 7;
|
|
const wpColor = wpStyle.color || '#ffffff';
|
|
const wpOutline = wpStyle.outlineColor || '#0078FF';
|
|
const wpOutlineW = wpStyle.outlineWidth != null ? wpStyle.outlineWidth : 2;
|
|
// 遍历并绘制航点标记
|
|
const originalPositions = [];
|
|
waypoints.forEach((wp, index) => {
|
|
const lon = parseFloat(wp.lng);
|
|
const lat = parseFloat(wp.lat);
|
|
// 使用 Number 转换 Double 类型
|
|
const altValue = Number(wp.alt || 5000);
|
|
const pos = Cesium.Cartesian3.fromDegrees(lon, lat, altValue);
|
|
originalPositions.push(pos);
|
|
this.viewer.entities.add({
|
|
id: `wp_${routeId}_${wp.id}`,
|
|
name: wp.name || `WP${index + 1}`,
|
|
position: pos,
|
|
properties: {
|
|
isMissionWaypoint: true,
|
|
routeId: routeId,
|
|
dbId: wp.id,
|
|
},
|
|
point: {
|
|
pixelSize: pixelSize,
|
|
color: Cesium.Color.fromCssColorString(wpColor),
|
|
outlineColor: Cesium.Color.fromCssColorString(wpOutline),
|
|
outlineWidth: wpOutlineW,
|
|
disableDepthTestDistance: Number.POSITIVE_INFINITY
|
|
},
|
|
label: {
|
|
text: wp.name || `WP${index + 1}`,
|
|
font: `${Math.max(9, pixelSize + 2)}px MicroSoft YaHei`,
|
|
pixelOffset: new Cesium.Cartesian2(0, -Math.max(14, pixelSize + 8)),
|
|
fillColor: Cesium.Color.WHITE,
|
|
outlineColor: Cesium.Color.BLACK,
|
|
outlineWidth: 2,
|
|
style: Cesium.LabelStyle.FILL_AND_OUTLINE
|
|
}
|
|
});
|
|
});
|
|
// 在起点渲染平台图标(当航线有关联平台且平台有图标时)
|
|
const iconUrl = (platform && platform.imageUrl) || (platform && platform.iconUrl);
|
|
if (iconUrl && originalPositions.length > 0) {
|
|
const platformBillboardId = `route-platform-${routeId}`;
|
|
const fullUrl = this.formatPlatformIconUrl(iconUrl);
|
|
this.viewer.entities.add({
|
|
id: platformBillboardId,
|
|
name: (platform && platform.name) || '平台',
|
|
position: originalPositions[0],
|
|
properties: { routeId: routeId },
|
|
billboard: {
|
|
image: fullUrl,
|
|
width: 144,
|
|
height: 144,
|
|
verticalOrigin: Cesium.VerticalOrigin.CENTER,
|
|
horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
|
|
scaleByDistance: new Cesium.NearFarScalar(500, 2.0, 200000, 0.4),
|
|
translucencyByDistance: new Cesium.NearFarScalar(1000, 1.0, 500000, 0.6)
|
|
}
|
|
});
|
|
}
|
|
// 绘制连线
|
|
if (waypoints.length > 1) {
|
|
let finalPathPositions = [];
|
|
for (let i = 0; i < waypoints.length; i++) {
|
|
const currPos = originalPositions[i];
|
|
const radius = this.getWaypointRadius(waypoints[i]);
|
|
// 如果不是首尾点且有半径,我们就画红色的弧线
|
|
if (i > 0 && i < waypoints.length - 1 && radius > 0) {
|
|
const prevPos = originalPositions[i - 1];
|
|
const nextPos = originalPositions[i + 1];
|
|
const arcPoints = this.computeArcPositions(prevPos, currPos, nextPos, radius);
|
|
// 绘制红色实线弧
|
|
this.viewer.entities.add({
|
|
id: `arc-line-${routeId}-${i}`,
|
|
polyline: {
|
|
positions: arcPoints,
|
|
width: 8,
|
|
material: Cesium.Color.RED,
|
|
clampToGround: true,
|
|
zIndex: 20
|
|
},
|
|
properties: {routeId: routeId}
|
|
});
|
|
console.log(`>>> 航点 ${waypoints[i].name} 已渲染红色转弯弧,半径: ${radius}`);
|
|
}
|
|
if (i === 0 || i === waypoints.length - 1 || radius <= 0) {
|
|
finalPathPositions.push(currPos);
|
|
} else {
|
|
const prevPos = originalPositions[i - 1];
|
|
const nextPos = originalPositions[i + 1];
|
|
// 计算弧线点集
|
|
const arcPoints = this.computeArcPositions(prevPos, currPos, nextPos, radius);
|
|
finalPathPositions.push(...arcPoints);
|
|
}
|
|
}
|
|
const lineWidth = lineStyle.width != null ? lineStyle.width : 4;
|
|
const lineColor = lineStyle.color || '#ffffff';
|
|
const gapColor = lineStyle.gapColor != null ? lineStyle.gapColor : '#000000';
|
|
const dashLen = lineStyle.dashLength != null ? lineStyle.dashLength : 20;
|
|
const useDash = (lineStyle.style || 'dash') === 'dash';
|
|
const lineMaterial = useDash
|
|
? new Cesium.PolylineDashMaterialProperty({
|
|
color: Cesium.Color.fromCssColorString(lineColor),
|
|
gapColor: Cesium.Color.fromCssColorString(gapColor),
|
|
dashLength: dashLen
|
|
})
|
|
: Cesium.Color.fromCssColorString(lineColor);
|
|
// 绘制包含弧线的 Polyline
|
|
const routeEntity = this.viewer.entities.add({
|
|
id: lineId,
|
|
polyline: {
|
|
positions: finalPathPositions,
|
|
width: lineWidth,
|
|
material: lineMaterial,
|
|
clampToGround: true,
|
|
zIndex: 1
|
|
},
|
|
properties: {isMissionRouteLine: true, routeId: routeId}
|
|
});
|
|
if (this.allEntities) {
|
|
this.allEntities.push({id: lineId, entity: routeEntity, type: 'route'});
|
|
}
|
|
}
|
|
},
|
|
// 计算单个点的转弯半径
|
|
getWaypointRadius(wp) {
|
|
const speed = wp.speed || 800;
|
|
const bankAngle = wp.turnAngle || 0;
|
|
if (bankAngle <= 0) return 0;
|
|
const v_mps = speed / 3.6;
|
|
const radians = bankAngle * (Math.PI / 180);
|
|
const g = 9.8;
|
|
return (v_mps * v_mps) / (g * Math.tan(radians));
|
|
},
|
|
|
|
// 计算转弯处的圆弧点集
|
|
computeArcPositions(p1, p2, p3, radius) {
|
|
const v1 = Cesium.Cartesian3.subtract(p1, p2, new Cesium.Cartesian3());
|
|
const v2 = Cesium.Cartesian3.subtract(p3, p2, new Cesium.Cartesian3());
|
|
const dir1 = Cesium.Cartesian3.normalize(v1, new Cesium.Cartesian3());
|
|
const dir2 = Cesium.Cartesian3.normalize(v2, new Cesium.Cartesian3());
|
|
|
|
const angle = Cesium.Cartesian3.angleBetween(dir1, dir2);
|
|
const dist = radius / Math.tan(angle / 2);
|
|
|
|
const t1 = Cesium.Cartesian3.add(p2, Cesium.Cartesian3.multiplyByScalar(dir1, dist, new Cesium.Cartesian3()), new Cesium.Cartesian3());
|
|
const t2 = Cesium.Cartesian3.add(p2, Cesium.Cartesian3.multiplyByScalar(dir2, dist, new Cesium.Cartesian3()), new Cesium.Cartesian3());
|
|
|
|
let arc = [];
|
|
// 采样15个点让弧线丝滑
|
|
for (let t = 0; t <= 1; t += 0.07) {
|
|
const c1 = Math.pow(1 - t, 2);
|
|
const c2 = 2 * (1 - t) * t;
|
|
const c3 = Math.pow(t, 2);
|
|
arc.push(new Cesium.Cartesian3(
|
|
c1 * t1.x + c2 * p2.x + c3 * t2.x,
|
|
c1 * t1.y + c2 * p2.y + c3 * t2.y,
|
|
c1 * t1.z + c2 * p2.z + c3 * t2.z
|
|
));
|
|
}
|
|
return arc;
|
|
},
|
|
removeRouteById(routeId) {
|
|
// 从地图上移除所有属于该 routeId 的实体
|
|
const entityList = this.viewer.entities.values;
|
|
for (let i = entityList.length - 1; i >= 0; i--) {
|
|
const entity = entityList[i];
|
|
let shouldRemove = false;
|
|
// 平台图标实体:通过 id 匹配
|
|
if (entity.id === `route-platform-${routeId}`) {
|
|
shouldRemove = true;
|
|
} else if (entity.properties && entity.properties.routeId) {
|
|
const id = entity.properties.routeId.getValue && entity.properties.routeId.getValue();
|
|
if (id === routeId) shouldRemove = true;
|
|
}
|
|
if (shouldRemove) this.viewer.entities.remove(entity);
|
|
}
|
|
this.allEntities = this.allEntities.filter(item => item.id !== routeId && item.id !== `route-platform-${routeId}`);
|
|
},
|
|
checkCesiumLoaded() {
|
|
if (typeof Cesium === 'undefined') {
|
|
console.error('Cesium未加载,请检查CDN链接');
|
|
// 可以设置重试机制
|
|
setTimeout(() => {
|
|
if (typeof Cesium !== 'undefined') {
|
|
this.initMap();
|
|
} else {
|
|
console.error('Cesium加载失败');
|
|
}
|
|
}, 1000);
|
|
} else {
|
|
this.initMap();
|
|
}
|
|
},
|
|
initMap() {
|
|
try {
|
|
// 确保 Cesium 已加载
|
|
Cesium.buildModuleUrl.setBaseUrl(window.CESIUM_BASE_URL)
|
|
Cesium.Ion.defaultAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJjN2MzMmE5OS01NGU3LTQzOGQtYjdjZi1mNGIwZTFjZjQ0NmEiLCJpZCI6MTQ0MDc2LCJpYXQiOjE2ODU3NjY1OTN9.iCmFY-5WNdvyAT-EO2j-unrFm4ZN9J6aSuB2wElQZ-I'
|
|
this.viewer = new Cesium.Viewer('cesiumViewer', {
|
|
animation: false,
|
|
fullscreenButton: false,
|
|
baseLayerPicker: false,
|
|
navigationInstructionsInitiallyVisible: false,
|
|
geocoder: false,
|
|
homeButton: false,
|
|
infoBox: false,
|
|
sceneModePicker: false,
|
|
selectionIndicator: false,
|
|
timeline: false,
|
|
navigationHelpButton: false,
|
|
sceneMode: Cesium.SceneMode.SCENE2D,
|
|
mapProjection: new Cesium.WebMercatorProjection(),
|
|
imageryProvider: false,
|
|
terrainProvider: new Cesium.EllipsoidTerrainProvider(),
|
|
baseLayer: false
|
|
})
|
|
this.viewer.cesiumWidget.creditContainer.style.display = "none"
|
|
this.loadOfflineMap()
|
|
this.setup2DConstraints()
|
|
// 初始视野:中国中心(仅中国瓦片时无需世界瓦片,国外区域会无瓦片显示为空白)
|
|
this.viewer.camera.setView({
|
|
destination: Cesium.Cartesian3.fromDegrees(116.3974, 39.9093, 12000000),
|
|
orientation: {
|
|
heading: 0,
|
|
pitch: -Cesium.Math.PI_OVER_TWO,
|
|
roll: 0
|
|
}
|
|
})
|
|
this.initScaleBar()
|
|
this.initPointMovement()
|
|
this.initRightClickHandler()
|
|
this.initHoverHandler()
|
|
this.initMouseCoordinates()
|
|
console.log('Cesium离线二维地图已加载')
|
|
console.log('Cesium离线二维地图已加载')
|
|
// 1. 定义全局拾取处理器
|
|
this.handler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas);
|
|
this.handler.setInputAction((click) => {
|
|
// 隐藏右键菜单
|
|
this.contextMenu.visible = false;
|
|
|
|
if (this.isDrawing) return;
|
|
|
|
const pickedObject = this.viewer.scene.pick(click.position);
|
|
if (Cesium.defined(pickedObject) && pickedObject.id) {
|
|
const entity = pickedObject.id;
|
|
|
|
// --- 修正后的安全日志 ---
|
|
console.log(">>> [点击检测] 实体ID:", entity.id);
|
|
|
|
// 获取属性时必须传入当前仿真时间
|
|
const now = Cesium.JulianDate.now();
|
|
const props = entity.properties ? entity.properties.getValue(now) : null;
|
|
|
|
console.log(">>> [点击检测] 业务数据详情:", props);
|
|
|
|
if (props && props.isMissionWaypoint) {
|
|
const dbId = props.dbId;
|
|
const routeId = props.routeId;
|
|
console.log(`>>> [地图触发] 点击了点 ${dbId}, 属于航线 ${routeId}`);
|
|
// 关键:把航线 ID 也传给父组件
|
|
this.$emit('open-waypoint-dialog', dbId, routeId);
|
|
} else if (props && props.isMissionRouteLine) {
|
|
const routeId = props.routeId;
|
|
console.log(`>>> [地图触发] 点击了航线 ${routeId}`);
|
|
// 触发航线编辑弹窗事件
|
|
this.$emit('open-route-dialog', routeId);
|
|
}
|
|
}
|
|
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
|
} catch (error) {
|
|
console.error('地图错误:', error)
|
|
// 如果Cesium加载失败,显示错误信息
|
|
this.showErrorMessage();
|
|
}
|
|
},
|
|
initRightClickHandler() {
|
|
// 创建屏幕空间事件处理器
|
|
this.rightClickHandler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas)
|
|
// 右键点击事件:显示上下文菜单
|
|
this.rightClickHandler.setInputAction((click) => {
|
|
// 如果正在绘制,不处理右键操作
|
|
if (this.isDrawing) {
|
|
return;
|
|
}
|
|
|
|
// 隐藏之前可能显示的菜单
|
|
this.contextMenu.visible = false;
|
|
|
|
const pickedObject = this.viewer.scene.pick(click.position)
|
|
if (Cesium.defined(pickedObject) && pickedObject.id) {
|
|
const pickedEntity = pickedObject.id
|
|
// 查找对应的实体数据
|
|
let entityData = this.allEntities.find(e => e.entity === pickedEntity || e === pickedEntity)
|
|
// 特殊处理:如果点击的是线段上的点,找到对应的线实体
|
|
if (!entityData) {
|
|
// 检查是否是线段上的点
|
|
for (const lineEntity of this.allEntities) {
|
|
if (lineEntity.type === 'line' && lineEntity.pointEntities) {
|
|
if (lineEntity.pointEntities.includes(pickedEntity)) {
|
|
entityData = lineEntity
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (entityData && entityData.type !== 'route') {
|
|
// 显示上下文菜单
|
|
this.contextMenu = {
|
|
visible: true,
|
|
position: {
|
|
x: click.position.x,
|
|
y: click.position.y
|
|
},
|
|
entityData: entityData
|
|
};
|
|
}
|
|
}
|
|
}, Cesium.ScreenSpaceEventType.RIGHT_CLICK)
|
|
},
|
|
// 初始化鼠标悬停事件处理器
|
|
initHoverHandler() {
|
|
// 创建屏幕空间事件处理器
|
|
this.hoverHandler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas)
|
|
// 鼠标移动事件:检测是否悬停在线上
|
|
this.hoverHandler.setInputAction((movement) => {
|
|
// 如果正在绘制,不处理悬停操作
|
|
if (this.isDrawing) {
|
|
this.hoverTooltip.visible = false;
|
|
return;
|
|
}
|
|
const pickedObject = this.viewer.scene.pick(movement.endPosition)
|
|
if (Cesium.defined(pickedObject) && pickedObject.id) {
|
|
const pickedEntity = pickedObject.id
|
|
// 查找对应的实体数据
|
|
let entityData = this.allEntities.find(e => e.entity === pickedEntity || e === pickedEntity)
|
|
// 特殊处理:如果悬停的是线段上的点,找到对应的线实体
|
|
if (!entityData) {
|
|
// 检查是否是线段上的点
|
|
for (const lineEntity of this.allEntities) {
|
|
if (lineEntity.type === 'line' && lineEntity.pointEntities) {
|
|
if (lineEntity.pointEntities.includes(pickedEntity)) {
|
|
entityData = lineEntity
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// 如果是线实体,显示长度信息
|
|
if (entityData && entityData.type === 'line') {
|
|
// 找到鼠标悬停在线段的哪一段
|
|
const segmentIndex = this.findClosestSegment(entityData.positions, movement.endPosition);
|
|
if (segmentIndex !== -1) {
|
|
// 计算累计长度(前segmentIndex+1段的长度)
|
|
let cumulativeLength = 0;
|
|
for (let i = 0; i < segmentIndex + 1; i++) {
|
|
if (i < entityData.positions.length - 1) {
|
|
cumulativeLength += Cesium.Cartesian3.distance(entityData.positions[i], entityData.positions[i + 1]);
|
|
}
|
|
}
|
|
// 计算当前段的方位角
|
|
const currentSegmentPositions = [entityData.positions[segmentIndex], entityData.positions[segmentIndex + 1]];
|
|
// 根据当前方位角类型计算方位角
|
|
const bearingType = entityData.bearingType || 'true';
|
|
const bearing = bearingType === 'magnetic'
|
|
? this.calculateMagneticBearing(currentSegmentPositions)
|
|
: this.calculateTrueBearing(currentSegmentPositions);
|
|
// 显示小框提示
|
|
this.hoverTooltip = {
|
|
visible: true,
|
|
content: `累计长度:${cumulativeLength.toFixed(2)} 米\n${bearingType === 'magnetic' ? '磁方位' : '真方位'}:${bearing.toFixed(2)}°`,
|
|
position: {
|
|
x: movement.endPosition.x + 10,
|
|
y: movement.endPosition.y - 10
|
|
}
|
|
};
|
|
} else {
|
|
// 如果没有找到对应的段,隐藏信息
|
|
this.hoverTooltip.visible = false;
|
|
}
|
|
} else {
|
|
// 如果不是线实体,隐藏信息
|
|
this.hoverTooltip.visible = false;
|
|
}
|
|
} else {
|
|
// 如果没有悬停在任何实体上,隐藏信息
|
|
this.hoverTooltip.visible = false;
|
|
}
|
|
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE)
|
|
},
|
|
showErrorMessage() {
|
|
const container = document.getElementById('cesiumViewer');
|
|
if (container) {
|
|
container.innerHTML = `
|
|
<div style="
|
|
width: 100%;
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: #f5f5f5;
|
|
color: #666;
|
|
font-family: Arial, sans-serif;
|
|
">
|
|
<h3 style="margin-bottom: 20px;">地图加载失败</h3>
|
|
<p>可能的原因:</p>
|
|
<ul style="text-align: left; margin: 10px 0;">
|
|
<li>网络连接问题</li>
|
|
<li>Cesium CDN资源未加载</li>
|
|
<li>浏览器兼容性问题</li>
|
|
</ul>
|
|
<button onclick="location.reload()" style="
|
|
margin-top: 20px;
|
|
padding: 10px 20px;
|
|
background: #4dabf7;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
">
|
|
重新加载
|
|
</button>
|
|
</div>
|
|
`;
|
|
}
|
|
},
|
|
setup2DConstraints() {
|
|
const scene = this.viewer.scene
|
|
const controller = scene.screenSpaceCameraController
|
|
controller.enableTilt = false
|
|
controller.enableRotate = false
|
|
controller.enableLook = false
|
|
scene.screenSpaceCameraController.maximumPitch = 0
|
|
scene.screenSpaceCameraController.minimumPitch = 0
|
|
},
|
|
loadOfflineMap() {
|
|
this.viewer.imageryLayers.removeAll()
|
|
try {
|
|
// 使用本地瓦片(tiles 目录,结构:z/x/y.png)替代高德在线地图
|
|
const origin = typeof window !== 'undefined' ? window.location.origin : ''
|
|
const tilesUrl = origin + '/tiles/{z}/{x}/{reverseY}.png'
|
|
const tilingScheme = new Cesium.WebMercatorTilingScheme()
|
|
const chinaRect = Cesium.Rectangle.fromDegrees(73.5, 18.0, 135.0, 53.5)
|
|
// 底层:全球 0–8 级(境外和中国在 0–8 级都显示)
|
|
const worldTiles = new Cesium.UrlTemplateImageryProvider({
|
|
url: tilesUrl,
|
|
minimumLevel: 0,
|
|
maximumLevel: 8,
|
|
tilingScheme,
|
|
credit: ''
|
|
})
|
|
this.viewer.imageryLayers.addImageryProvider(worldTiles)
|
|
// 顶层:仅中国范围 0–14 级(中国内可缩放到 14 级,境外不请求)
|
|
const chinaTiles = new Cesium.UrlTemplateImageryProvider({
|
|
url: tilesUrl,
|
|
minimumLevel: 0,
|
|
maximumLevel: 14,
|
|
rectangle: chinaRect,
|
|
tilingScheme,
|
|
credit: ''
|
|
})
|
|
this.viewer.imageryLayers.addImageryProvider(chinaTiles)
|
|
} catch (error) {
|
|
console.error('加载本地瓦片失败:', error)
|
|
this.showGridLayer()
|
|
}
|
|
},
|
|
showGridLayer() {
|
|
const gridProvider = new Cesium.GridImageryProvider()
|
|
this.viewer.imageryLayers.addImageryProvider(gridProvider)
|
|
this.addCoordinateLabels()
|
|
},
|
|
addCoordinateLabels() {
|
|
for (let lon = -180; lon <= 180; lon += 30) {
|
|
for (let lat = -90; lat <= 90; lat += 30) {
|
|
this.viewer.entities.add({
|
|
position: Cesium.Cartesian3.fromDegrees(lon, lat),
|
|
label: {
|
|
text: `${lat}°N\n${lon}°E`,
|
|
font: '12px sans-serif',
|
|
fillColor: Cesium.Color.BLACK,
|
|
outlineColor: Cesium.Color.WHITE,
|
|
outlineWidth: 2,
|
|
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
|
|
verticalOrigin: Cesium.VerticalOrigin.CENTER,
|
|
pixelOffset: new Cesium.Cartesian2(10, 0)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
},
|
|
// ================== 绘制功能 ==================
|
|
toggleDrawing(mode) {
|
|
if (mode === null) {
|
|
// 当模式为null时,直接停止绘制
|
|
this.stopDrawing()
|
|
this.drawingMode = null
|
|
} else if (this.drawingMode === mode) {
|
|
// 停止当前绘制
|
|
this.stopDrawing()
|
|
this.drawingMode = null
|
|
} else {
|
|
// 停止之前的绘制
|
|
if (this.drawingMode) {
|
|
this.stopDrawing()
|
|
}
|
|
// 开始新的绘制
|
|
this.drawingMode = mode
|
|
this.startDrawing(mode)
|
|
}
|
|
},
|
|
startDrawing(mode) {
|
|
this.stopDrawing()
|
|
this.drawingPoints = []
|
|
this.drawingStartPoint = null
|
|
this.isDrawing = true
|
|
this.drawingHandler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas)
|
|
// 根据模式设置不同的鼠标样式和事件
|
|
switch (mode) {
|
|
case 'point':
|
|
this.startPointDrawing()
|
|
break
|
|
case 'line':
|
|
this.startLineDrawing()
|
|
break
|
|
case 'polygon':
|
|
this.startPolygonDrawing()
|
|
break
|
|
case 'rectangle':
|
|
this.startRectangleDrawing()
|
|
break
|
|
case 'circle':
|
|
this.startCircleDrawing()
|
|
break
|
|
case 'sector':
|
|
this.startSectorDrawing()
|
|
break
|
|
case 'arrow':
|
|
this.startArrowDrawing()
|
|
break
|
|
case 'text':
|
|
this.startTextDrawing()
|
|
break
|
|
case 'image':
|
|
this.startImageDrawing()
|
|
break
|
|
}
|
|
this.viewer.scene.canvas.style.cursor = 'crosshair'
|
|
console.log(`开始绘制 ${this.getTypeName(mode)}`)
|
|
},
|
|
stopDrawing() {
|
|
// 销毁处理器
|
|
if (this.drawingHandler) {
|
|
this.drawingHandler.destroy();
|
|
this.drawingHandler = null;
|
|
}
|
|
//还原鼠标样式
|
|
this.viewer.scene.canvas.style.cursor = 'default';
|
|
// 彻底清理临时点
|
|
const allEntities = this.viewer.entities.values;
|
|
for (let i = allEntities.length - 1; i >= 0; i--) {
|
|
const entity = allEntities[i];
|
|
if (entity.id && (
|
|
entity.id.toString().startsWith('temp_wp_') ||
|
|
entity.id.toString().includes('temp-preview')
|
|
)) {
|
|
this.viewer.entities.remove(entity);
|
|
}
|
|
}
|
|
//清理已知的预览实体
|
|
if (this.tempEntity) {
|
|
this.viewer.entities.remove(this.tempEntity);
|
|
this.tempEntity = null;
|
|
}
|
|
if (this.tempPreviewEntity) {
|
|
this.viewer.entities.remove(this.tempPreviewEntity);
|
|
this.tempPreviewEntity = null;
|
|
}
|
|
// 清理点实体数组
|
|
if (this.drawingPointEntities && this.drawingPointEntities.length > 0) {
|
|
this.drawingPointEntities.forEach(pointEntity => {
|
|
this.viewer.entities.remove(pointEntity);
|
|
});
|
|
this.drawingPointEntities = [];
|
|
}
|
|
// 重置状态
|
|
this.drawingPoints = [];
|
|
this.drawingStartPoint = null;
|
|
this.isDrawing = false;
|
|
this.activeCursorPosition = null;
|
|
// 重新初始化右键处理器
|
|
if (!this.rightClickHandler) {
|
|
this.initRightClickHandler();
|
|
}
|
|
// 隐藏测量结果
|
|
this.measurementResult = null;
|
|
this.hoverTooltip.visible = false;
|
|
this.viewer.scene.canvas.style.cursor = 'default';
|
|
console.log(">>> 绘制状态已彻底重置,临时实体已清理");
|
|
},
|
|
cancelDrawing() {
|
|
// 取消绘制,清理临时实体和状态
|
|
if (this.tempEntity) {
|
|
this.viewer.entities.remove(this.tempEntity);
|
|
this.tempEntity = null;
|
|
}
|
|
if (this.tempPreviewEntity) {
|
|
this.viewer.entities.remove(this.tempPreviewEntity);
|
|
this.tempPreviewEntity = null;
|
|
}
|
|
if (this.drawingPointEntities) {
|
|
this.drawingPointEntities.forEach(pointEntity => {
|
|
this.viewer.entities.remove(pointEntity);
|
|
});
|
|
this.drawingPointEntities = [];
|
|
}
|
|
this.drawingPoints = [];
|
|
this.activeCursorPosition = null;
|
|
// 隐藏小框提示
|
|
this.hoverTooltip.visible = false;
|
|
},
|
|
// ********************************************************************
|
|
// 绘制点
|
|
startPointDrawing() {
|
|
this.drawingHandler.setInputAction((click) => {
|
|
const position = this.getClickPosition(click.position)
|
|
if (position) {
|
|
const {lat, lng} = this.cartesianToLatLng(position)
|
|
this.addPointEntity(lat, lng)
|
|
}
|
|
}, Cesium.ScreenSpaceEventType.LEFT_CLICK)
|
|
},
|
|
// 绘制线
|
|
startLineDrawing() {
|
|
this.drawingPoints = [];
|
|
this.drawingPointEntities = []; // 存储点实体
|
|
// 清除可能存在的旧实体
|
|
if (this.tempEntity) this.viewer.entities.remove(this.tempEntity);
|
|
if (this.tempPreviewEntity) this.viewer.entities.remove(this.tempPreviewEntity);
|
|
this.tempEntity = null;
|
|
this.tempPreviewEntity = null;
|
|
// 1. 鼠标移动事件:更新坐标变量并实时计算线段长度
|
|
this.drawingHandler.setInputAction((movement) => {
|
|
const newPosition = this.getClickPosition(movement.endPosition);
|
|
if (newPosition) {
|
|
this.activeCursorPosition = newPosition;
|
|
// 当已经有至少一个点时,实时计算线段长度
|
|
if (this.drawingPoints.length > 0) {
|
|
// 计算从最后一个点到当前鼠标位置的线段长度
|
|
const tempPositions = [...this.drawingPoints, newPosition];
|
|
const length = this.calculateLineLength(tempPositions);
|
|
// 默认为真方位,因为绘制过程中还没有bearingType属性
|
|
const bearing = this.calculateTrueBearing(tempPositions);
|
|
// 更新小框提示,显示实时长度和真方位角
|
|
this.hoverTooltip = {
|
|
visible: true,
|
|
content: `长度:${length.toFixed(2)} 米\n真方位:${bearing.toFixed(2)}°`,
|
|
position: {
|
|
x: movement.endPosition.x + 10,
|
|
y: movement.endPosition.y - 10
|
|
}
|
|
};
|
|
} else {
|
|
// 如果没有点,隐藏提示
|
|
this.hoverTooltip.visible = false;
|
|
}
|
|
}
|
|
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
|
|
// 2. 鼠标点击事件:确定点位
|
|
this.drawingHandler.setInputAction((click) => {
|
|
const position = this.getClickPosition(click.position);
|
|
if (position) {
|
|
this.drawingPoints.push(position);
|
|
// 创建点实体并添加到场景中
|
|
this.entityCounter++;
|
|
const pointId = `point_${this.entityCounter}`;
|
|
const pointEntity = this.viewer.entities.add({
|
|
id: pointId,
|
|
position: position,
|
|
point: {
|
|
pixelSize: this.defaultStyles.point.size,
|
|
color: Cesium.Color.fromCssColorString(this.defaultStyles.point.color),
|
|
outlineColor: Cesium.Color.WHITE,
|
|
outlineWidth: 2
|
|
}
|
|
});
|
|
this.drawingPointEntities.push(pointEntity);
|
|
// 移除旧的预览虚线
|
|
if (this.tempPreviewEntity) {
|
|
this.viewer.entities.remove(this.tempPreviewEntity);
|
|
this.tempPreviewEntity = null;
|
|
}
|
|
// 创建或更新实线
|
|
if (this.drawingPoints.length > 1) {
|
|
if (this.tempEntity) {
|
|
this.viewer.entities.remove(this.tempEntity);
|
|
}
|
|
this.tempEntity = this.viewer.entities.add({
|
|
polyline: {
|
|
positions: this.drawingPoints,
|
|
width: this.defaultStyles.line.width,
|
|
material: Cesium.Color.fromCssColorString(this.defaultStyles.line.color),
|
|
clampToGround: true
|
|
}
|
|
});
|
|
}
|
|
// 创建新的预览虚线(使用 CallbackProperty 实现实时更新)
|
|
this.tempPreviewEntity = this.viewer.entities.add({
|
|
polyline: {
|
|
positions: new Cesium.CallbackProperty(() => {
|
|
if (this.activeCursorPosition) {
|
|
return [this.drawingPoints[this.drawingPoints.length - 1], this.activeCursorPosition];
|
|
}
|
|
return [this.drawingPoints[this.drawingPoints.length - 1]];
|
|
}, false),
|
|
width: this.defaultStyles.line.width,
|
|
material: new Cesium.PolylineDashMaterialProperty({
|
|
color: Cesium.Color.fromCssColorString(this.defaultStyles.line.color),
|
|
dashLength: 16
|
|
}),
|
|
clampToGround: true
|
|
}
|
|
});
|
|
}
|
|
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
|
// 3. 右键完成绘制
|
|
this.drawingHandler.setInputAction(() => {
|
|
// 移除临时实体
|
|
if (this.tempPreviewEntity) {
|
|
this.viewer.entities.remove(this.tempPreviewEntity);
|
|
this.tempPreviewEntity = null;
|
|
}
|
|
if (this.tempEntity) {
|
|
this.viewer.entities.remove(this.tempEntity);
|
|
this.tempEntity = null;
|
|
}
|
|
if (this.drawingPoints.length > 1) {
|
|
this.finishLineDrawing();
|
|
} else {
|
|
// 取消绘制时,移除所有点实体
|
|
this.drawingPointEntities.forEach(pointEntity => {
|
|
this.viewer.entities.remove(pointEntity);
|
|
});
|
|
this.drawingPointEntities = [];
|
|
this.cancelDrawing();
|
|
}
|
|
// 重置鼠标位置
|
|
this.activeCursorPosition = null;
|
|
}, Cesium.ScreenSpaceEventType.RIGHT_CLICK);
|
|
},
|
|
finishLineDrawing() {
|
|
// 将预览线段转换为最终线段
|
|
if (this.drawingPoints.length > 1) {
|
|
// 移除预览线段
|
|
if (this.tempPreviewEntity) {
|
|
this.viewer.entities.remove(this.tempPreviewEntity);
|
|
this.tempPreviewEntity = null;
|
|
}
|
|
// 创建最终的实线实体
|
|
const entity = this.addLineEntity([...this.drawingPoints], [...this.drawingPointEntities]);
|
|
// 计算长度
|
|
const length = this.calculateLineLength([...this.drawingPoints]);
|
|
// 不显示测量面板,使用小框提示
|
|
// this.measurementResult = {
|
|
// distance: length,
|
|
// type: 'line'
|
|
// };
|
|
// 重置绘制点数组,保持绘制状态以继续绘制
|
|
this.drawingPoints = [];
|
|
this.drawingPointEntities = [];
|
|
this.tempEntity = null;
|
|
return entity;
|
|
} else {
|
|
// 取消绘制时,移除所有点实体
|
|
this.drawingPointEntities.forEach(pointEntity => {
|
|
this.viewer.entities.remove(pointEntity);
|
|
});
|
|
this.drawingPointEntities = [];
|
|
this.cancelDrawing();
|
|
return null;
|
|
}
|
|
},
|
|
// 绘制多边形
|
|
startPolygonDrawing() {
|
|
this.drawingPoints = [];
|
|
// 1. 清理旧实体
|
|
// 移除之前可能遗留的实体,确保干净的画布
|
|
if (this.tempEntity) this.viewer.entities.remove(this.tempEntity);
|
|
if (this.tempPreviewEntity) this.viewer.entities.remove(this.tempPreviewEntity);
|
|
this.tempEntity = null;
|
|
this.tempPreviewEntity = null;
|
|
// 2. 鼠标移动事件:只负责更新坐标变量,不涉及繁重的绘图逻辑
|
|
this.drawingHandler.setInputAction((movement) => {
|
|
const newPosition = this.getClickPosition(movement.endPosition);
|
|
if (newPosition) {
|
|
this.activeCursorPosition = newPosition;
|
|
}
|
|
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
|
|
// 3. 鼠标点击事件:添加关键点
|
|
this.drawingHandler.setInputAction((click) => {
|
|
const position = this.getClickPosition(click.position);
|
|
if (position) {
|
|
this.drawingPoints.push(position);
|
|
// === 关键逻辑:点击第一个点时,创建唯一的“动态多边形” ===
|
|
if (this.drawingPoints.length === 1) {
|
|
this.activeCursorPosition = position; // 初始化鼠标位置
|
|
this.tempEntity = this.viewer.entities.add({
|
|
// --- 填充面配置 ---
|
|
polygon: {
|
|
// hierarchy 使用 CallbackProperty 实现动态填充
|
|
hierarchy: new Cesium.CallbackProperty(() => {
|
|
// 组合:已确定的点 + 当前鼠标位置
|
|
if (this.activeCursorPosition) {
|
|
return new Cesium.PolygonHierarchy([...this.drawingPoints, this.activeCursorPosition]);
|
|
}
|
|
return new Cesium.PolygonHierarchy(this.drawingPoints);
|
|
}, false),
|
|
// 使用半透明颜色,方便看到地图底图
|
|
material: Cesium.Color.fromCssColorString(this.defaultStyles.polygon.color).withAlpha(0.5),
|
|
// 确保贴地
|
|
perPositionHeight: false
|
|
},
|
|
// --- 边框线配置 ---
|
|
polyline: {
|
|
// positions 使用 CallbackProperty 实现动态闭合线
|
|
positions: new Cesium.CallbackProperty(() => {
|
|
if (this.activeCursorPosition) {
|
|
// 闭合回路:[所有点, 鼠标位置, 回到起点]
|
|
return [...this.drawingPoints, this.activeCursorPosition, this.drawingPoints[0]];
|
|
}
|
|
return this.drawingPoints;
|
|
}, false),
|
|
width: this.defaultStyles.line.width,
|
|
// 边框使用虚线,表示正在编辑中
|
|
material: new Cesium.PolylineDashMaterialProperty({
|
|
color: Cesium.Color.fromCssColorString(this.defaultStyles.line.color),
|
|
dashLength: 16
|
|
}),
|
|
clampToGround: true
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
|
// 4. 右键完成绘制
|
|
this.drawingHandler.setInputAction(() => {
|
|
if (this.drawingPoints.length >= 3) {
|
|
this.finishPolygonDrawing(); // 调用原有的完成逻辑
|
|
} else {
|
|
this.cancelDrawing(); // 点数不够则取消
|
|
}
|
|
// 重置状态
|
|
this.activeCursorPosition = null;
|
|
}, Cesium.ScreenSpaceEventType.RIGHT_CLICK);
|
|
},
|
|
finishPolygonDrawing() {
|
|
const positions = [...this.drawingPoints]
|
|
const entity = this.addPolygonEntity(positions)
|
|
// 计算面积
|
|
const area = this.calculatePolygonArea(positions)
|
|
this.measurementResult = {
|
|
area: area,
|
|
type: 'polygon'
|
|
}
|
|
// 重置绘制点数组,保持绘制状态以继续绘制
|
|
this.drawingPoints = []
|
|
if (this.tempEntity) {
|
|
this.viewer.entities.remove(this.tempEntity)
|
|
this.tempEntity = null
|
|
}
|
|
return entity
|
|
},
|
|
// 绘制矩形(优化版:两点定矩形,实时预览)
|
|
startRectangleDrawing() {
|
|
// 重置绘制状态
|
|
this.drawingPoints = []; // 存储起点和终点
|
|
// 1. 清理旧实体
|
|
if (this.tempEntity) this.viewer.entities.remove(this.tempEntity);
|
|
this.tempEntity = null;
|
|
// 2. 鼠标移动事件:更新鼠标位置
|
|
this.drawingHandler.setInputAction((movement) => {
|
|
const newPosition = this.getClickPosition(movement.endPosition);
|
|
if (newPosition) {
|
|
this.activeCursorPosition = newPosition;
|
|
}
|
|
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
|
|
// 3. 鼠标点击事件
|
|
this.drawingHandler.setInputAction((click) => {
|
|
const position = this.getClickPosition(click.position);
|
|
if (position) {
|
|
// --- 情况A:第一次点击(确定起点) ---
|
|
if (this.drawingPoints.length === 0) {
|
|
this.drawingPoints.push(position);
|
|
this.activeCursorPosition = position; // 初始化鼠标位置
|
|
// 创建动态预览矩形
|
|
this.tempEntity = this.viewer.entities.add({
|
|
rectangle: {
|
|
// 关键:使用 CallbackProperty 动态计算矩形范围
|
|
coordinates: new Cesium.CallbackProperty(() => {
|
|
if (this.drawingPoints.length > 0 && this.activeCursorPosition) {
|
|
// 使用 Cesium 内置工具,根据两个点(对角)自动计算矩形范围
|
|
return Cesium.Rectangle.fromCartesianArray([this.drawingPoints[0], this.activeCursorPosition]);
|
|
}
|
|
return Cesium.Rectangle.fromDegrees(0, 0, 0, 0);
|
|
}, false),
|
|
// 样式:半透明填充 + 边框
|
|
material: Cesium.Color.fromCssColorString(this.defaultStyles.polygon.color).withAlpha(0.5),
|
|
outline: true,
|
|
outlineColor: Cesium.Color.fromCssColorString(this.defaultStyles.line.color),
|
|
outlineWidth: 2,
|
|
clampToGround: true // 贴地
|
|
}
|
|
});
|
|
}
|
|
// --- 情况B:第二次点击(确定终点) ---
|
|
else if (this.drawingPoints.length === 1) {
|
|
this.drawingPoints.push(position);
|
|
// 停止监听鼠标移动,因为形状已确定
|
|
this.activeCursorPosition = null;
|
|
// 调用完成逻辑
|
|
this.finishRectangleDrawing();
|
|
}
|
|
}
|
|
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
|
// 4. 右键取消
|
|
this.drawingHandler.setInputAction(() => {
|
|
this.cancelDrawing();
|
|
}, Cesium.ScreenSpaceEventType.RIGHT_CLICK);
|
|
},
|
|
finishRectangleDrawing() {
|
|
// 1. 获取最终的矩形范围对象
|
|
const rect = Cesium.Rectangle.fromCartesianArray(this.drawingPoints);
|
|
// 2. 移除动态预览的临时实体
|
|
if (this.tempEntity) {
|
|
this.viewer.entities.remove(this.tempEntity);
|
|
this.tempEntity = null;
|
|
}
|
|
// 3. 创建最终显示的静态实体
|
|
this.entityCounter++;
|
|
const id = `rectangle_${this.entityCounter}`;
|
|
const finalEntity = this.viewer.entities.add({
|
|
id: id,
|
|
rectangle: {
|
|
coordinates: rect,
|
|
material: Cesium.Color.fromCssColorString(this.defaultStyles.polygon.color).withAlpha(this.defaultStyles.polygon.opacity),
|
|
outline: true,
|
|
outlineColor: Cesium.Color.fromCssColorString(this.defaultStyles.polygon.color),
|
|
outlineWidth: this.defaultStyles.polygon.width,
|
|
clampToGround: true
|
|
}
|
|
});
|
|
// 4. 记录到实体列表
|
|
const entityData = {
|
|
id: id,
|
|
type: 'rectangle',
|
|
points: this.drawingPoints.map(p => this.cartesianToLatLng(p)),
|
|
positions: this.drawingPoints,
|
|
entity: finalEntity,
|
|
color: this.defaultStyles.polygon.color,
|
|
opacity: this.defaultStyles.polygon.opacity,
|
|
width: this.defaultStyles.polygon.width,
|
|
label: `矩形 ${this.entityCounter}`
|
|
};
|
|
this.allEntities.push(entityData);
|
|
// 5. 计算并显示面积
|
|
const area = this.calculateRectangleArea(rect);
|
|
this.measurementResult = {
|
|
area: area,
|
|
type: 'rectangle'
|
|
};
|
|
// 6. 重置状态,保持绘制状态以继续绘制
|
|
this.drawingPoints = [];
|
|
},
|
|
// 计算矩形面积(辅助方法)
|
|
calculateRectangleArea(rectangle) {
|
|
// 获取地球椭球体对象
|
|
const ellipsoid = this.viewer.scene.globe.ellipsoid;
|
|
// 方法一:使用 Cesium 几何管道计算(更精确,但需要特定模块支持)
|
|
// const geometry = new Cesium.RectangleGeometry({ rectangle: rectangle });
|
|
// const geometryInstance = Cesium.RectangleGeometry.createGeometry(geometry);
|
|
// ...比较复杂
|
|
// 方法二:使用采样估算(简单且足够精确)
|
|
// 获取矩形的四个角(弧度)
|
|
const west = rectangle.west;
|
|
const south = rectangle.south;
|
|
const east = rectangle.east;
|
|
const north = rectangle.north;
|
|
// 计算中心点的纬度
|
|
const centerLat = (south + north) / 2;
|
|
// 计算宽度(东西向距离):使用余弦定理校正纬度影响
|
|
// 地球半径约为 6378137 米
|
|
const R = 6378137;
|
|
const width = (east - west) * R * Math.cos(centerLat);
|
|
// 计算高度(南北向距离)
|
|
const height = (north - south) * R;
|
|
// 面积 = 宽 * 高
|
|
const area = Math.abs(width * height);
|
|
return area;
|
|
},
|
|
// 绘制圆形
|
|
startCircleDrawing() {
|
|
// 重置绘制状态
|
|
this.drawingPoints = []; // 存储圆心
|
|
this.activeCursorPosition = null;
|
|
// 1. 清理旧实体
|
|
if (this.tempEntity) {
|
|
try {
|
|
this.viewer.entities.remove(this.tempEntity);
|
|
} catch (e) {
|
|
console.warn('Failed to remove temp entity:', e);
|
|
}
|
|
this.tempEntity = null;
|
|
}
|
|
if (this.tempPreviewEntity) {
|
|
try {
|
|
this.viewer.entities.remove(this.tempPreviewEntity);
|
|
} catch (e) {
|
|
console.warn('Failed to remove temp preview entity:', e);
|
|
}
|
|
this.tempPreviewEntity = null;
|
|
}
|
|
// 2. 重置事件处理器
|
|
this.drawingHandler.removeInputAction(Cesium.ScreenSpaceEventType.MOUSE_MOVE);
|
|
this.drawingHandler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
|
this.drawingHandler.removeInputAction(Cesium.ScreenSpaceEventType.RIGHT_CLICK);
|
|
// 3. 鼠标移动事件
|
|
this.drawingHandler.setInputAction((movement) => {
|
|
try {
|
|
const newPosition = this.getClickPosition(movement.endPosition);
|
|
if (newPosition) {
|
|
this.activeCursorPosition = newPosition;
|
|
}
|
|
} catch (e) {
|
|
console.warn('Mouse move error:', e);
|
|
}
|
|
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
|
|
// 4. 鼠标点击事件
|
|
this.drawingHandler.setInputAction((click) => {
|
|
try {
|
|
const position = this.getClickPosition(click.position);
|
|
if (position) {
|
|
// --- 情况A:第一次点击(确定圆心) ---
|
|
if (this.drawingPoints.length === 0) {
|
|
this.drawingPoints.push(position);
|
|
this.activeCursorPosition = position;
|
|
// 创建动态预览圆形
|
|
if (this.tempEntity) {
|
|
try {
|
|
this.viewer.entities.remove(this.tempEntity);
|
|
} catch (e) {
|
|
console.warn('Failed to remove existing temp entity:', e);
|
|
}
|
|
this.tempEntity = null;
|
|
}
|
|
// 确保有有效的圆心位置
|
|
if (position) {
|
|
// 先创建一个固定半径的临时圆,避免半径为0的情况
|
|
const initialRadius = 100; // 初始半径设为100米
|
|
this.tempEntity = this.viewer.entities.add({
|
|
position: position, // 直接使用确定的圆心位置
|
|
ellipse: {
|
|
// 关键:使用 CallbackProperty 动态计算半径(半长轴和半短轴)
|
|
semiMajorAxis: new Cesium.CallbackProperty(() => {
|
|
try {
|
|
if (this.activeCursorPosition && this.drawingPoints.length > 0 && this.drawingPoints[0]) {
|
|
const center = this.drawingPoints[0];
|
|
const edge = this.activeCursorPosition;
|
|
if (center && edge && typeof center.x === 'number' && typeof edge.x === 'number') {
|
|
const distance = Cesium.Cartesian3.distance(center, edge);
|
|
return isFinite(distance) && distance > 0 ? distance : initialRadius;
|
|
}
|
|
}
|
|
return initialRadius;
|
|
} catch (e) {
|
|
return initialRadius;
|
|
}
|
|
}, false),
|
|
semiMinorAxis: new Cesium.CallbackProperty(() => {
|
|
try {
|
|
if (this.activeCursorPosition && this.drawingPoints.length > 0 && this.drawingPoints[0]) {
|
|
const center = this.drawingPoints[0];
|
|
const edge = this.activeCursorPosition;
|
|
if (center && edge && typeof center.x === 'number' && typeof edge.x === 'number') {
|
|
const distance = Cesium.Cartesian3.distance(center, edge);
|
|
return isFinite(distance) && distance > 0 ? distance : initialRadius;
|
|
}
|
|
}
|
|
return initialRadius;
|
|
} catch (e) {
|
|
return initialRadius;
|
|
}
|
|
}, false),
|
|
// 样式设置
|
|
material: Cesium.Color.fromCssColorString(this.defaultStyles.polygon.color).withAlpha(0.5),
|
|
outline: true,
|
|
outlineColor: Cesium.Color.fromCssColorString(this.defaultStyles.line.color),
|
|
outlineWidth: 2,
|
|
// height: 0, // 如果需要贴地可开启或使用 heightReference
|
|
}
|
|
});
|
|
}
|
|
}
|
|
// --- 情况B:第二次点击(确定边缘/半径) ---
|
|
else if (this.drawingPoints.length === 1) {
|
|
// 记录边缘点(虽然圆只需要圆心和半径,但记录下来方便后续处理)
|
|
this.drawingPoints.push(position);
|
|
this.activeCursorPosition = null; // 停止动态更新
|
|
// 传递边缘点位置去结束绘制
|
|
this.finishCircleDrawing(position);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.warn('Mouse click error:', e);
|
|
}
|
|
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
|
// 5. 右键取消
|
|
this.drawingHandler.setInputAction(() => {
|
|
try {
|
|
this.cancelDrawing();
|
|
} catch (e) {
|
|
console.warn('Right click error:', e);
|
|
}
|
|
}, Cesium.ScreenSpaceEventType.RIGHT_CLICK);
|
|
},
|
|
finishCircleDrawing(edgePosition) {
|
|
const centerPoint = this.drawingPoints[0];
|
|
// 1. 计算最终半径
|
|
const radius = Cesium.Cartesian3.distance(centerPoint, edgePosition);
|
|
// 2. 移除动态预览实体
|
|
if (this.tempEntity) {
|
|
this.viewer.entities.remove(this.tempEntity);
|
|
this.tempEntity = null;
|
|
}
|
|
// 3. 创建最终显示的静态实体
|
|
this.entityCounter++;
|
|
const id = `circle_${this.entityCounter}`;
|
|
const finalEntity = this.viewer.entities.add({
|
|
id: id,
|
|
position: centerPoint,
|
|
ellipse: {
|
|
semiMajorAxis: radius,
|
|
semiMinorAxis: radius,
|
|
material: Cesium.Color.fromCssColorString(this.defaultStyles.polygon.color).withAlpha(this.defaultStyles.polygon.opacity),
|
|
outline: true,
|
|
outlineColor: Cesium.Color.fromCssColorString(this.defaultStyles.polygon.color),
|
|
outlineWidth: this.defaultStyles.polygon.width
|
|
}
|
|
});
|
|
// 4. 记录实体
|
|
const entityData = {
|
|
id: id,
|
|
type: 'circle',
|
|
points: [centerPoint, edgePosition].map(p => this.cartesianToLatLng(p)),
|
|
positions: [centerPoint, edgePosition],
|
|
entity: finalEntity,
|
|
color: this.defaultStyles.polygon.color,
|
|
opacity: this.defaultStyles.polygon.opacity,
|
|
width: this.defaultStyles.polygon.width,
|
|
radius: radius,
|
|
label: `圆形 ${this.entityCounter}`
|
|
};
|
|
this.allEntities.push(entityData);
|
|
// 5. 计算面积 (π * r²) 并显示
|
|
// 半径单位是米,面积单位是平方米
|
|
const area = Math.PI * Math.pow(radius, 2);
|
|
this.measurementResult = {
|
|
radius: radius, // 也可以额外显示半径
|
|
area: area,
|
|
type: 'circle'
|
|
};
|
|
// 6. 重置状态,保持绘制状态以继续绘制
|
|
this.drawingPoints = [];
|
|
this.activeCursorPosition = null;
|
|
},
|
|
// 绘制扇形
|
|
startSectorDrawing() {
|
|
// 重置绘制状态
|
|
this.drawingPoints = []; // 存储圆心、半径端点、角度端点
|
|
// 1. 清理旧实体
|
|
if (this.tempEntity) this.viewer.entities.remove(this.tempEntity);
|
|
if (this.tempPreviewEntity) this.viewer.entities.remove(this.tempPreviewEntity);
|
|
this.tempEntity = null;
|
|
this.tempPreviewEntity = null;
|
|
// 2. 鼠标移动事件
|
|
this.drawingHandler.setInputAction((movement) => {
|
|
const newPosition = this.getClickPosition(movement.endPosition);
|
|
if (newPosition) {
|
|
this.activeCursorPosition = newPosition;
|
|
}
|
|
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
|
|
// 3. 鼠标点击事件
|
|
this.drawingHandler.setInputAction((click) => {
|
|
const position = this.getClickPosition(click.position);
|
|
if (position) {
|
|
// --- 情况A:第一次点击(确定圆心) ---
|
|
if (this.drawingPoints.length === 0) {
|
|
this.drawingPoints.push(position);
|
|
this.activeCursorPosition = position;
|
|
// 创建动态预览半径线
|
|
this.tempPreviewEntity = this.viewer.entities.add({
|
|
polyline: {
|
|
positions: new Cesium.CallbackProperty(() => {
|
|
if (this.drawingPoints.length > 0 && this.activeCursorPosition) {
|
|
return [this.drawingPoints[0], this.activeCursorPosition];
|
|
}
|
|
return [];
|
|
}, false),
|
|
width: this.defaultStyles.sector.width,
|
|
material: new Cesium.PolylineDashMaterialProperty({
|
|
color: Cesium.Color.fromCssColorString(this.defaultStyles.sector.color),
|
|
dashLength: 16
|
|
}),
|
|
clampToGround: true
|
|
}
|
|
});
|
|
}
|
|
// --- 情况B:第二次点击(确定半径) ---
|
|
else if (this.drawingPoints.length === 1) {
|
|
this.drawingPoints.push(position);
|
|
this.activeCursorPosition = position; // 更新 activeCursorPosition 为实际点击位置
|
|
const centerPoint = this.drawingPoints[0];
|
|
const radiusPoint = this.drawingPoints[1];
|
|
const fixedRadius = Cesium.Cartesian3.distance(centerPoint, radiusPoint);
|
|
// 移除半径预览线
|
|
if (this.tempPreviewEntity) {
|
|
this.viewer.entities.remove(this.tempPreviewEntity);
|
|
this.tempPreviewEntity = null;
|
|
}
|
|
// 创建动态预览扇形
|
|
this.tempEntity = this.viewer.entities.add({
|
|
polygon: {
|
|
hierarchy: new Cesium.CallbackProperty(() => {
|
|
if (this.drawingPoints.length > 1 && this.activeCursorPosition) {
|
|
const centerPoint = this.drawingPoints[0];
|
|
const radiusPoint = this.drawingPoints[1];
|
|
if (!isFinite(fixedRadius) || fixedRadius === 0) {
|
|
return new Cesium.PolygonHierarchy([]);
|
|
}
|
|
const startAngle = this.calculatePointAngle(centerPoint, radiusPoint);
|
|
const endAngle = this.calculatePointAngle(centerPoint, this.activeCursorPosition);
|
|
const positions = this.generateSectorPositions(centerPoint, fixedRadius, startAngle, endAngle);
|
|
return new Cesium.PolygonHierarchy(positions);
|
|
}
|
|
return new Cesium.PolygonHierarchy([]);
|
|
}, false),
|
|
material: Cesium.Color.fromCssColorString(this.defaultStyles.sector.color).withAlpha(0.5),
|
|
outline: true,
|
|
outlineColor: Cesium.Color.fromCssColorString(this.defaultStyles.sector.color),
|
|
outlineWidth: this.defaultStyles.sector.width
|
|
}
|
|
});
|
|
}
|
|
// --- 情况C:第三次点击(确定角度) ---
|
|
else if (this.drawingPoints.length === 2) {
|
|
this.drawingPoints.push(position);
|
|
this.activeCursorPosition = null; // 停止动态更新
|
|
// 传递角度点位置去结束绘制
|
|
this.finishSectorDrawing(this.drawingPoints[0], this.drawingPoints[1], this.drawingPoints[2]);
|
|
}
|
|
}
|
|
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
|
// 4. 右键取消
|
|
this.drawingHandler.setInputAction(() => {
|
|
this.cancelDrawing();
|
|
}, Cesium.ScreenSpaceEventType.RIGHT_CLICK);
|
|
},
|
|
// 完成扇形绘制
|
|
finishSectorDrawing(centerPoint, radiusPoint, anglePoint) {
|
|
const radius = Cesium.Cartesian3.distance(centerPoint, radiusPoint);
|
|
const startAngle = this.calculatePointAngle(centerPoint, radiusPoint);
|
|
const endAngle = this.calculatePointAngle(centerPoint, anglePoint);
|
|
// 1. 移除动态预览实体
|
|
if (this.tempEntity) {
|
|
this.viewer.entities.remove(this.tempEntity);
|
|
this.tempEntity = null;
|
|
}
|
|
// 2. 生成扇形顶点
|
|
const positions = this.generateSectorPositions(centerPoint, radius, startAngle, endAngle);
|
|
// 3. 创建最终显示的静态实体
|
|
const finalEntity = this.viewer.entities.add({
|
|
id: 'sector-' + new Date().getTime(),
|
|
name: `扇形 ${this.entityCounter}`,
|
|
polygon: {
|
|
hierarchy: new Cesium.PolygonHierarchy(positions),
|
|
material: Cesium.Color.fromCssColorString(this.defaultStyles.sector.color).withAlpha(0.5),
|
|
outline: true,
|
|
outlineColor: Cesium.Color.fromCssColorString(this.defaultStyles.sector.color),
|
|
outlineWidth: this.defaultStyles.sector.width,
|
|
perPositionHeight: false
|
|
}
|
|
});
|
|
// 4. 记录实体
|
|
this.entityCounter++;
|
|
const entityData = {
|
|
id: `sector_${this.entityCounter}`,
|
|
type: 'sector',
|
|
center: this.cartesianToLatLng(centerPoint),
|
|
radius: radius,
|
|
startAngle: startAngle,
|
|
endAngle: endAngle,
|
|
positions: positions,
|
|
entity: finalEntity,
|
|
color: this.defaultStyles.sector.color,
|
|
opacity: 0.5,
|
|
width: this.defaultStyles.sector.width,
|
|
label: `扇形 ${this.entityCounter}`
|
|
};
|
|
this.allEntities.push(entityData);
|
|
// 5. 重置状态,保持绘制状态以继续绘制
|
|
this.drawingPoints = [];
|
|
},
|
|
// 计算角度
|
|
calculateAngle(center, start, end) {
|
|
const startLL = Cesium.Cartographic.fromCartesian(start);
|
|
const endLL = Cesium.Cartographic.fromCartesian(end);
|
|
const centerLL = Cesium.Cartographic.fromCartesian(center);
|
|
// 计算两点相对于圆心的角度
|
|
const startAngle = Math.atan2(startLL.latitude - centerLL.latitude, startLL.longitude - centerLL.longitude);
|
|
const endAngle = Math.atan2(endLL.latitude - centerLL.latitude, endLL.longitude - centerLL.longitude);
|
|
// 返回角度差
|
|
return endAngle - startAngle;
|
|
},
|
|
// 计算角度差
|
|
calculateAngleDiff(center, start, end) {
|
|
const startLL = Cesium.Cartographic.fromCartesian(start);
|
|
const endLL = Cesium.Cartographic.fromCartesian(end);
|
|
const centerLL = Cesium.Cartographic.fromCartesian(center);
|
|
// 计算两点相对于圆心的角度
|
|
const startAngle = Math.atan2(startLL.latitude - centerLL.latitude, startLL.longitude - centerLL.longitude);
|
|
const endAngle = Math.atan2(endLL.latitude - centerLL.latitude, endLL.longitude - centerLL.longitude);
|
|
// 计算角度差(确保为正值)
|
|
let angleDiff = endAngle - startAngle;
|
|
if (angleDiff < 0) {
|
|
angleDiff += 2 * Math.PI;
|
|
}
|
|
// 确保角度差在合理范围内
|
|
return Math.max(0.1, Math.min(Math.PI * 2, angleDiff));
|
|
},
|
|
// 计算点相对于圆心的角度
|
|
calculatePointAngle(center, point) {
|
|
const pointLL = Cesium.Cartographic.fromCartesian(point);
|
|
const centerLL = Cesium.Cartographic.fromCartesian(center);
|
|
// 计算点相对于圆心的角度
|
|
const angle = Math.atan2(pointLL.latitude - centerLL.latitude, pointLL.longitude - centerLL.longitude);
|
|
return angle;
|
|
},
|
|
// 生成扇形顶点位置
|
|
generateSectorPositions(center, radius, startAngle, endAngle) {
|
|
const positions = [];
|
|
const centerLL = Cesium.Cartographic.fromCartesian(center);
|
|
// 添加圆心
|
|
positions.push(center);
|
|
// 计算角度差(顺时针方向)
|
|
let angleDiff = startAngle - endAngle;
|
|
if (angleDiff < 0) {
|
|
angleDiff += 2 * Math.PI;
|
|
}
|
|
// 确保角度差不为零
|
|
angleDiff = Math.max(0.01, angleDiff);
|
|
// 计算扇形的顶点数(根据角度差确定,确保平滑)
|
|
const numPoints = Math.max(5, Math.ceil(angleDiff * 180 / Math.PI / 10));
|
|
const angleStep = angleDiff / (numPoints - 1);
|
|
// 生成扇形的顶点(顺时针方向)
|
|
for (let i = 0; i < numPoints; i++) {
|
|
const currentAngle = startAngle - i * angleStep;
|
|
const distance = radius / 6378137; // 转换为弧度
|
|
const lat = centerLL.latitude + Math.sin(currentAngle) * distance;
|
|
const lng = centerLL.longitude + Math.cos(currentAngle) * distance / Math.cos(centerLL.latitude);
|
|
const position = Cesium.Cartesian3.fromRadians(lng, lat);
|
|
positions.push(position);
|
|
}
|
|
// 闭合扇形
|
|
positions.push(center);
|
|
return positions;
|
|
},
|
|
// 计算两点之间的距离(米)
|
|
calculateDistance(point1, point2) {
|
|
return Cesium.Cartesian3.distance(point1, point2);
|
|
},
|
|
// 绘制箭头
|
|
startArrowDrawing() {
|
|
this.drawingPoints = []; // 存储起点和终点
|
|
// 1. 清理旧实体
|
|
if (this.tempEntity) this.viewer.entities.remove(this.tempEntity);
|
|
if (this.tempPreviewEntity) this.viewer.entities.remove(this.tempPreviewEntity);
|
|
this.tempEntity = null;
|
|
this.tempPreviewEntity = null;
|
|
// 2. 鼠标移动事件
|
|
this.drawingHandler.setInputAction((movement) => {
|
|
const newPosition = this.getClickPosition(movement.endPosition);
|
|
if (newPosition) {
|
|
this.activeCursorPosition = newPosition;
|
|
}
|
|
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
|
|
// 3. 鼠标点击事件
|
|
this.drawingHandler.setInputAction((click) => {
|
|
const position = this.getClickPosition(click.position);
|
|
if (position) {
|
|
this.drawingPoints.push(position);
|
|
// --- 情况A:第一次点击(确定起点) ---
|
|
if (this.drawingPoints.length === 1) {
|
|
this.activeCursorPosition = position; // 初始化鼠标位置
|
|
// 创建动态预览箭头
|
|
this.tempPreviewEntity = this.viewer.entities.add({
|
|
polyline: {
|
|
// 使用 CallbackProperty 动态获取位置
|
|
positions: new Cesium.CallbackProperty(() => {
|
|
// 只有当有点且鼠标位置存在时才渲染
|
|
if (this.drawingPoints.length > 0 && this.activeCursorPosition) {
|
|
// 获取最后一个已确认的点
|
|
const lastPoint = this.drawingPoints[this.drawingPoints.length - 1];
|
|
// 返回 [最后一个点, 当前鼠标位置]
|
|
return [lastPoint, this.activeCursorPosition];
|
|
}
|
|
return [];
|
|
}, false),
|
|
width: 8, // 增加宽度以获得更大的箭头头部
|
|
// 使用箭头材质
|
|
material: new Cesium.PolylineArrowMaterialProperty(
|
|
Cesium.Color.fromCssColorString(this.defaultStyles.arrow.color)
|
|
),
|
|
clampToGround: true, // 贴地
|
|
widthInMeters: false // 使用像素宽度模式
|
|
}
|
|
});
|
|
}
|
|
// --- 情况B:第二次点击(确定终点) ---
|
|
else {
|
|
// 停止监听鼠标移动,因为形状已确定
|
|
this.activeCursorPosition = null;
|
|
// 调用完成逻辑
|
|
this.finishArrowDrawing();
|
|
}
|
|
}
|
|
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
|
// 4. 右键取消
|
|
this.drawingHandler.setInputAction(() => {
|
|
this.cancelDrawing();
|
|
}, Cesium.ScreenSpaceEventType.RIGHT_CLICK);
|
|
},
|
|
// 完成箭头绘制
|
|
finishArrowDrawing() {
|
|
// 将预览箭头转换为最终箭头
|
|
if (this.drawingPoints.length > 1) {
|
|
// 移除预览箭头
|
|
if (this.tempPreviewEntity) {
|
|
this.viewer.entities.remove(this.tempPreviewEntity);
|
|
this.tempPreviewEntity = null;
|
|
}
|
|
// 创建最终的箭头实体
|
|
const entity = this.addArrowEntity([...this.drawingPoints]);
|
|
// 重置绘制点数组,保持绘制状态以继续绘制
|
|
this.drawingPoints = [];
|
|
return entity;
|
|
} else {
|
|
this.cancelDrawing();
|
|
return null;
|
|
}
|
|
},
|
|
// 添加箭头实体
|
|
addArrowEntity(positions) {
|
|
this.entityCounter++
|
|
const id = `arrow_${this.entityCounter}`
|
|
// 创建箭头实体,使用固定宽度以确保等比例放大
|
|
const entity = this.viewer.entities.add({
|
|
id: id,
|
|
name: `箭头 ${this.entityCounter}`,
|
|
polyline: {
|
|
positions: positions,
|
|
width: 8, // 增加宽度以获得更大的箭头头部
|
|
material: new Cesium.PolylineArrowMaterialProperty(
|
|
Cesium.Color.fromCssColorString(this.defaultStyles.arrow.color)
|
|
),
|
|
clampToGround: true,
|
|
// 使用像素宽度模式,确保箭头在缩放时保持比例
|
|
widthInMeters: false
|
|
}
|
|
})
|
|
const entityData = {
|
|
id,
|
|
type: 'arrow',
|
|
points: positions.map(p => this.cartesianToLatLng(p)),
|
|
positions: positions,
|
|
entity: entity,
|
|
color: this.defaultStyles.arrow.color,
|
|
width: this.defaultStyles.arrow.width,
|
|
label: `箭头 ${this.entityCounter}`
|
|
}
|
|
this.allEntities.push(entityData)
|
|
entity.clickHandler = (e) => {
|
|
this.selectEntity(entityData)
|
|
e.stopPropagation()
|
|
}
|
|
return entityData
|
|
},
|
|
// 绘制文本框
|
|
startTextDrawing() {
|
|
// 1. 清理旧实体
|
|
if (this.tempEntity) this.viewer.entities.remove(this.tempEntity);
|
|
this.tempEntity = null;
|
|
// 2. 鼠标点击事件
|
|
this.drawingHandler.setInputAction((click) => {
|
|
const position = this.getClickPosition(click.position);
|
|
if (position) {
|
|
// 弹出输入框,让用户输入文本内容
|
|
const text = prompt('请输入文本内容:', '文本');
|
|
if (text) {
|
|
// 创建文本实体
|
|
this.addTextEntity(position, text);
|
|
}
|
|
}
|
|
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
|
// 3. 右键取消
|
|
this.drawingHandler.setInputAction(() => {
|
|
this.cancelDrawing();
|
|
}, Cesium.ScreenSpaceEventType.RIGHT_CLICK);
|
|
},
|
|
// 添加文本实体
|
|
addTextEntity(position, text) {
|
|
this.entityCounter++
|
|
const id = `text_${this.entityCounter}`
|
|
// 获取经纬度坐标
|
|
const {lat, lng} = this.cartesianToLatLng(position)
|
|
const entity = this.viewer.entities.add({
|
|
id: id,
|
|
name: `文本 ${this.entityCounter}`,
|
|
position: position,
|
|
label: {
|
|
text: text,
|
|
font: this.defaultStyles.text.font,
|
|
fillColor: Cesium.Color.fromCssColorString(this.defaultStyles.text.color),
|
|
outlineColor: Cesium.Color.BLACK,
|
|
outlineWidth: 2,
|
|
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
|
|
verticalOrigin: Cesium.VerticalOrigin.CENTER,
|
|
horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
|
|
backgroundColor: Cesium.Color.fromCssColorString(this.defaultStyles.text.backgroundColor),
|
|
backgroundPadding: new Cesium.Cartesian2(10, 5),
|
|
pixelOffset: new Cesium.Cartesian2(0, 0),
|
|
// 随地图缩放调整大小
|
|
scaleByDistance: new Cesium.NearFarScalar(
|
|
1000, // 近距离(米)
|
|
1.0, // 近距离时的缩放比例
|
|
500000, // 远距离(米)
|
|
0.1 // 远距离时的缩放比例(不为0,保持可见)
|
|
),
|
|
// 随地图缩放调整透明度
|
|
translucencyByDistance: new Cesium.NearFarScalar(
|
|
1000, // 近距离(米)
|
|
1.0, // 近距离时的透明度
|
|
500000, // 远距离(米)
|
|
0.3 // 远距离时的透明度
|
|
)
|
|
}
|
|
})
|
|
const entityData = {
|
|
id,
|
|
type: 'text',
|
|
lat,
|
|
lng,
|
|
text: text,
|
|
position: position,
|
|
entity: entity,
|
|
color: this.defaultStyles.text.color,
|
|
font: this.defaultStyles.text.font,
|
|
backgroundColor: this.defaultStyles.text.backgroundColor,
|
|
label: `文本 ${this.entityCounter}`
|
|
}
|
|
this.allEntities.push(entityData)
|
|
entity.clickHandler = (e) => {
|
|
this.selectEntity(entityData)
|
|
e.stopPropagation()
|
|
}
|
|
return entityData
|
|
},
|
|
// 绘制图片
|
|
startImageDrawing() {
|
|
// 1. 清理旧实体
|
|
if (this.tempEntity) this.viewer.entities.remove(this.tempEntity);
|
|
this.tempEntity = null;
|
|
// 2. 鼠标点击事件
|
|
this.drawingHandler.setInputAction((click) => {
|
|
const position = this.getClickPosition(click.position);
|
|
if (position) {
|
|
// 创建一个隐藏的文件输入元素
|
|
const fileInput = document.createElement('input');
|
|
fileInput.type = 'file';
|
|
fileInput.accept = 'image/*';
|
|
// 监听文件选择事件
|
|
fileInput.onchange = (e) => {
|
|
const file = e.target.files[0];
|
|
if (file) {
|
|
// 创建 FormData 对象用于上传
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
// 上传文件到服务器
|
|
this.uploadImage(formData, position);
|
|
}
|
|
};
|
|
// 触发文件选择对话框
|
|
fileInput.click();
|
|
}
|
|
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
|
|
// 3. 右键取消
|
|
this.drawingHandler.setInputAction(() => {
|
|
this.cancelDrawing();
|
|
}, Cesium.ScreenSpaceEventType.RIGHT_CLICK);
|
|
},
|
|
// 上传图片
|
|
uploadImage(formData, position) {
|
|
// 显示加载状态
|
|
this.$modal.loading('图片上传中,请稍候...');
|
|
try {
|
|
// 创建请求配置
|
|
const config = {
|
|
headers: {
|
|
'Content-Type': 'multipart/form-data'
|
|
}
|
|
};
|
|
// 发送请求 - 使用项目封装的 request 实例
|
|
request.post('/common/upload', formData, config)
|
|
.then((response) => {
|
|
this.$modal.closeLoading();
|
|
if (response.code === 200) {
|
|
// 上传成功,获取图片 URL
|
|
let imageUrl = response.url;
|
|
// 创建图片实体
|
|
this.addImageEntity(position, imageUrl);
|
|
} else {
|
|
this.$modal.msgError('图片上传失败:' + (response.msg || '未知错误'));
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
this.$modal.closeLoading();
|
|
if (error.response) {
|
|
// 服务器返回错误
|
|
if (error.response.data && error.response.data.msg) {
|
|
this.$modal.msgError('图片上传失败:' + error.response.data.msg);
|
|
} else {
|
|
this.$modal.msgError('图片上传失败:服务器返回错误 ' + error.response.status);
|
|
}
|
|
} else if (error.request) {
|
|
// 请求发送但没有收到响应
|
|
this.$modal.msgError('图片上传失败:无法连接到服务器,请检查网络');
|
|
} else {
|
|
// 请求配置错误
|
|
this.$modal.msgError('图片上传失败:' + error.message);
|
|
}
|
|
});
|
|
} catch (error) {
|
|
this.$modal.closeLoading();
|
|
this.$modal.msgError('图片上传失败:' + error.message);
|
|
}
|
|
},
|
|
// 添加图片实体
|
|
addImageEntity(position, imageUrl) {
|
|
this.entityCounter++
|
|
const id = `image_${this.entityCounter}`
|
|
// 获取经纬度坐标
|
|
const {lat, lng} = this.cartesianToLatLng(position)
|
|
const entity = this.viewer.entities.add({
|
|
id: id,
|
|
name: `图片 ${this.entityCounter}`,
|
|
position: position,
|
|
billboard: {
|
|
image: imageUrl,
|
|
width: this.defaultStyles.image.width,
|
|
height: this.defaultStyles.image.height,
|
|
verticalOrigin: Cesium.VerticalOrigin.CENTER,
|
|
horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
|
|
// 随地图缩放调整大小
|
|
scaleByDistance: new Cesium.NearFarScalar(
|
|
1000, // 近距离(米)
|
|
1.0, // 近距离时的缩放比例
|
|
500000, // 远距离(米)
|
|
0.1 // 远距离时的缩放比例(不为0,保持可见)
|
|
),
|
|
// 随地图缩放调整透明度
|
|
translucencyByDistance: new Cesium.NearFarScalar(
|
|
1000, // 近距离(米)
|
|
1.0, // 近距离时的透明度
|
|
500000, // 远距离(米)
|
|
0.3 // 远距离时的透明度
|
|
)
|
|
}
|
|
})
|
|
const entityData = {
|
|
id,
|
|
type: 'image',
|
|
lat,
|
|
lng,
|
|
imageUrl: imageUrl,
|
|
position: position,
|
|
entity: entity,
|
|
width: this.defaultStyles.image.width,
|
|
height: this.defaultStyles.image.height,
|
|
label: `图片 ${this.entityCounter}`
|
|
}
|
|
this.allEntities.push(entityData)
|
|
entity.clickHandler = (e) => {
|
|
this.selectEntity(entityData)
|
|
e.stopPropagation()
|
|
}
|
|
return entityData
|
|
},
|
|
// ================== 实体创建方法 ==================
|
|
addPointEntity(lat, lng) {
|
|
this.entityCounter++
|
|
const id = `point_${this.entityCounter}`
|
|
const entity = this.viewer.entities.add({
|
|
id: id,
|
|
name: `点 ${this.entityCounter}`,
|
|
position: Cesium.Cartesian3.fromDegrees(lng, lat),
|
|
point: {
|
|
pixelSize: this.defaultStyles.point.size,
|
|
color: Cesium.Color.fromCssColorString(this.defaultStyles.point.color),
|
|
outlineColor: Cesium.Color.WHITE,
|
|
outlineWidth: 2
|
|
},
|
|
label: {
|
|
text: `${this.entityCounter}`,
|
|
font: '14px Arial',
|
|
fillColor: Cesium.Color.WHITE,
|
|
outlineColor: Cesium.Color.BLACK,
|
|
outlineWidth: 2,
|
|
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
|
|
verticalOrigin: Cesium.VerticalOrigin.CENTER,
|
|
horizontalOrigin: Cesium.HorizontalOrigin.CENTER
|
|
}
|
|
})
|
|
const entityData = {
|
|
id,
|
|
type: 'point',
|
|
lat,
|
|
lng,
|
|
entity: entity,
|
|
color: this.defaultStyles.point.color,
|
|
size: this.defaultStyles.point.size,
|
|
label: `点 ${this.entityCounter}`
|
|
}
|
|
this.allEntities.push(entityData)
|
|
// 添加点击事件
|
|
entity.clickHandler = (e) => {
|
|
this.selectEntity(entityData)
|
|
e.stopPropagation()
|
|
}
|
|
return entityData
|
|
},
|
|
addLineEntity(positions, pointEntities = []) {
|
|
this.entityCounter++
|
|
const id = `line_${this.entityCounter}`
|
|
const entity = this.viewer.entities.add({
|
|
id: id,
|
|
name: `线 ${this.entityCounter}`,
|
|
polyline: {
|
|
positions: positions,
|
|
width: this.defaultStyles.line.width,
|
|
material: Cesium.Color.fromCssColorString(this.defaultStyles.line.color),
|
|
clampToGround: true
|
|
}
|
|
})
|
|
const entityData = {
|
|
id,
|
|
type: 'line',
|
|
points: positions.map(p => this.cartesianToLatLng(p)),
|
|
positions: positions,
|
|
entity: entity,
|
|
pointEntities: pointEntities, // 存储点实体
|
|
color: this.defaultStyles.line.color,
|
|
width: this.defaultStyles.line.width,
|
|
label: `线 ${this.entityCounter}`,
|
|
bearingType: 'true' // 默认真方位
|
|
}
|
|
this.allEntities.push(entityData)
|
|
entity.clickHandler = (e) => {
|
|
this.selectEntity(entityData)
|
|
e.stopPropagation()
|
|
}
|
|
return entityData
|
|
},
|
|
addPolygonEntity(positions) {
|
|
this.entityCounter++
|
|
const id = `polygon_${this.entityCounter}`
|
|
// 闭合多边形
|
|
const polygonPositions = [...positions, positions[0]]
|
|
const entity = this.viewer.entities.add({
|
|
id: id,
|
|
name: `面 ${this.entityCounter}`,
|
|
polygon: {
|
|
hierarchy: new Cesium.PolygonHierarchy(polygonPositions),
|
|
material: Cesium.Color.fromCssColorString(this.defaultStyles.polygon.color)
|
|
.withAlpha(this.defaultStyles.polygon.opacity),
|
|
outline: true,
|
|
outlineColor: Cesium.Color.fromCssColorString(this.defaultStyles.polygon.color),
|
|
outlineWidth: this.defaultStyles.polygon.width
|
|
}
|
|
})
|
|
const entityData = {
|
|
id,
|
|
type: 'polygon',
|
|
points: positions.map(p => this.cartesianToLatLng(p)),
|
|
positions: polygonPositions,
|
|
entity: entity,
|
|
color: this.defaultStyles.polygon.color,
|
|
opacity: this.defaultStyles.polygon.opacity,
|
|
width: this.defaultStyles.polygon.width,
|
|
label: `面 ${this.entityCounter}`
|
|
}
|
|
this.allEntities.push(entityData)
|
|
entity.clickHandler = (e) => {
|
|
this.selectEntity(entityData)
|
|
e.stopPropagation()
|
|
}
|
|
return entityData
|
|
},
|
|
addRectangleEntity(coordinates) {
|
|
this.entityCounter++
|
|
const id = `rectangle_${this.entityCounter}`
|
|
const entity = this.viewer.entities.add({
|
|
id: id,
|
|
name: `矩形 ${this.entityCounter}`,
|
|
rectangle: {
|
|
coordinates: coordinates,
|
|
material: Cesium.Color.fromCssColorString(this.defaultStyles.rectangle.color)
|
|
.withAlpha(this.defaultStyles.rectangle.opacity),
|
|
outline: true,
|
|
outlineColor: Cesium.Color.fromCssColorString(this.defaultStyles.rectangle.color),
|
|
outlineWidth: this.defaultStyles.rectangle.width
|
|
}
|
|
})
|
|
// 【重要修改】直接把 entity 推入数组,修复清除功能的 bug
|
|
this.allEntities.push(entity)
|
|
return entity
|
|
},
|
|
addCircleEntity(center, radius) {
|
|
// 确保半径有效
|
|
const validRadius = Math.max(radius, 1)
|
|
this.entityCounter++
|
|
const id = `circle_${this.entityCounter}`
|
|
const entity = this.viewer.entities.add({
|
|
id: id,
|
|
name: `圆形 ${this.entityCounter}`,
|
|
position: center, // 圆心位置
|
|
// 【优化】使用 ellipse (椭圆) 绘制圆形
|
|
ellipse: {
|
|
semiMinorAxis: validRadius, // 半短轴 = 半径
|
|
semiMajorAxis: validRadius, // 半长轴 = 半径
|
|
material: Cesium.Color.fromCssColorString(this.defaultStyles.circle.color)
|
|
.withAlpha(this.defaultStyles.circle.opacity),
|
|
outline: true,
|
|
outlineColor: Cesium.Color.fromCssColorString(this.defaultStyles.circle.color),
|
|
outlineWidth: this.defaultStyles.circle.width
|
|
}
|
|
})
|
|
// 【重要修改】直接把 entity 推入数组
|
|
this.allEntities.push(entity)
|
|
return entity
|
|
},
|
|
// ================== 工具方法 ==================
|
|
getClickPosition(pixelPosition) {
|
|
const cartesian = this.viewer.camera.pickEllipsoid(pixelPosition, this.viewer.scene.globe.ellipsoid)
|
|
return cartesian
|
|
},
|
|
cartesianToLatLng(cartesian) {
|
|
const cartographic = Cesium.Cartographic.fromCartesian(cartesian)
|
|
return {
|
|
lat: Cesium.Math.toDegrees(cartographic.latitude),
|
|
lng: Cesium.Math.toDegrees(cartographic.longitude)
|
|
}
|
|
},
|
|
calculateRectangle(start, end) {
|
|
const startLL = this.cartesianToLatLng(start)
|
|
const endLL = this.cartesianToLatLng(end)
|
|
return Cesium.Rectangle.fromDegrees(
|
|
Math.min(startLL.lng, endLL.lng),
|
|
Math.min(startLL.lat, endLL.lat),
|
|
Math.max(startLL.lng, endLL.lng),
|
|
Math.max(startLL.lat, endLL.lat)
|
|
)
|
|
},
|
|
calculateCircle(center, radius, segments) {
|
|
const positions = []
|
|
const centerLL = Cesium.Cartographic.fromCartesian(center)
|
|
for (let i = 0; i < segments; i++) {
|
|
const angle = (i / segments) * Math.PI * 2
|
|
const lat = centerLL.latitude + (radius / 6378137) * Math.sin(angle)
|
|
const lon = centerLL.longitude + (radius / 6378137) * Math.cos(angle) / Math.cos(centerLL.latitude)
|
|
positions.push(Cesium.Cartesian3.fromRadians(lon, lat))
|
|
}
|
|
return positions
|
|
},
|
|
calculateLineLength(positions) {
|
|
if (!positions || positions.length < 2) {
|
|
return 0;
|
|
}
|
|
let totalLength = 0
|
|
for (let i = 0; i < positions.length - 1; i++) {
|
|
totalLength += Cesium.Cartesian3.distance(positions[i], positions[i + 1])
|
|
}
|
|
return totalLength
|
|
},
|
|
calculateTrueBearing(positions) {
|
|
if (!positions || positions.length < 2) {
|
|
return 0;
|
|
}
|
|
// 获取最后一段线段的两个端点
|
|
const startPos = positions[positions.length - 2];
|
|
const endPos = positions[positions.length - 1];
|
|
|
|
// 将笛卡尔坐标转换为地理坐标
|
|
const startCartographic = Cesium.Cartographic.fromCartesian(startPos);
|
|
const endCartographic = Cesium.Cartographic.fromCartesian(endPos);
|
|
|
|
// 转换为弧度
|
|
const startLat = startCartographic.latitude;
|
|
const startLng = startCartographic.longitude;
|
|
const endLat = endCartographic.latitude;
|
|
const endLng = endCartographic.longitude;
|
|
|
|
// 计算真方位角
|
|
const dLng = endLng - startLng;
|
|
const y = Math.sin(dLng) * Math.cos(endLat);
|
|
const x = Math.cos(startLat) * Math.sin(endLat) - Math.sin(startLat) * Math.cos(endLat) * Math.cos(dLng);
|
|
let bearing = Math.atan2(y, x);
|
|
|
|
// 转换为角度并调整到0-360范围
|
|
bearing = Cesium.Math.toDegrees(bearing);
|
|
return (bearing + 360) % 360;
|
|
},
|
|
// 计算磁偏角(简化模型)
|
|
calculateMagneticDeclination(lat, lng) {
|
|
// 注意:这是一个简化的磁偏角计算模型
|
|
// 实际应用中,应该使用更精确的模型,如 WMM (World Magnetic Model)
|
|
|
|
// 转换为角度
|
|
const latDeg = Cesium.Math.toDegrees(lat);
|
|
const lngDeg = Cesium.Math.toDegrees(lng);
|
|
|
|
// 简化的磁偏角计算(仅适用于中国地区)
|
|
// 实际值会随时间变化,此处为近似值
|
|
let declination;
|
|
|
|
if (lngDeg >= 73 && lngDeg <= 135 && latDeg >= 18 && latDeg <= 53) {
|
|
// 中国地区简化磁偏角模型
|
|
// 基于2020年数据,每年约变化0.1度
|
|
const year = new Date().getFullYear();
|
|
const yearOffset = (year - 2020) * 0.1;
|
|
|
|
if (lngDeg < 100) {
|
|
// 西部地区
|
|
declination = 2.0 - yearOffset;
|
|
} else if (lngDeg < 115) {
|
|
// 中部地区
|
|
declination = 1.0 - yearOffset;
|
|
} else {
|
|
// 东部地区
|
|
declination = 0.5 - yearOffset;
|
|
}
|
|
} else {
|
|
// 其他地区默认磁偏角为0
|
|
declination = 0;
|
|
}
|
|
|
|
return declination;
|
|
},
|
|
// 计算磁方位角
|
|
calculateMagneticBearing(positions) {
|
|
if (!positions || positions.length < 2) {
|
|
return 0;
|
|
}
|
|
|
|
// 计算真方位角
|
|
const trueBearing = this.calculateTrueBearing(positions);
|
|
|
|
// 获取线段起点的地理坐标
|
|
const startPos = positions[positions.length - 2];
|
|
const startCartographic = Cesium.Cartographic.fromCartesian(startPos);
|
|
const startLat = startCartographic.latitude;
|
|
const startLng = startCartographic.longitude;
|
|
|
|
// 计算磁偏角
|
|
const declination = this.calculateMagneticDeclination(startLat, startLng);
|
|
|
|
// 计算磁方位角(真方位角加上磁偏角)
|
|
let magneticBearing = trueBearing + declination;
|
|
|
|
// 调整到0-360范围
|
|
return (magneticBearing + 360) % 360;
|
|
},
|
|
calculatePolygonArea(positions) {
|
|
if (positions.length < 3) return 0
|
|
let area = 0
|
|
const n = positions.length
|
|
for (let i = 0; i < n; i++) {
|
|
const j = (i + 1) % n
|
|
const p1 = Cesium.Cartographic.fromCartesian(positions[i])
|
|
const p2 = Cesium.Cartographic.fromCartesian(positions[j])
|
|
area += p1.longitude * p2.latitude - p2.longitude * p1.latitude
|
|
}
|
|
area = Math.abs(area) * 6378137 * 6378137 / 2
|
|
return area
|
|
},
|
|
// ================== 实体管理 ==================
|
|
selectEntity(entity) {
|
|
this.selectedEntity = entity
|
|
// 居中显示
|
|
if (entity.type === 'point') {
|
|
this.viewer.camera.flyTo({
|
|
destination: Cesium.Cartesian3.fromDegrees(entity.lng, entity.lat, 1000),
|
|
duration: 1.0
|
|
})
|
|
} else if (entity.positions && entity.positions.length > 0) {
|
|
const rectangle = Cesium.Rectangle.fromCartographicArray(
|
|
entity.positions.map(p => Cesium.Cartographic.fromCartesian(p))
|
|
)
|
|
this.viewer.camera.flyTo({
|
|
destination: rectangle,
|
|
duration: 1.0
|
|
})
|
|
}
|
|
},
|
|
updateEntityStyle(entityData = null) {
|
|
const data = entityData || this.selectedEntity
|
|
if (!data || !data.entity) return
|
|
const entity = data.entity
|
|
switch (data.type) {
|
|
case 'point':
|
|
if (entity.point) {
|
|
entity.point.color = Cesium.Color.fromCssColorString(data.color)
|
|
entity.point.pixelSize = data.size
|
|
}
|
|
break
|
|
case 'line':
|
|
if (entity.polyline) {
|
|
entity.polyline.material = Cesium.Color.fromCssColorString(data.color)
|
|
entity.polyline.width = data.width
|
|
}
|
|
break
|
|
case 'polygon':
|
|
if (entity.polygon) {
|
|
entity.polygon.material = Cesium.Color.fromCssColorString(data.color).withAlpha(data.opacity)
|
|
entity.polygon.outlineColor = Cesium.Color.fromCssColorString(data.color)
|
|
entity.polygon.outlineWidth = data.width
|
|
}
|
|
break
|
|
case 'rectangle':
|
|
if (entity.rectangle) {
|
|
entity.rectangle.material = Cesium.Color.fromCssColorString(data.color).withAlpha(data.opacity)
|
|
entity.rectangle.outlineColor = Cesium.Color.fromCssColorString(data.color)
|
|
entity.rectangle.outlineWidth = data.width
|
|
}
|
|
break
|
|
case 'circle':
|
|
if (entity.ellipse) {
|
|
entity.ellipse.material = Cesium.Color.fromCssColorString(data.color).withAlpha(data.opacity)
|
|
entity.ellipse.outlineColor = Cesium.Color.fromCssColorString(data.color)
|
|
entity.ellipse.outlineWidth = data.width
|
|
}
|
|
break
|
|
case 'sector':
|
|
if (entity.polygon) {
|
|
entity.polygon.material = Cesium.Color.fromCssColorString(data.color).withAlpha(data.opacity)
|
|
entity.polygon.outlineColor = Cesium.Color.fromCssColorString(data.color)
|
|
entity.polygon.outlineWidth = data.width
|
|
}
|
|
break
|
|
case 'arrow':
|
|
if (entity.polyline) {
|
|
entity.polyline.material = new Cesium.PolylineArrowMaterialProperty(Cesium.Color.fromCssColorString(data.color))
|
|
entity.polyline.width = data.width
|
|
}
|
|
break
|
|
case 'text':
|
|
if (entity.label) {
|
|
entity.label.fillColor = Cesium.Color.fromCssColorString(data.color)
|
|
entity.label.font = data.font
|
|
}
|
|
break
|
|
}
|
|
},
|
|
updateEntityLabel() {
|
|
if (!this.selectedEntity || !this.selectedEntity.entity) return
|
|
const entity = this.selectedEntity.entity
|
|
entity.name = this.selectedEntity.label
|
|
// 如果是点,更新标签
|
|
if (this.selectedEntity.type === 'point') {
|
|
entity.label.text = this.selectedEntity.label
|
|
}
|
|
},
|
|
deleteSelectedEntity() {
|
|
if (this.selectedEntity) {
|
|
this.removeEntity(this.selectedEntity.id)
|
|
this.selectedEntity = null
|
|
}
|
|
},
|
|
// 从右键菜单删除实体
|
|
deleteEntityFromContextMenu() {
|
|
if (this.contextMenu.entityData) {
|
|
const entityData = this.contextMenu.entityData
|
|
if (entityData.id) {
|
|
this.removeEntity(entityData.id)
|
|
} else if (entityData.entity && entityData.entity.id) {
|
|
this.removeEntity(entityData.entity.id)
|
|
}
|
|
// 隐藏右键菜单
|
|
this.contextMenu.visible = false
|
|
}
|
|
},
|
|
// 更新实体属性
|
|
updateEntityProperty(property, value) {
|
|
if (this.contextMenu.entityData) {
|
|
const entityData = this.contextMenu.entityData
|
|
// 更新实体数据
|
|
entityData[property] = value
|
|
// 更新实体样式
|
|
this.updateEntityStyle(entityData)
|
|
// 隐藏右键菜单
|
|
this.contextMenu.visible = false
|
|
}
|
|
},
|
|
removeEntity(id) {
|
|
// 查找对应的实体数据
|
|
const index = this.allEntities.findIndex(e =>
|
|
e.id === id ||
|
|
(e.entity && e.entity.id === id) ||
|
|
(e.type === 'line' && e.pointEntities && e.pointEntities.some(p => p.id === id))
|
|
)
|
|
if (index > -1) {
|
|
const entity = this.allEntities[index]
|
|
// 从地图中移除
|
|
if (entity instanceof Cesium.Entity) {
|
|
// 情况 A: 直接是 Cesium Entity 对象
|
|
this.viewer.entities.remove(entity)
|
|
} else if (entity.entity) {
|
|
// 情况 B: 包装对象,包含 entity 属性
|
|
this.viewer.entities.remove(entity.entity)
|
|
}
|
|
// 移除线实体相关的点实体
|
|
if (entity.type === 'line' && entity.pointEntities) {
|
|
entity.pointEntities.forEach(pointEntity => {
|
|
this.viewer.entities.remove(pointEntity)
|
|
})
|
|
}
|
|
// 从数组中移除
|
|
this.allEntities.splice(index, 1)
|
|
// 如果删除的是选中的实体,清空选中状态
|
|
if (this.selectedEntity && (this.selectedEntity.id === id || (this.selectedEntity.entity && this.selectedEntity.entity.id === id))) {
|
|
this.selectedEntity = null
|
|
}
|
|
}
|
|
},
|
|
clearAll() {
|
|
// 1. 检查数组是否有内容
|
|
if (this.allEntities && this.allEntities.length > 0) {
|
|
// 2. 遍历每一个对象进行删除
|
|
this.allEntities.forEach(item => {
|
|
try {
|
|
// 情况 A: 数组里存的是原生的 Cesium Entity (点、线通常是这种情况)
|
|
if (item instanceof Cesium.Entity) {
|
|
this.viewer.entities.remove(item);
|
|
}
|
|
// 情况 B: 数组里存的是包装对象 (你的矩形、圆可能是这种 { entity: ... })
|
|
else if (item.entity && item.entity instanceof Cesium.Entity) {
|
|
this.viewer.entities.remove(item.entity);
|
|
}
|
|
// 情况 C: 兜底方案,尝试通过 ID 删除
|
|
else if (item.id) {
|
|
this.viewer.entities.removeById(item.id);
|
|
}
|
|
// 移除线实体相关的点实体
|
|
if (item.type === 'line' && item.pointEntities) {
|
|
item.pointEntities.forEach(pointEntity => {
|
|
this.viewer.entities.remove(pointEntity);
|
|
});
|
|
}
|
|
} catch (e) {
|
|
console.warn('删除实体失败:', e);
|
|
}
|
|
});
|
|
}
|
|
// 3. 清空数组
|
|
this.allEntities = [];
|
|
// 4. 清理可能残留的绘制过程中的临时图形
|
|
if (this.tempEntity) {
|
|
this.viewer.entities.remove(this.tempEntity);
|
|
this.tempEntity = null;
|
|
}
|
|
if (this.tempPreviewEntity) {
|
|
this.viewer.entities.remove(this.tempPreviewEntity);
|
|
this.tempPreviewEntity = null;
|
|
}
|
|
// 5. 重置其他状态(如测量面板、绘制状态)
|
|
this.measurementResult = null;
|
|
this.stopDrawing();
|
|
},
|
|
// ================== 其他方法 ==================
|
|
getTypeName(type) {
|
|
const names = {
|
|
point: '点',
|
|
line: '线',
|
|
polygon: '面',
|
|
rectangle: '矩形',
|
|
circle: '圆形',
|
|
sector: '扇形',
|
|
arrow: '箭头',
|
|
text: '文本',
|
|
image: '图片'
|
|
}
|
|
return names[type] || type
|
|
},
|
|
exportData() {
|
|
const data = {
|
|
version: '1.0',
|
|
date: new Date().toISOString(),
|
|
entities: this.allEntities.map(entity => ({
|
|
id: entity.id,
|
|
type: entity.type,
|
|
label: entity.label,
|
|
color: entity.color,
|
|
data: entity.type === 'point' ? {
|
|
lat: entity.lat,
|
|
lng: entity.lng
|
|
} : entity.type === 'line' || entity.type === 'polygon' ? {
|
|
points: entity.points
|
|
} : entity.type === 'rectangle' ? {
|
|
coordinates: entity.coordinates
|
|
} : {
|
|
center: entity.center,
|
|
radius: entity.radius
|
|
}
|
|
}))
|
|
}
|
|
const blob = new Blob([JSON.stringify(data, null, 2)], {type: 'application/json'})
|
|
const url = URL.createObjectURL(blob)
|
|
const a = document.createElement('a')
|
|
a.href = url
|
|
a.download = `cesium-drawing-${new Date().toISOString().slice(0, 10)}.json`
|
|
a.click()
|
|
URL.revokeObjectURL(url)
|
|
console.log('数据已导出', data)
|
|
},
|
|
importData() {
|
|
const input = document.createElement('input')
|
|
input.type = 'file'
|
|
input.accept = '.json'
|
|
input.onchange = (e) => {
|
|
const file = e.target.files[0]
|
|
if (!file) return
|
|
const reader = new FileReader()
|
|
reader.onload = (event) => {
|
|
try {
|
|
const data = JSON.parse(event.target.result)
|
|
if (data.entities && Array.isArray(data.entities)) {
|
|
data.entities.forEach(entityData => {
|
|
this.importEntity(entityData)
|
|
})
|
|
console.log('数据已导入', data)
|
|
this.$message.success(`成功导入 ${data.entities.length} 个实体`)
|
|
} else {
|
|
this.$message.error('文件格式不正确')
|
|
}
|
|
} catch (error) {
|
|
console.error('导入失败', error)
|
|
this.$message.error('文件解析失败')
|
|
}
|
|
}
|
|
reader.readAsText(file)
|
|
}
|
|
input.click()
|
|
},
|
|
importEntity(entityData) {
|
|
let entity
|
|
const color = entityData.color || '#008aff'
|
|
switch (entityData.type) {
|
|
case 'point':
|
|
entity = this.viewer.entities.add({
|
|
position: Cesium.Cartesian3.fromDegrees(entityData.data.lng, entityData.data.lat),
|
|
point: {
|
|
pixelSize: 10,
|
|
color: Cesium.Color.fromCssColorString(color),
|
|
outlineColor: Cesium.Color.WHITE,
|
|
outlineWidth: 2
|
|
},
|
|
label: {
|
|
text: entityData.label || '点',
|
|
font: '14px sans-serif',
|
|
fillColor: Cesium.Color.WHITE,
|
|
outlineColor: Cesium.Color.BLACK,
|
|
outlineWidth: 2,
|
|
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
|
|
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
|
|
pixelOffset: new Cesium.Cartesian2(0, -10)
|
|
}
|
|
})
|
|
break
|
|
case 'line':
|
|
const linePositions = entityData.data.points.map(p => Cesium.Cartesian3.fromDegrees(p.lng, p.lat))
|
|
entity = this.viewer.entities.add({
|
|
polyline: {
|
|
positions: linePositions,
|
|
width: 3,
|
|
material: Cesium.Color.fromCssColorString(color),
|
|
clampToGround: true
|
|
},
|
|
label: {
|
|
text: entityData.label || '线',
|
|
font: '14px sans-serif',
|
|
fillColor: Cesium.Color.WHITE,
|
|
outlineColor: Cesium.Color.BLACK,
|
|
outlineWidth: 2,
|
|
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
|
|
position: linePositions[0]
|
|
}
|
|
})
|
|
break
|
|
case 'polygon':
|
|
const polygonPositions = entityData.data.points.map(p => Cesium.Cartesian3.fromDegrees(p.lng, p.lat))
|
|
entity = this.viewer.entities.add({
|
|
polygon: {
|
|
hierarchy: polygonPositions,
|
|
material: Cesium.Color.fromCssColorString(color).withAlpha(0.5),
|
|
outline: true,
|
|
outlineColor: Cesium.Color.fromCssColorString(color),
|
|
outlineWidth: 2
|
|
},
|
|
label: {
|
|
text: entityData.label || '面',
|
|
font: '14px sans-serif',
|
|
fillColor: Cesium.Color.WHITE,
|
|
outlineColor: Cesium.Color.BLACK,
|
|
outlineWidth: 2,
|
|
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
|
|
position: polygonPositions[0]
|
|
}
|
|
})
|
|
break
|
|
case 'rectangle':
|
|
const rectCoords = entityData.data.coordinates
|
|
entity = this.viewer.entities.add({
|
|
rectangle: {
|
|
coordinates: Cesium.Rectangle.fromDegrees(rectCoords.west, rectCoords.south, rectCoords.east, rectCoords.north),
|
|
material: Cesium.Color.fromCssColorString(color).withAlpha(0.5),
|
|
outline: true,
|
|
outlineColor: Cesium.Color.fromCssColorString(color),
|
|
outlineWidth: 2
|
|
},
|
|
label: {
|
|
text: entityData.label || '矩形',
|
|
font: '14px sans-serif',
|
|
fillColor: Cesium.Color.WHITE,
|
|
outlineColor: Cesium.Color.BLACK,
|
|
outlineWidth: 2,
|
|
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
|
|
position: Cesium.Cartesian3.fromDegrees((rectCoords.west + rectCoords.east) / 2, (rectCoords.south + rectCoords.north) / 2)
|
|
}
|
|
})
|
|
break
|
|
case 'circle':
|
|
// 检查半径是否有效
|
|
const radius = entityData.data.radius || 1000
|
|
if (radius <= 0) {
|
|
this.$message.error('圆形半径必须大于0')
|
|
return
|
|
}
|
|
entity = this.viewer.entities.add({
|
|
position: Cesium.Cartesian3.fromDegrees(entityData.data.center.lng, entityData.data.center.lat),
|
|
ellipse: {
|
|
semiMinorAxis: radius,
|
|
semiMajorAxis: radius,
|
|
material: Cesium.Color.fromCssColorString(color).withAlpha(0.5),
|
|
outline: true,
|
|
outlineColor: Cesium.Color.fromCssColorString(color),
|
|
outlineWidth: 2
|
|
},
|
|
label: {
|
|
text: entityData.label || '圆形',
|
|
font: '14px sans-serif',
|
|
fillColor: Cesium.Color.WHITE,
|
|
outlineColor: Cesium.Color.BLACK,
|
|
outlineWidth: 2,
|
|
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
|
|
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
|
|
pixelOffset: new Cesium.Cartesian2(0, -10)
|
|
}
|
|
})
|
|
break
|
|
}
|
|
if (entity) {
|
|
this.allEntities.push({
|
|
id: entity.id,
|
|
type: entityData.type,
|
|
label: entityData.label,
|
|
color: color,
|
|
...entityData.data
|
|
})
|
|
}
|
|
},
|
|
handleLocate() {
|
|
this.locateDialogVisible = true
|
|
},
|
|
|
|
handleLocateConfirm(location) {
|
|
const { lng, lat } = location
|
|
if (this.viewer) {
|
|
this.viewer.camera.flyTo({
|
|
destination: Cesium.Cartesian3.fromDegrees(lng, lat, 100000),
|
|
orientation: {
|
|
heading: Cesium.Math.toRadians(0.0),
|
|
pitch: Cesium.Math.toRadians(-90.0),
|
|
roll: 0.0
|
|
},
|
|
duration: 2
|
|
})
|
|
this.$message.success(`已定位到经度 ${lng.toFixed(4)},纬度 ${lat.toFixed(4)}`)
|
|
}
|
|
},
|
|
|
|
handleLocateCancel() {
|
|
this.$message.info('已取消定位')
|
|
},
|
|
updateSelectOptions(refName, dataList, labelField) {
|
|
const select = document.querySelector(`select[ref="${refName}"]`)
|
|
if (!select) return
|
|
|
|
const currentValue = select.value
|
|
select.innerHTML = ''
|
|
|
|
const defaultOption = document.createElement('option')
|
|
defaultOption.value = ''
|
|
defaultOption.textContent = refName === 'routeSelect' ? '请选择航线' : '请选择航点'
|
|
select.appendChild(defaultOption)
|
|
|
|
dataList.forEach(item => {
|
|
const option = document.createElement('option')
|
|
option.value = item.id
|
|
option.textContent = typeof labelField === 'function' ? labelField(item) : item[labelField]
|
|
select.appendChild(option)
|
|
})
|
|
|
|
select.value = currentValue
|
|
},
|
|
initScaleBar() {
|
|
const that = this
|
|
const update = () => {
|
|
that.updateScaleBar()
|
|
}
|
|
update()
|
|
this.viewer.camera.changed.addEventListener(update)
|
|
this.viewer.camera.moveEnd.addEventListener(update)
|
|
this.scaleBarCleanup = () => {
|
|
that.viewer.camera.changed.removeEventListener(update)
|
|
that.viewer.camera.moveEnd.removeEventListener(update)
|
|
}
|
|
},
|
|
/** 根据当前视口计算比例尺(兼容 2D/WebMercator,多位置尝试 pick + 视口宽度备用) */
|
|
getScaleBarInfo() {
|
|
if (!this.viewer || !this.viewer.scene.canvas) return null
|
|
const canvas = this.viewer.scene.canvas
|
|
const w = canvas.clientWidth
|
|
const h = canvas.clientHeight
|
|
if (w <= 0 || h <= 0) return null
|
|
const ellipsoid = this.viewer.scene.globe.ellipsoid
|
|
const camera = this.viewer.camera
|
|
const barPx = 80
|
|
const centerX = w / 2
|
|
// 多组参考点尝试(2D 下 pickEllipsoid 可能只在部分区域有效)
|
|
const tryPoints = [
|
|
[centerX - barPx / 2, h - 50],
|
|
[centerX + barPx / 2, h - 50]
|
|
].concat([
|
|
[centerX - barPx / 2, h / 2],
|
|
[centerX + barPx / 2, h / 2]
|
|
])
|
|
let leftCartesian = null
|
|
let rightCartesian = null
|
|
leftCartesian = camera.pickEllipsoid(new Cesium.Cartesian2(tryPoints[0][0], tryPoints[0][1]), ellipsoid)
|
|
rightCartesian = camera.pickEllipsoid(new Cesium.Cartesian2(tryPoints[1][0], tryPoints[1][1]), ellipsoid)
|
|
if (!leftCartesian || !rightCartesian) {
|
|
leftCartesian = camera.pickEllipsoid(new Cesium.Cartesian2(tryPoints[2][0], tryPoints[2][1]), ellipsoid)
|
|
rightCartesian = camera.pickEllipsoid(new Cesium.Cartesian2(tryPoints[3][0], tryPoints[3][1]), ellipsoid)
|
|
}
|
|
if (leftCartesian && rightCartesian) {
|
|
const rawMeters = Cesium.Cartesian3.distance(leftCartesian, rightCartesian)
|
|
if (rawMeters > 0) {
|
|
const niceMeters = this.niceScaleValue(rawMeters)
|
|
const widthPx = Math.round((niceMeters / rawMeters) * barPx)
|
|
const text = niceMeters >= 1000 ? `${(niceMeters / 1000).toFixed(0)} 公里` : `${Math.round(niceMeters)} 米`
|
|
return { text, widthPx, niceMeters }
|
|
}
|
|
}
|
|
// 2D/WebMercator 备用:用整屏宽度对应的地理范围计算(四角 pick 得到视口矩形)
|
|
const leftBottom = camera.pickEllipsoid(new Cesium.Cartesian2(0, h - 20), ellipsoid)
|
|
const rightBottom = camera.pickEllipsoid(new Cesium.Cartesian2(w, h - 20), ellipsoid)
|
|
if (leftBottom && rightBottom) {
|
|
const widthMeters = Cesium.Cartesian3.distance(leftBottom, rightBottom)
|
|
if (widthMeters > 0) {
|
|
const metersPerPx = widthMeters / w
|
|
const rawMeters = metersPerPx * barPx
|
|
const niceMeters = this.niceScaleValue(rawMeters)
|
|
const widthPx = Math.round(niceMeters / metersPerPx)
|
|
const text = niceMeters >= 1000 ? `${(niceMeters / 1000).toFixed(0)} 公里` : `${Math.round(niceMeters)} 米`
|
|
return { text, widthPx: Math.min(120, Math.max(40, widthPx)), niceMeters }
|
|
}
|
|
}
|
|
// 最后备用:从 2D 相机视锥估算(正交宽度 -> 米)
|
|
if (this.viewer.scene.mode === Cesium.SceneMode.SCENE2D && camera.frustum && camera.frustum.right !== undefined) {
|
|
const frustumWidth = camera.frustum.right - camera.frustum.left
|
|
if (frustumWidth > 0) {
|
|
const metersPerPx = frustumWidth / w
|
|
const rawMeters = metersPerPx * barPx
|
|
const niceMeters = this.niceScaleValue(rawMeters)
|
|
const widthPx = Math.round(niceMeters / metersPerPx)
|
|
const text = niceMeters >= 1000 ? `${(niceMeters / 1000).toFixed(0)} 公里` : `${Math.round(niceMeters)} 米`
|
|
return { text, widthPx: Math.min(120, Math.max(40, widthPx)), niceMeters }
|
|
}
|
|
}
|
|
return null
|
|
},
|
|
/** 将实际距离圆整为易读的刻度值(米) */
|
|
niceScaleValue(meters) {
|
|
const candidates = [10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000, 100000, 200000, 500000]
|
|
let best = candidates[0]
|
|
for (let i = 0; i < candidates.length; i++) {
|
|
if (candidates[i] <= meters * 1.5) best = candidates[i]
|
|
}
|
|
return best
|
|
},
|
|
updateScaleBar() {
|
|
const info = this.getScaleBarInfo()
|
|
if (info) {
|
|
this.scaleBarText = info.text
|
|
this.scaleBarWidthPx = Math.min(120, Math.max(40, info.widthPx))
|
|
} else {
|
|
this.scaleBarText = '--'
|
|
this.scaleBarWidthPx = 80
|
|
}
|
|
this.$nextTick()
|
|
},
|
|
initPointMovement() {
|
|
// 创建屏幕空间事件处理器
|
|
this.pointMovementHandler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas)
|
|
let selectedPoint = null
|
|
let selectedLineEntity = null
|
|
let pointIndex = -1
|
|
let originalCameraController = null
|
|
let isMoving = false
|
|
// 鼠标按下事件:选择点
|
|
this.pointMovementHandler.setInputAction((click) => {
|
|
const pickedObject = this.viewer.scene.pick(click.position)
|
|
if (Cesium.defined(pickedObject) && pickedObject.id) {
|
|
const pickedEntity = pickedObject.id
|
|
// 检查是否点击了点实体
|
|
if (pickedEntity.point) {
|
|
// 查找包含该点的线实体
|
|
for (const lineEntity of this.allEntities) {
|
|
if (lineEntity.type === 'line' && lineEntity.pointEntities) {
|
|
const index = lineEntity.pointEntities.indexOf(pickedEntity)
|
|
if (index !== -1) {
|
|
selectedPoint = pickedEntity
|
|
selectedLineEntity = lineEntity
|
|
pointIndex = index
|
|
isMoving = true
|
|
// 禁用相机控制器,使地图固定
|
|
originalCameraController = this.viewer.scene.screenSpaceCameraController.enableInputs
|
|
this.viewer.scene.screenSpaceCameraController.enableInputs = false
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}, Cesium.ScreenSpaceEventType.LEFT_DOWN)
|
|
// 鼠标移动事件:移动点
|
|
this.pointMovementHandler.setInputAction((movement) => {
|
|
if (isMoving && selectedPoint && selectedLineEntity) {
|
|
const newPosition = this.getClickPosition(movement.endPosition)
|
|
if (newPosition) {
|
|
// 更新点的位置
|
|
selectedPoint.position = newPosition
|
|
// 创建新的位置数组,确保 Cesium 能够检测到变化
|
|
const newPositions = [...selectedLineEntity.positions]
|
|
newPositions[pointIndex] = newPosition
|
|
// 移除旧的线段实体
|
|
this.viewer.entities.remove(selectedLineEntity.entity)
|
|
// 清除所有可能存在的重复线段
|
|
const entitiesToRemove = []
|
|
this.viewer.entities.values.forEach(e => {
|
|
if (e.id && e.id === selectedLineEntity.id) {
|
|
entitiesToRemove.push(e)
|
|
}
|
|
})
|
|
entitiesToRemove.forEach(e => {
|
|
this.viewer.entities.remove(e)
|
|
})
|
|
// 创建新的线段实体
|
|
const newEntity = this.viewer.entities.add({
|
|
id: selectedLineEntity.id,
|
|
name: selectedLineEntity.label,
|
|
polyline: {
|
|
positions: newPositions,
|
|
width: selectedLineEntity.width,
|
|
material: Cesium.Color.fromCssColorString(selectedLineEntity.color),
|
|
clampToGround: true
|
|
}
|
|
})
|
|
// 更新线实体的引用和位置数组
|
|
selectedLineEntity.entity = newEntity
|
|
selectedLineEntity.positions = newPositions
|
|
// 更新点数据
|
|
selectedLineEntity.points[pointIndex] = this.cartesianToLatLng(newPosition)
|
|
// 重新计算距离
|
|
const length = this.calculateLineLength(selectedLineEntity.positions)
|
|
|
|
// 强制刷新地图渲染
|
|
this.viewer.scene.requestRender()
|
|
}
|
|
}
|
|
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE)
|
|
|
|
// 鼠标释放事件:结束移动
|
|
this.pointMovementHandler.setInputAction(() => {
|
|
// 恢复相机控制器
|
|
if (originalCameraController !== null) {
|
|
this.viewer.scene.screenSpaceCameraController.enableInputs = originalCameraController
|
|
originalCameraController = null
|
|
}
|
|
|
|
|
|
isMoving = false
|
|
selectedPoint = null
|
|
selectedLineEntity = null
|
|
pointIndex = -1
|
|
}, Cesium.ScreenSpaceEventType.LEFT_UP)
|
|
},
|
|
|
|
// 初始化鼠标经纬度显示
|
|
initMouseCoordinates() {
|
|
// 创建屏幕空间事件处理器
|
|
this.mouseCoordinatesHandler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas)
|
|
// 鼠标移动事件:更新经纬度
|
|
this.mouseCoordinatesHandler.setInputAction((movement) => {
|
|
const cartesian = this.viewer.camera.pickEllipsoid(movement.endPosition, this.viewer.scene.globe.ellipsoid)
|
|
if (cartesian) {
|
|
const cartographic = Cesium.Cartographic.fromCartesian(cartesian)
|
|
const longitude = Cesium.Math.toDegrees(cartographic.longitude)
|
|
const latitude = Cesium.Math.toDegrees(cartographic.latitude)
|
|
this.coordinatesText = `经度: ${longitude.toFixed(6)}, 纬度: ${latitude.toFixed(6)}`
|
|
} else {
|
|
this.coordinatesText = '经度: --, 纬度: --'
|
|
}
|
|
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE)
|
|
},
|
|
destroyViewer() {
|
|
this.stopDrawing()
|
|
this.clearAll()
|
|
|
|
if (this.pointMovementHandler) {
|
|
this.pointMovementHandler.destroy()
|
|
this.pointMovementHandler = null
|
|
}
|
|
|
|
if (this.rightClickHandler) {
|
|
this.rightClickHandler.destroy()
|
|
this.rightClickHandler = null
|
|
}
|
|
|
|
if (this.mouseCoordinatesHandler) {
|
|
this.mouseCoordinatesHandler.destroy()
|
|
this.mouseCoordinatesHandler = null
|
|
}
|
|
|
|
if (typeof this.scaleBarCleanup === 'function') {
|
|
this.scaleBarCleanup()
|
|
this.scaleBarCleanup = null
|
|
}
|
|
|
|
if (this.viewer) {
|
|
this.viewer.destroy()
|
|
this.viewer = null
|
|
}
|
|
},
|
|
// 查找鼠标悬停在线段的哪一段
|
|
findClosestSegment(positions, mousePosition) {
|
|
if (!positions || positions.length < 2) {
|
|
return -1;
|
|
}
|
|
|
|
let closestDistance = Number.MAX_VALUE;
|
|
let closestSegmentIndex = -1;
|
|
|
|
// 遍历每一段线段
|
|
for (let i = 0; i < positions.length - 1; i++) {
|
|
const startPoint = positions[i];
|
|
const endPoint = positions[i + 1];
|
|
|
|
// 计算鼠标在屏幕上的点到线段的距离
|
|
const distance = this.distanceToSegment(mousePosition, startPoint, endPoint);
|
|
|
|
// 如果距离小于当前最小距离,更新最小距离和对应的段索引
|
|
if (distance < closestDistance) {
|
|
closestDistance = distance;
|
|
closestSegmentIndex = i;
|
|
}
|
|
}
|
|
|
|
// 如果最小距离小于一个阈值,返回对应的段索引
|
|
const threshold = 10; // 像素阈值
|
|
return closestDistance < threshold ? closestSegmentIndex : -1;
|
|
},
|
|
// 计算鼠标在屏幕上的点到线段的距离
|
|
distanceToSegment(mousePosition, startPoint, endPoint) {
|
|
// 将3D点转换为屏幕坐标
|
|
const startScreen = this.viewer.scene.cartesianToCanvasCoordinates(startPoint);
|
|
const endScreen = this.viewer.scene.cartesianToCanvasCoordinates(endPoint);
|
|
|
|
if (!startScreen || !endScreen) {
|
|
return Number.MAX_VALUE;
|
|
}
|
|
|
|
// 计算线段的向量
|
|
const lineVec = [endScreen.x - startScreen.x, endScreen.y - startScreen.y];
|
|
// 计算从线段起点到鼠标点的向量
|
|
const mouseVec = [mousePosition.x - startScreen.x, mousePosition.y - startScreen.y];
|
|
|
|
// 计算线段的长度平方
|
|
const lineLengthSquared = lineVec[0] * lineVec[0] + lineVec[1] * lineVec[1];
|
|
|
|
if (lineLengthSquared === 0) {
|
|
// 线段长度为0,返回鼠标点到线段起点的距离
|
|
return Math.sqrt(mouseVec[0] * mouseVec[0] + mouseVec[1] * mouseVec[1]);
|
|
}
|
|
|
|
// 计算鼠标点在直线上的投影参数t
|
|
const t = Math.max(0, Math.min(1, (mouseVec[0] * lineVec[0] + mouseVec[1] * lineVec[1]) / lineLengthSquared));
|
|
|
|
// 计算投影点
|
|
const projection = [startScreen.x + t * lineVec[0], startScreen.y + t * lineVec[1]];
|
|
|
|
// 计算鼠标点到投影点的距离
|
|
const distanceVec = [mousePosition.x - projection[0], mousePosition.y - projection[1]];
|
|
return Math.sqrt(distanceVec[0] * distanceVec[0] + distanceVec[1] * distanceVec[1]);
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.cesium-container {
|
|
width: 100vw;
|
|
height: 100vh;
|
|
position: relative;
|
|
}
|
|
|
|
#cesiumViewer {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
/* 自定义比例尺样式 */
|
|
:deep(.scale-bar-container) {
|
|
user-select: none;
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* 隐藏Cesium的默认控件 */
|
|
:deep(.cesium-viewer-bottom) {
|
|
display: none !important;
|
|
}
|
|
|
|
:deep(.cesium-credit-logoContainer) {
|
|
display: none !important;
|
|
}
|
|
|
|
:deep(.cesium-credit-textContainer) {
|
|
display: none !important;
|
|
}
|
|
|
|
/* 地图右下角信息面板:比例尺在上、经纬度在下,整体略下移减少遮挡 */
|
|
.map-info-panel {
|
|
position: absolute;
|
|
bottom: 10px;
|
|
right: 12px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-end;
|
|
gap: 6px;
|
|
z-index: 1000;
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* 比例尺:高德风格,浅色底、圆角、刻度线 */
|
|
.scale-bar {
|
|
background: rgba(255, 255, 255, 0.65);
|
|
color: #333;
|
|
padding: 4px 8px 6px;
|
|
border-radius: 4px;
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
min-width: 60px;
|
|
}
|
|
.scale-bar-text {
|
|
font-size: 12px;
|
|
font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif;
|
|
margin-bottom: 4px;
|
|
color: #555;
|
|
}
|
|
.scale-bar-line {
|
|
height: 2px;
|
|
background: #555;
|
|
position: relative;
|
|
flex-shrink: 0;
|
|
}
|
|
.scale-bar-tick {
|
|
position: absolute;
|
|
top: 0;
|
|
width: 2px;
|
|
height: 6px;
|
|
background: #555;
|
|
}
|
|
.scale-bar-tick-left {
|
|
left: 0;
|
|
top: -4px;
|
|
}
|
|
.scale-bar-tick-right {
|
|
right: 0;
|
|
left: auto;
|
|
top: -4px;
|
|
}
|
|
|
|
/* 经纬度显示(在比例尺下方) */
|
|
.map-info-panel .coordinates-display {
|
|
position: static;
|
|
bottom: auto;
|
|
right: auto;
|
|
background-color: rgba(0, 0, 0, 0.7);
|
|
color: white;
|
|
padding: 6px 10px;
|
|
border-radius: 4px;
|
|
font-size: 13px;
|
|
font-family: Arial, sans-serif;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
|
min-width: 200px;
|
|
text-align: right;
|
|
}
|
|
</style>
|
|
|