<template>
|
<div>
|
<el-dialog
|
title="拍摄点编辑"
|
:visible="dialogVisible"
|
append-to-body
|
fullscreen
|
:before-close="handleClose">
|
<el-row :gutter="6">
|
<el-col :span="4">
|
<el-card class="noScroll">
|
<div class="chooseModel">
|
<el-button type="primary" @click='chooseModel'>选择模型</el-button>
|
<div class="modelType">
|
模型名称:{{ modelName }}
|
</div>
|
<div>
|
模型类型:{{ templateType }}
|
</div>
|
<el-form label-width="70px" :model="form">
|
<el-form-item label="模板名称">
|
<el-input v-model="form.templateName"></el-input>
|
</el-form-item>
|
</el-form>
|
</div>
|
<div class="modelTree">
|
<div class='modelTreeTitle'>巡检点目录</div>
|
<el-tree
|
class="filter-tree"
|
:data="treeData"
|
:props="defaultProps"
|
draggable
|
:allow-drop="allowDrop"
|
@node-drop="handleDrop"
|
default-expand-all
|
ref="tree">
|
</el-tree>
|
</div>
|
</el-card>
|
</el-col>
|
<el-col :span="20">
|
<InitMap v-if="showMap" @mergePoint="mergePoint" :towerUrl="towerUrl"></InitMap>
|
</el-col>
|
</el-row>
|
<span slot="footer" class="dialog-footer">
|
<el-button @click="closeModal">取 消</el-button>
|
<el-button type="primary" @click="submit()" >确 定</el-button>
|
</span>
|
</el-dialog>
|
<ChooseModelDialog v-if="showModel" @cancel ='cancel' @getRowData="getRowData" ></ChooseModelDialog>
|
</div>
|
</template>
|
<script>
|
import CesiumMap from "../../../../utils/components/cesium-map.vue";
|
import InitMap from "../../../../utils/components/init-map.vue";
|
import ChooseModelDialog from './chooseModelDialog.vue';
|
import { addPoint} from "@/api/system/template"
|
let globalon = 0
|
let globalat = 0
|
let towerHeight = 47.47
|
const centerCartesian = Cesium.Cartesian3.fromDegrees(globalon,globalat , 0)
|
let rotationAngle = Cesium.Math.toRadians(0)
|
let relativeData
|
export default{
|
name:'shootPointDialog',
|
components: {
|
CesiumMap,
|
ChooseModelDialog,
|
InitMap
|
},
|
data(){
|
return{
|
dialogVisible :true,
|
showModel:false,
|
treeData:[],
|
defaultProps: {
|
children: 'children',
|
label: 'label'
|
},
|
// 相机初始状态
|
initialCameraPosition: null,
|
initialCameraOrientation: null,
|
animationFrameId: null,
|
showMap:false,
|
towerUrl:null,
|
chooseModelId:null,
|
modelName:null,
|
templateType:null,
|
form:{}
|
}
|
},
|
mounted(){
|
},
|
methods:{
|
addPoint(viewer, position, color, label) {
|
viewer.entities.add({
|
name: label,
|
position,
|
point: {
|
pixelSize: 10,
|
color: color,
|
heightReference: Cesium.HeightReference.NONE
|
},
|
label: {
|
text: label,
|
font: '12px sans-serif',
|
pixelOffset: new Cesium.Cartesian2(10, -10),
|
style: Cesium.LabelStyle.FILL,
|
fillColor: color,
|
showBackground: true,
|
}
|
});
|
},
|
drawLine(viewer, positions, color, dashed = false) {
|
viewer.entities.add({
|
polyline: {
|
positions,
|
width: 2,
|
material: dashed
|
? new Cesium.PolylineDashMaterialProperty({
|
color: color,
|
dashLength: 8,
|
})
|
: color,
|
}
|
});
|
},
|
handleClose(){
|
this.dialogVisible = false
|
},
|
chooseModel(){
|
this.showModel = true
|
},
|
closeModal(){
|
this.$emit('close')
|
},
|
cancel(){
|
this.showModel = false
|
},
|
rotateAllPoints(dataList) {
|
return dataList.map(item => {
|
console.log(item)
|
const groundPoint = Cesium.Cartesian3.fromDegrees(item.longitude, item.latitude, item.height);
|
const towerPoint = Cesium.Cartesian3.fromDegrees(0, 0, 0);
|
|
// 旋转地面点本身(会变,但绕自身旋转不会移动)
|
const newGroundPoint = this.rotateAroundPoint( groundPoint,towerPoint, rotationAngle);
|
const newGroundCarto = Cesium.Cartographic.fromCartesian(newGroundPoint);
|
const newGround = {
|
...item,
|
longitude: Cesium.Math.toDegrees(newGroundCarto.longitude),
|
latitude: Cesium.Math.toDegrees(newGroundCarto.latitude),
|
height: newGroundCarto.height
|
};
|
console.log(newGround)
|
// 处理子空中点
|
newGround.children = item.children.map(child => {
|
const airPoint = Cesium.Cartesian3.fromDegrees(child.longitude, child.latitude, child.height);
|
const rotatedAirPoint = this.rotateAroundPoint( airPoint,towerPoint, rotationAngle);
|
const rotatedAirCarto = Cesium.Cartographic.fromCartesian(rotatedAirPoint);
|
|
return {
|
...child,
|
longitude: Cesium.Math.toDegrees(rotatedAirCarto.longitude),
|
latitude: Cesium.Math.toDegrees(rotatedAirCarto.latitude),
|
height: rotatedAirCarto.height
|
};
|
});
|
|
return newGround;
|
});
|
},
|
mergePoint(arr,mergeNumber,viewer){
|
this.treeData = arr
|
const result = this.replaceCloseChildrenWithHighestPoint(this.treeData,mergeNumber);
|
// const updated = this.insertRaisedPoints(result, 0,0, 236, 130);
|
const updated = this.insertRaisedPoints(result, globalon, globalat, towerHeight);
|
this.treeData = updated
|
const airPoints = []
|
this.treeData.forEach(item => {
|
const ground = item
|
const air = item.children[0]
|
|
// 地面点到空中点 — 白色虚线
|
viewer.entities.add({
|
polyline: {
|
positions: Cesium.Cartesian3.fromDegreesArrayHeights([
|
ground.longitude, ground.latitude, ground.height,
|
air.longitude, air.latitude, air.height
|
]),
|
width: 5,
|
material: new Cesium.PolylineDashMaterialProperty({
|
color: Cesium.Color.RED,
|
dashLength: 16.0
|
})
|
}
|
})
|
|
airPoints.push(air)
|
})
|
if (airPoints.length > 1) {
|
const positions = airPoints.map(p => [p.longitude, p.latitude, p.height]).flat()
|
viewer.entities.add({
|
polyline: {
|
positions: Cesium.Cartesian3.fromDegreesArrayHeights(positions),
|
width: 5,
|
material: Cesium.Color.RED
|
}
|
})
|
}
|
console.log(this.treeData)
|
},
|
//数组 塔的精度 塔的纬度 塔的高度 半径
|
insertRaisedPoints(data, centerLon, centerLat, centerHeight, radius = 10) {
|
const center = Cesium.Cartesian3.fromDegrees(centerLon, centerLat, centerHeight);
|
const result = JSON.parse(JSON.stringify(data)); // 深拷贝避免污染原数据
|
const inserts = []; // 用于存储插入项及其目标位置
|
|
for (let i = 0; i < result.length - 1; i++) {
|
const pointA = result[i].children?.[0];
|
const pointB = result[i + 1].children?.[0];
|
|
if (!pointA || !pointB) continue;
|
|
const cartA = Cesium.Cartesian3.fromDegrees(pointA.longitude, pointA.latitude, pointA.height);
|
const cartB = Cesium.Cartesian3.fromDegrees(pointB.longitude, pointB.latitude, pointB.height);
|
|
const closest = this.closestPointOnSegment(cartA, cartB, center);
|
const distance = Cesium.Cartesian3.distance(closest, center);
|
console.log(distance)
|
if (distance < radius) {
|
// 对第 i 个点插入新数据
|
const groundA = result[i];
|
const raisedA = {
|
id: this.generateId(),
|
label: groundA.label+ "-加高",
|
longitude: groundA.longitude,
|
latitude: groundA.latitude,
|
height: groundA.height,
|
children: [
|
{
|
...pointA,
|
id: this.generateId(),
|
label: pointA.label + "-加高",
|
height: pointA.height + 50
|
}
|
]
|
};
|
inserts.push({ index: i + 1, item: raisedA });
|
|
// 对第 i+1 个点插入新数据
|
const groundB = result[i + 1];
|
const raisedB = {
|
id: this.generateId(),
|
label: groundB.label,
|
longitude: groundB.longitude,
|
latitude: groundB.latitude,
|
height: groundB.height,
|
children: [
|
{
|
...pointB,
|
id: this.generateId(),
|
label: pointB.label + "-加高",
|
height: pointB.height + 50
|
}
|
]
|
};
|
inserts.unshift({ index: i + 2, item: raisedB }); // 注意插入后下标需要 +1
|
i++; // 跳过已处理对,避免重复
|
}
|
}
|
|
// 逆序插入,避免下标错位
|
inserts.reverse().forEach(({ index, item }) => {
|
result.splice(index, 0, item);
|
});
|
|
return result;
|
},
|
|
closestPointOnSegment(A, B, P) {
|
const AB = Cesium.Cartesian3.subtract(B, A, new Cesium.Cartesian3());
|
const AP = Cesium.Cartesian3.subtract(P, A, new Cesium.Cartesian3());
|
const ab2 = Cesium.Cartesian3.dot(AB, AB);
|
const ap_ab = Cesium.Cartesian3.dot(AP, AB);
|
const t = Cesium.Math.clamp(ap_ab / ab2, 0.0, 1.0);
|
const result = new Cesium.Cartesian3();
|
Cesium.Cartesian3.multiplyByScalar(AB, t, result);
|
return Cesium.Cartesian3.add(A, result, result);
|
},
|
generateId() {
|
return 'id_' + Math.random().toString(36).substring(2, 10);
|
},
|
replaceCloseChildrenWithHighestPoint(data, maxDistance ) {
|
const childrenPoints = [];
|
|
// 收集所有 children 点及其父引用
|
data.forEach(parent => {
|
parent.children.forEach(child => {
|
childrenPoints.push({
|
parent,
|
child
|
});
|
});
|
});
|
|
const groups = [];
|
|
// 标记是否被分组
|
const visited = new Array(childrenPoints.length).fill(false);
|
|
for (let i = 0; i < childrenPoints.length; i++) {
|
if (visited[i]) continue;
|
|
const group = [childrenPoints[i]];
|
visited[i] = true;
|
|
const pointA = Cesium.Cartesian3.fromDegrees(
|
childrenPoints[i].child.longitude,
|
childrenPoints[i].child.latitude
|
);
|
|
for (let j = i + 1; j < childrenPoints.length; j++) {
|
if (visited[j]) continue;
|
|
const pointB = Cesium.Cartesian3.fromDegrees(
|
childrenPoints[j].child.longitude,
|
childrenPoints[j].child.latitude
|
);
|
|
const distance = Cesium.Cartesian3.distance(pointA, pointB);
|
if (distance <= maxDistance) {
|
group.push(childrenPoints[j]);
|
visited[j] = true;
|
}
|
}
|
|
groups.push(group);
|
}
|
|
// 替换为最高的点
|
groups.forEach(group => {
|
let highest = group[0].child;
|
group.forEach(item => {
|
if (item.child.height > highest.height) {
|
highest = item.child;
|
}
|
});
|
|
// 替换所有该组中的 children
|
group.forEach(item => {
|
item.parent.children = [Object.assign({}, highest)]; // 深拷贝防止引用混乱
|
});
|
});
|
|
return data;
|
},
|
getRowData(row){
|
console.log(row)
|
this.modelName = row.modelName
|
this.templateType = row.modelType
|
this.chooseModelId = row.id
|
towerHeight = row.towerHeight || 47.47
|
this.showMap =false
|
this.$nextTick(()=>{
|
this.towerUrl = row.modelRoute
|
this.showMap = true
|
})
|
},
|
beforeDestroy() {
|
// 清理 RAF 和事件
|
cancelAnimationFrame(this.animationFrameId);
|
document.removeEventListener("keydown", this.onKeyDown);
|
document.removeEventListener("keyup", this.onKeyUp);
|
},
|
allowDrag (draggingNode) {
|
// 例如根节点也允许拖动,这里直接返回 true
|
return true
|
},
|
allowDrop (draggingNode, dropNode, dropType) {
|
if (dropType === 'inner') return false
|
|
const dragParent = draggingNode.parent
|
const dropParent = dropNode.parent
|
|
// 如果都是顶级节点(parent 为 null),允许互换
|
if (!dragParent && !dropParent) {
|
return dropType === 'prev' || dropType === 'next'
|
}
|
|
// 如果有父节点,必须是同一个父节点
|
if (dragParent && dropParent && dragParent === dropParent) {
|
return dropType === 'prev' || dropType === 'next'
|
}
|
|
// 其他情况不允许
|
return false
|
},
|
handleDrop (draggingNode, dropNode, dropType, ev) {
|
console.log('拖拽完成', { draggingNode, dropNode, dropType })
|
},
|
submit(){
|
if(!this.form.templateName){
|
return this.$message({
|
message: '请先输入模板名称再保存',
|
type: 'warning'
|
})
|
}
|
// this.dialogVisible = false
|
// const dealArr = this.submitDealData()
|
const transformed = this.treeData.map((ground, index) => {
|
const pointNumber = index + 1;
|
const child = ground.children?.[0] || {};
|
|
return {
|
altitude: child.height,
|
ardGroundPoint: [
|
{
|
height: ground.height,
|
latitude: ground.latitude,
|
longitude: ground.longitude,
|
pointNumber: 1,
|
targetName: ground.label
|
}
|
],
|
latitude: child.latitude,
|
longitude: child.longitude,
|
pointNumber: pointNumber,
|
targetName: child.label
|
};
|
})
|
console.log(transformed)
|
let parmas = {
|
templateName:this.form.templateName,
|
modelId:this.chooseModelId,
|
ardListWayPointsLS:transformed
|
}
|
addPoint(parmas).then(res=>{
|
console.log(res)
|
if(res.code == 200) {
|
this.$message({
|
message: '新增模板成功',
|
type: 'success'
|
})
|
this.$emit('on-submit')
|
this.$emit('close')
|
}
|
})
|
console.log(parmas)
|
},
|
// 计算点A绕点B逆时针旋转指定角度后的新位置
|
// 空中点或者地面点笛卡尔坐标 塔的笛卡尔坐标 塔的朝向值
|
rotateAroundPoint(startPoint,pivotPoint,rotationAngle) {
|
// 创建一个从B点到本地坐标系的转换矩阵(东方向为X轴,北方向为Y轴,垂直方向为Z轴)
|
const transformationMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(pivotPoint);
|
// 获取世界坐标系到本地坐标系的转换矩阵
|
const inverseTransformationMatrix = Cesium.Matrix4.inverse(transformationMatrix,new Cesium.Matrix4());
|
// 将A点转换到局部坐标系中
|
const localStartPoint = Cesium.Matrix4.multiplyByPoint(inverseTransformationMatrix, startPoint,new Cesium.Cartesian3());
|
// 计算A点在局部坐标系中逆时针旋转指定角度后的新位置
|
const rotatedX = localStartPoint.x * Math.cos(rotationAngle) + localStartPoint.y * Math.sin(rotationAngle);
|
const rotatedY = localStartPoint.y * Math.cos(rotationAngle) - localStartPoint.x * Math.sin(rotationAngle);
|
const rotatedZ = localStartPoint.z; // Z轴坐标保持不变
|
// 将旋转后的局部坐标转换回世界坐标系
|
return Cesium.Matrix4.multiplyByPoint(transformationMatrix, new Cesium.Cartesian3(rotatedX, rotatedY, rotatedZ), new Cesium.Cartesian3());
|
}
|
}
|
}
|
</script>
|
<style scoped>
|
.noScroll{
|
height: calc(100vh - 185px);
|
overflow: auto;
|
position: relative;
|
}
|
.modelType{
|
margin-top: 10px;
|
}
|
.modelTreeTitle {
|
text-align: center;
|
}
|
.chooseModel{
|
height: 120px;
|
/* border: 1px solid #dddddd; */
|
}
|
.cameraView{
|
height: 250px;
|
text-align: center;
|
border: 1px solid #dddddd;
|
}
|
.uavView{
|
height: 250px;
|
text-align: center;
|
border: 1px solid #dddddd;
|
|
}
|
.operationControl{
|
text-align: center;
|
height: 220px;
|
border: 1px solid #dddddd;
|
|
}
|
.container {
|
text-align: left;
|
/* margin-top: 60px; */
|
font-family: Arial, sans-serif;
|
}
|
|
.keypad {
|
display: inline-block;
|
padding:5px;
|
background: #f2f2f2;
|
border-radius: 16px;
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
}
|
|
.row {
|
display: flex;
|
justify-content: center;
|
margin: 10px 0;
|
}
|
|
.key {
|
width: 60px;
|
height: 60px;
|
margin: 0 10px;
|
background-color: white;
|
border-radius: 10px;
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15);
|
font-size: 12x;
|
font-weight: bold;
|
text-align: center;
|
line-height: 60px;
|
transition: all 0.2s ease;
|
}
|
|
.key.active {
|
background-color: #42b983;
|
color: white;
|
font-size: 12px;
|
box-shadow: 0 4px 15px rgba(66, 185, 131, 0.6);
|
}
|
.arrow-pad {
|
display: inline-block;
|
padding: 10px;
|
float: right;
|
/* margin-left: 50px; */
|
background: #f2f2f2;
|
border-radius: 16px;
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
margin-bottom: 20px;
|
}
|
|
.arrow-row {
|
display: flex;
|
justify-content: center;
|
margin: 5px 0;
|
}
|
|
.arrow {
|
width: 60px;
|
height: 60px;
|
margin: 0 5px;
|
background-color: white;
|
border-radius: 10px;
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15);
|
font-size: 12px;
|
font-weight: bold;
|
text-align: center;
|
line-height: 60px;
|
transition: all 0.2s ease;
|
}
|
|
.arrow.active {
|
background-color: #42b983;
|
color: white;
|
transform: scale(1.1);
|
box-shadow: 0 4px 15px rgba(66, 185, 131, 0.6);
|
}
|
|
.arrow.empty {
|
background: transparent;
|
box-shadow: none;
|
}
|
</style>
|