diff --git a/src/App.vue b/src/App.vue index 75d28a8..3258cbe 100644 --- a/src/App.vue +++ b/src/App.vue @@ -9,6 +9,99 @@ import * as egm96 from 'egm96-universal' const store = useStore() const isDraggingSlider = ref(false) const isMinimapExpanded = ref(false) +const canCreateWayline = ref(false) + +// 导出航线 +function exportWayline() { + if (window.wayline) { + const json = window.wayline.exportToJson() + const blob = new Blob([json], { type: 'application/json' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `wayline_${new Date().toISOString().slice(0, 19).replace(/[-:]/g, '')}.json` + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + } +} + +// 导入航线 +function importWayline() { + const input = document.createElement('input') + input.type = 'file' + input.accept = '.json' + input.onchange = (e) => { + const target = e.target as HTMLInputElement + if (target.files && target.files[0]) { + const file = target.files[0] + const reader = new FileReader() + reader.onload = (event) => { + try { + const json = event.target?.result as string + if (window.wayline) { + window.wayline.destroy() + } + window.wayline = Wayline.importFromJson(json, false) + } catch (error) { + console.error('Failed to import wayline:', error) + alert('导入航线失败,请检查文件格式') + } + } + reader.readAsText(file) + } + } + input.click() +} + +// 开始创建航线 +function startCreateWayline() { + canCreateWayline.value = true + // 更改光标样式 + viewer.scene.canvas.style.cursor = 'crosshair' +} + +// 结束创建航线 +function endCreateWayline() { + canCreateWayline.value = false + // 恢复默认光标 + viewer.scene.canvas.style.cursor = 'default' +} + +// 保存航点状态 +function saveWaypointState() { + if (window.wayline && window.wayline.selectedWaypoint) { + const selectedWaypoint = window.wayline.selectedWaypoint + const minimapViewer = window.wayline.minimapViewer + + // 同步偏航角和云台状态 + selectedWaypoint.orientation = { + heading: minimapViewer.camera.heading, + pitch: minimapViewer.camera.pitch, + roll: minimapViewer.camera.roll + } + + // 同步视锥体状态(FOV) + selectedWaypoint.fov = minimapViewer.camera.frustum.fov + + // 同步高度信息 + const cartographic = Cartographic.fromCartesian(selectedWaypoint.position) + selectedWaypoint.alt = cartographic.height + const height = egm96.meanSeaLevel( + cesiumMath.toDegrees(cartographic.latitude), + cesiumMath.toDegrees(cartographic.longitude) + ) + selectedWaypoint.asl = selectedWaypoint.alt - height + + // 更新航点选择状态,触发UI更新 + selectedWaypoint.unselect() + selectedWaypoint.select() + + // 同步到store + window.wayline.syncPointListToStore() + } +} // 声明全局变量类型 declare global { @@ -151,6 +244,8 @@ onMounted(async () => { const scene = viewer.scene const handler = new ScreenSpaceEventHandler(scene.canvas) handler.setInputAction(async function (click: ScreenSpaceEventHandler.PositionedEvent) { + if (!canCreateWayline.value) return + const ray = scene.camera.getPickRay(click.position) if (!ray) { @@ -159,15 +254,20 @@ onMounted(async () => { const cartesian = scene.globe.pick(ray, scene) if (defined(cartesian)) { + // 恢复默认光标 viewer.scene.canvas.style.cursor = 'default' + canCreateWayline.value = false const cartographic = Cartographic.fromCartesian(cartesian) const height = egm96.meanSeaLevel(cesiumMath.toDegrees(cartographic.latitude), cesiumMath.toDegrees(cartographic.longitude)) const terrainPosition = await getTerrainPosition(cartographic) - window.wayline = new Wayline(terrainPosition) + // 直接创建Wayline实例,第一个创建的点就是航点 + window.wayline = new Wayline() + // 将点击位置作为第一个航点 + window.wayline.createWayPoint(terrainPosition) - handler.removeInputAction(ScreenSpaceEventType.LEFT_CLICK) + // 不移除点击事件,因为用户可能需要创建多个航线 } }, ScreenSpaceEventType.LEFT_CLICK) @@ -256,7 +356,7 @@ async function getTerrainPosition(cartographic: Cartographic) {
-
@@ -296,20 +396,15 @@ async function getTerrainPosition(cartographic: Cartographic) {
航点列表 ({{ store.pointList.length }}) -
- - +
@@ -588,6 +683,29 @@ async function getTerrainPosition(cartographic: Cartographic) { color: #2F80ED; } +/* 导入导出按钮 */ +.wayline-import-export { + display: flex; + gap: 6px; +} + +.import-export-btn { + padding: 4px 10px; + border: none; + border-radius: 4px; + background: rgba(0, 214, 144, 0.2); + color: #00D690; + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; +} + +.import-export-btn:hover { + background: rgba(0, 214, 144, 0.3); + color: #00F0A0; +} + /* 顶部中央:镜头模式切换 */ /* 左上角:放大/缩小按钮(按截图) */ .minimap-expand-btn { diff --git a/src/basemap.ts b/src/basemap.ts index 3e018c4..3a328ae 100644 --- a/src/basemap.ts +++ b/src/basemap.ts @@ -20,7 +20,6 @@ export async function createViewer(containerId: string) { viewer.scene.msaaSamples = 4 viewer.scene.globe.depthTestAgainstTerrain = true - viewer.scene.canvas.style.cursor = 'url("/takeoff-active.svg") 16 16, auto' viewer.camera.setView({ destination: new Cartesian3( -2353775.3111441266, diff --git a/src/wayline.ts b/src/wayline.ts index 691eba0..ad7a0be 100644 --- a/src/wayline.ts +++ b/src/wayline.ts @@ -44,43 +44,41 @@ export class Wayline { pointList: WayPoint[] = [] selectedWaypoint: WayPoint | null = null baseHeight: number = 120 - takeOffPoint: Cartesian3 - takeOffHeight: number - top: Cartesian3 minimapViewer!: Viewer debugFrustum!: DebugCameraPrimitive wayLineEntity!: Entity - takeOffPointEntity!: Entity uavEntity!: Entity + editable: boolean - constructor(takeOffPoint: Cartesian3) { - this.takeOffPoint = takeOffPoint - const cartographic = Cartographic.fromCartesian(takeOffPoint) - - this.takeOffHeight = cartographic.height - this.top = Cartesian3.fromRadians( - cartographic.longitude, - cartographic.latitude, - 120 - ) + constructor(editable: boolean = true) { + this.editable = editable this.createMinimap() this.createWayline() - this.createUav() + if (this.editable) { + this.createUav() + } this.setupMiniCameraEvents() - this.createFrustum() + if (this.editable) { + this.createFrustum() + } + + // 设置初始相机位置(使用默认位置) this.minimapViewer.camera.setView({ - destination: this.top, + destination: Cartesian3.fromRadians(0, 0, 10000000), orientation: { pitch: 0, - roll: 0 + roll: 0, + heading: 0 } }) - this.setupDragEvent() - this.setupMovementEvent() - this.setupSelectEvent() + if (this.editable) { + this.setupDragEvent() + this.setupMovementEvent() + this.setupSelectEvent() + } this.syncPointListToStore() // 初始化云台数据 @@ -106,6 +104,9 @@ export class Wayline { createWayPoint(position: Cartesian3): void { const pointList = this.pointList + + // 直接使用相机当前位置作为航点位置,包括高度 + // position参数就是从相机位置获取的,所以直接使用 const wayPoint = new WayPoint(position, this, pointList.length) pointList.push(wayPoint) @@ -488,12 +489,12 @@ export class Wayline { } createWayline(): void { - const { takeOffPoint, pointList } = this + const { pointList } = this this.wayLineEntity = viewer.entities.add({ polyline: { positions: new CallbackProperty(() => { - return [takeOffPoint, this.top, ...pointList.map(item => item.position)] + return pointList.map(item => item.position) }, false), width: 5, material: new CustomMaterialProperty({ @@ -504,14 +505,6 @@ export class Wayline { }) } }) - - this.takeOffPointEntity = viewer.entities.add({ - position: takeOffPoint, - billboard: { - image: './takeoff-active.svg', - scale: 1.0 - } - }) } createUav(): void { @@ -607,14 +600,85 @@ export class Wayline { destroy(): void { viewer.entities.remove(this.wayLineEntity) - viewer.entities.remove(this.takeOffPointEntity) - viewer.scene.primitives.remove(this.debugFrustum) + if (this.editable) { + if (this.debugFrustum) { + viewer.scene.primitives.remove(this.debugFrustum) + } + if (this.uavEntity) { + viewer.entities.remove(this.uavEntity) + } + } for (const point of this.pointList) { (point as any).delete() } } + + exportToJson(): string { + const waylineData = { + pointList: this.pointList.map(point => { + const cartographic = Cartographic.fromCartesian(point.position) + return { + longitude: CesiumMath.toDegrees(cartographic.longitude), + latitude: CesiumMath.toDegrees(cartographic.latitude), + height: cartographic.height, + asl: point.asl, + alt: point.alt, + fov: point.fov, + orientation: { + heading: CesiumMath.toDegrees(point.orientation.heading), + pitch: CesiumMath.toDegrees(point.orientation.pitch), + roll: CesiumMath.toDegrees(point.orientation.roll) + } + } + }) + } + + return JSON.stringify(waylineData, null, 2) + } + + static importFromJson(json: string, editable: boolean = true): Wayline { + try { + const waylineData = JSON.parse(json) + + if (!waylineData.pointList) { + throw new Error('Invalid wayline data format') + } + + const wayline = new Wayline(editable) + + waylineData.pointList.forEach((pointData: any, index: number) => { + if (!pointData.longitude || !pointData.latitude || !pointData.height) { + throw new Error(`Invalid point data at index ${index}`) + } + + const position = Cartesian3.fromRadians( + CesiumMath.toRadians(pointData.longitude), + CesiumMath.toRadians(pointData.latitude), + pointData.height + ) + + const wayPoint = new WayPoint(position, wayline, index) + wayPoint.asl = pointData.asl + wayPoint.alt = pointData.alt + wayPoint.fov = pointData.fov + wayPoint.orientation = { + heading: CesiumMath.toRadians(pointData.orientation.heading), + pitch: CesiumMath.toRadians(pointData.orientation.pitch), + roll: CesiumMath.toRadians(pointData.orientation.roll) + } + + wayline.pointList.push(wayPoint) + }) + + wayline.syncPointListToStore() + return wayline + } catch (error) { + console.error('Failed to import wayline:', error) + throw error + } + } } function getVerticalMetersPerPixel(viewer: Viewer, worldPosition: Cartesian3): number { diff --git a/src/waypoint.ts b/src/waypoint.ts index 363d49d..35233a3 100644 --- a/src/waypoint.ts +++ b/src/waypoint.ts @@ -61,9 +61,8 @@ export class WayPoint { const url = generateNumberSvgDataUrl(index + 1) - this.wayPointEntity = viewer.entities.add({ + const entityOptions: any = { position, - wayPoint: this, point: { pixelSize: 10, color: Color.RED, @@ -86,7 +85,13 @@ export class WayPoint { verticalOrigin: VerticalOrigin.BOTTOM, disableDepthTestDistance: Number.POSITIVE_INFINITY } - }) + } + + if (this.wayline.editable) { + entityOptions.wayPoint = this + } + + this.wayPointEntity = viewer.entities.add(entityOptions) } updatePosition(position: Cartesian3): void { @@ -180,7 +185,6 @@ export class WayPoint { this.selected = false this.wayPointEntity.billboard.image = generateNumberSvgDataUrl(this.index + 1) - if (this.distanceLabel0) viewer.entities.remove(this.distanceLabel0) if (this.distanceLabel1) viewer.entities.remove(this.distanceLabel1) if (this.distanceLabel2) viewer.entities.remove(this.distanceLabel2) if (this.heightLabel) viewer.entities.remove(this.heightLabel) @@ -188,9 +192,6 @@ export class WayPoint { if (this.debugCameraPrimitive) viewer.scene.primitives.remove(this.debugCameraPrimitive) this.wayline.syncPointListToStore() } - distanceLabel0(distanceLabel0: any) { - throw new Error('Method not implemented.') - } delete(): void { const pointList = this.wayline.pointList @@ -262,9 +263,6 @@ export class WayPoint { if (prev) { this.distanceLabel1 = this.createMidpointLabel(prev.position, current.position) - } else { - this.distanceLabel0 = this.createMidpointLabel(this.wayline.takeOffPoint, this.wayline.top) - this.distanceLabel1 = this.createMidpointLabel(this.wayline.top, current.position) } if (next) {