import CreateFrustum from '@/utils/cesium/frustum/CreateFrustum'
|
|
import arrow from '@/assets/gltf/arrow.gltf'
|
import * as Cesium from 'cesium'
|
import { Decimal } from 'decimal.js'
|
import _, { cloneDeep, throttle } from 'lodash'
|
import * as turf from '@turf/turf'
|
import { DRONE_LIST } from '@/const/device'
|
import { cameraModeList } from '@/const/drc'
|
import { getCameraInfoApi } from '@/api/payload'
|
import takeoffImg from '@/assets/images/routePlan/pointAirLine/takeoff.svg'
|
import { cartesian3Convert } from '@/utils/cesium/mapUtil'
|
import { analyzeKmzFile, removeTextKey, XMLToJSON } from '@/utils/cesium/kmz'
|
import { camelToSnake } from '@/utils/util'
|
|
let createFrustum = null
|
let editPoint = null
|
let startPointEntity = null
|
|
// 获取两点的角度
|
export function getAngle (start, end) {
|
let point1 = turf.point([start.longitude, start.latitude])
|
let point2 = turf.point([end.longitude, end.latitude])
|
let attitude_head = turf.bearing(point2, point1)
|
let flyRotateYaw = attitude_head >= 0 ? attitude_head - 180 : 180 + attitude_head
|
return _.round(flyRotateYaw, 2)
|
}
|
|
// 创建或更新椎体
|
export const setFrustum = (params, viewer) => {
|
function hasValue (variable) {
|
return variable !== null && variable !== undefined && variable.toString().trim() !== ''
|
}
|
|
function transformValue (value, type = 1) {
|
let curValue = Number(value)
|
if (type === 1) {
|
let diffValue = curValue === 2 ? 200 : curValue === 200 ? 2 : new Decimal(Number(200)).minus(Number(curValue))
|
return new Decimal(Number(diffValue)).dividedBy(Number(10))
|
}
|
return new Decimal(Number(value)).dividedBy(Number(2)).dividedBy(Number(100))
|
}
|
|
const { longitude, latitude, height, fov = 19.5, heading = 0, pitch = 0 } = params
|
if (hasValue(longitude) && hasValue(latitude) && hasValue(height) && hasValue(fov) && hasValue(heading) && hasValue(pitch)) {
|
createFrustum?.clear()
|
createFrustum = new CreateFrustum(viewer, {
|
position: { longitude, latitude, altitude: height },
|
width: 400,
|
height: 300,
|
near: 0.01,
|
far: 200,
|
fov: transformValue(fov || 19.5),
|
roll: 0,
|
pitch: pitch - 90,
|
heading: heading - 90,
|
})
|
}
|
}
|
|
// 删除椎体
|
export const removeFrustum = () => {
|
createFrustum?.clear()
|
createFrustum = null
|
}
|
|
// 设置箭头
|
export function setArrowPoint (roamPoint, viewer) {
|
if (!(roamPoint.longitude && roamPoint.latitude && roamPoint.height)) return
|
const position = Cesium.Cartesian3.fromDegrees(roamPoint.longitude, roamPoint.latitude, roamPoint.height)
|
const heading = roamPoint.arrowHeading || 0
|
const convertHeading = heading >= 0 ? heading - 180 : heading + 180
|
if (editPoint) {
|
editPoint.position = position
|
editPoint.orientation = Cesium.Transforms.headingPitchRollQuaternion(
|
position,
|
new Cesium.HeadingPitchRoll(
|
Cesium.Math.toRadians(-90 + convertHeading), // 头朝向 (Heading) +90°
|
0, // 俯仰角 (Pitch)
|
0 // 横滚角 (Roll)
|
)
|
)
|
return
|
}
|
|
editPoint = viewer.entities.add({
|
id: 'edit-point',
|
position,
|
model: {
|
uri: arrow,
|
scale: 0.00001, // 放大模型
|
minimumPixelSize: 50, // 让模型在屏幕上最小显示 40px
|
color: Cesium.Color.CORNFLOWERBLUE,
|
colorBlendMode: Cesium.ColorBlendMode.MIX,
|
colorBlendAmount: 0.5,
|
},
|
orientation: Cesium.Transforms.headingPitchRollQuaternion(
|
position,
|
new Cesium.HeadingPitchRoll(
|
Cesium.Math.toRadians(-90 + convertHeading), // 头朝向 (Heading) +90°
|
0, // 俯仰角 (Pitch)
|
0 // 横滚角 (Roll)
|
)
|
),
|
})
|
}
|
|
// 删除箭头
|
export function removeArrowPoint (viewer) {
|
if (editPoint) {
|
viewer.entities.remove(editPoint)
|
editPoint = null
|
}
|
}
|
|
// 获取当前机型支持的摄像头
|
export async function getCameraInfoList (drone_info) {
|
const res = await getCameraInfoApi(drone_info, 2)
|
let list = []
|
let zoomRang = { zoomMax: 56, zoomMin: 2 }
|
res.data?.data?.forEach(item => {
|
if (item.camera_mode === 2) {
|
zoomRang.zoomMax = _.round(item.zoom_factor_max)
|
zoomRang.zoomMin = _.round(item.zoom_factor_min)
|
}
|
if (item.is_storage === 1) {
|
const find = cameraModeList.find(item1 => item1.camera_mode === item.camera_mode)
|
find && list.push({ ...find, ...item })
|
}
|
})
|
return { list, zoomRang }
|
}
|
|
/**
|
* 判断action里面是否有key
|
* @param action
|
* @param key
|
* @returns {{hasKey: boolean, val: *}|{hasKey: boolean, val: null}}
|
*/
|
export function actionHasKey (action, key) {
|
const curActionParams = action?.action_actuator_func_param
|
const val = curActionParams?.[key]
|
if (val !== undefined) {
|
return { hasKey: true, val }
|
} else {
|
return { hasKey: false, val: null }
|
}
|
}
|
|
|
// 处理保存参数
|
export function handleSaveParams ({ pointList, startPoint, globalSettings }) {
|
const isWGS84 = globalSettings.execute_height_mode === 'WGS84'
|
let speak_file_ids = []
|
const { longitude, latitude, height: startPointHeight } = startPoint
|
let point_params = _.cloneDeep(pointList)
|
point_params.shift()
|
point_params = point_params.map(item => {
|
if (!item?.action_modes?.length) item.action_modes = undefined
|
item?.action_modes?.forEach(item1 => {
|
// 如果是gimbalPitchRotate,需要修改为gimbalRotate
|
if (item1.action_actuator_func === 'gimbalPitchRotate') {
|
item1.action_actuator_func = 'gimbalRotate'
|
}
|
// 如果有喊话,需要收集它的id
|
if (item1.action_actuator_func === 'megaphone') {
|
if (item1.action_actuator_func_param.megaphone_operate_type === 0) {
|
speak_file_ids.push(item1.action_actuator_func_param.speak_file_id)
|
}
|
}
|
// 如果有变焦,需要 *24
|
const { hasKey: has_focal_length, val: focal_length } = actionHasKey(item1, 'focal_length')
|
if (has_focal_length && focal_length) {
|
item1.action_actuator_func_param.focal_length = focal_length * 24
|
}
|
})
|
return {
|
...item,
|
insert_waypoint: 0,
|
click_tiles: 0,
|
execute_height: isWGS84 ? item.height : item.height - startPointHeight,
|
}
|
})
|
const payload_info = DRONE_LIST.flatMap(item => item.payloads).find(
|
item => item.value === globalSettings.drone_info
|
)?.payload_info
|
|
const params = _.pick(globalSettings, [
|
'execute_height_mode',
|
'id',
|
'wayline_name',
|
'drone_info',
|
'payload_info',
|
'take_off_security_height',
|
'take_off_ref_point',
|
'auto_flight_speed',
|
'wayline_type',
|
'image_format',
|
])
|
return {
|
...params,
|
point_params,
|
payload_info,
|
speak_file_ids,
|
global_height: isWGS84 ? globalSettings.absoluteHeight : globalSettings.relativeHeight,
|
take_off_ref_point: `${latitude},${longitude},${startPointHeight}`,
|
is_save: '1', //是否存航线库 0会立即飞
|
rth_altitude: 120, //返航高度
|
fly_to_wayline_mode: 'safely',
|
}
|
}
|
|
// 解析kmz文件的时候处理pointList 写在这里
|
export function handlePointListForKmz ({ placemark, startPoint, execute_height_mode, auto_flight_speed }) {
|
if (!placemark?.length) return []
|
const isWGS84 = execute_height_mode === 'WGS84'
|
const list = placemark.map(item => {
|
const [longitude, latitude] = item.point.coordinates.trim().split(',').map(i => _.round(i, 6))
|
const [latitude1, longitude1] = (item?.waypoint_heading_param?.waypoint_poi_point?.trim()?.split(',') || [])
|
.map(i => _.round(i, 6))
|
let action_modes = item?.action_group?.action || []
|
action_modes = Array.isArray(action_modes) ? action_modes : [action_modes]
|
const height = isWGS84 ? item.execute_height : item.execute_height + startPoint.height
|
return {
|
longitude,
|
latitude,
|
height: _.round(height,2),
|
waypoint_speed: item.waypoint_speed,
|
waypoint_poi_point: longitude1 ? { longitude: longitude1, latitude: latitude1 } : undefined,//兴趣点
|
action_modes,
|
}
|
})
|
const pointStart = {
|
...startPoint,
|
flyRotateYaw: 0,
|
pointType: 'start',
|
waypoint_speed: auto_flight_speed,
|
heading: 0, // 椎体 heading
|
arrowHeading: 0, // 飞行器 heading
|
pitch: 0, //椎体俯仰角
|
fov: 19.5, // 这个是椎体的展示截面大小
|
}
|
list.forEach(item => {
|
item?.action_modes?.forEach(item1 => {
|
let { hasKey: has_focal_length, val: focal_length } = actionHasKey(item1, 'focal_length')
|
// 处理focalLength需要/24 focalLength可能是‘1,024’
|
if (has_focal_length && focal_length) {
|
focal_length = typeof focal_length === 'string' ? focal_length.replace(/,/g, '') : focal_length
|
item1.action_actuator_func_param.focal_length = Number(focal_length) / 24
|
}
|
})
|
item.arrowHeading = 0
|
})
|
return [pointStart, ...list]
|
}
|
|
|
export async function analysisPointLineKmz (kmzUrl) {
|
const res = await analyzeKmzFile(`${kmzUrl}?_t=${new Date().getTime()}`)
|
const templateXML = await res.fileInfoObj['wpmz/template.kml']
|
const waylinesXML = await res.fileInfoObj['wpmz/waylines.wpml']
|
const templateXMLJSON = XMLToJSON(templateXML)?.['Document']
|
const waylinesXMLJSON = XMLToJSON(waylinesXML)?.['Document']
|
const templateXMLObj = camelToSnake(removeTextKey(templateXMLJSON))
|
const waylinesXMLObj = camelToSnake(removeTextKey(waylinesXMLJSON))
|
|
const {
|
mission_config: {
|
take_off_ref_point,
|
drone_info: { drone_enum_value, drone_sub_enum_value } = {}
|
} = {},
|
folder: {
|
payload_param: { image_format } = {},
|
placemark: {
|
polygon,
|
margin: buffer_distance_meters = 0
|
},
|
global_height, auto_flight_speed,
|
template_type: templateType
|
} = {},
|
} = templateXMLObj
|
const [latitude, longitude, height] = take_off_ref_point.split(',').map(item => _.round(item, 6))
|
let startPoint = { latitude, longitude, height: Number.isNaN(height) ? 0 : height }
|
|
const {
|
mission_config: { take_off_security_height } = {},
|
folder: polygonPointFolder
|
} = waylinesXMLObj
|
|
let execute_height_mode = '', pointPlacemark
|
|
if (templateType === "mapping3d") {
|
execute_height_mode = polygonPointFolder[0].execute_height_mode
|
pointPlacemark = polygonPointFolder.map(i => i.placemark)
|
} else {
|
execute_height_mode = polygonPointFolder.execute_height_mode
|
pointPlacemark = polygonPointFolder.placemark
|
}
|
// 取出点位
|
const coordinates = polygon?.outer_boundary_is.linear_ring
|
.coordinates?.split('\n') || []
|
// 数组转换
|
let polygonList = coordinates.map((coordinate) =>
|
coordinate
|
.replace(/\s+/g, '')
|
.split(',')
|
.map((v) => Number(v)),
|
)
|
polygonList.pop()
|
const templatePlacemark = templateXMLObj.folder.placemark
|
// 点航线list
|
let pointList = handlePointListForKmz({
|
placemark: polygonPointFolder.placemark,
|
startPoint,
|
execute_height_mode,
|
auto_flight_speed
|
}).map((i,index)=> {
|
// 注入是否使用全局高度参数
|
return {
|
...i,
|
use_global_height: index > 0 ? templatePlacemark[index-1]?.use_global_height : 0
|
}
|
})
|
|
return {
|
image_format,
|
startPoint,
|
global_height,
|
auto_flight_speed,
|
take_off_ref_point,
|
drone_enum_value,
|
drone_sub_enum_value,
|
take_off_security_height,//起飞安全高度
|
execute_height_mode,//执行高度模式
|
pointList,//航点航线 点list
|
templateType,
|
pointPlacemark,
|
polygonList,
|
buffer_distance_meters
|
}
|
}
|
|
// 生成起飞点
|
export function generateStartPoint (movement, viewer) {
|
// 1. 获取笛卡尔坐标
|
const cartesian = viewer.scene.pickPosition(movement.endPosition)
|
if (!cartesian) return
|
// 2. 笛卡尔转为弧度
|
const cartographic = Cesium.Cartographic.fromCartesian(cartesian)
|
const position = Cesium.Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude)
|
// 4. 更新或创建点实体
|
if (startPointEntity) {
|
startPointEntity.position = position
|
return
|
}
|
startPointEntity = viewer.entities.add({
|
position,
|
id: 'wayline-point-start-init',
|
billboard: {
|
image: takeoffImg,
|
outlineWidth: 0,
|
width: 36,
|
height: 36,
|
scale: 1.0,
|
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
|
},
|
})
|
}
|
// 获取起飞点位置
|
export function getStartPointLast (viewer) {
|
const point = cartesian3Convert(startPointEntity.position.getValue(), viewer) //经纬度
|
removeStartPoint(viewer)
|
return point
|
}
|
export function removeStartPoint (viewer) {
|
startPointEntity && viewer.entities.remove(startPointEntity)
|
startPointEntity = null
|
}
|
|
// todo 获取新的云台偏航角(后续做更细致得控制)
|
export function getNewGimbalRollRotateAngle (list, index) {
|
const flyRotateYaw = list[index].flyRotateYaw
|
const actions = list[index].action_modes
|
// 当前点击的点有云台偏航角
|
const hasGimbalRollRotate = actions?.some(item => item?.action_actuator_func_param?.gimbal_yaw_rotate_enable === 1)
|
let value = 0
|
list.slice(0, index + 1).forEach(item => {
|
item?.action_modes?.forEach(item1 => {
|
const param = item1?.action_actuator_func_param
|
const find = param?.gimbal_yaw_rotate_enable === 1
|
if (find) {
|
value = param?.gimbal_yaw_rotate_angle
|
}
|
})
|
})
|
return hasGimbalRollRotate ? value : value + flyRotateYaw
|
}
|
|
// 之前的俯仰角 value
|
export function getBeforeGimbalPitchRotateAngle (list, index) {
|
let value = 0
|
list.slice(0, index + 1).forEach(item => {
|
item?.action_modes?.forEach(item1 => {
|
const param = item1?.action_actuator_func_param
|
const find = param?.gimbal_pitch_rotate_enable === 1
|
if (find) {
|
value = param?.gimbal_pitch_rotate_angle
|
}
|
})
|
})
|
return value
|
}
|
|
|
// 之前的变焦 value
|
export function getBeforeZoomValue (list, index) {
|
let value = 5
|
list.slice(0, index + 1).forEach(item => {
|
item?.action_modes?.forEach(item1 => {
|
const find = item1?.action_actuator_func === 'zoom'
|
if (find) {
|
value = item1?.action_actuator_func_param?.focal_length
|
}
|
})
|
})
|
return value
|
}
|
|
// 创建广告牌
|
export function createBillboard (title, fillColor, isHighlighted = false) {
|
const billboard = document.createElement('canvas')
|
billboard.width = 48
|
billboard.height = 48
|
const ctx = billboard.getContext('2d')
|
|
// 如果选中,绘制阴影
|
if (isHighlighted) {
|
ctx.shadowColor = 'rgba(97, 211, 150, 0.9)' // 阴影颜色(#61d396半透明)
|
ctx.shadowBlur = 12 // 阴影模糊程度
|
ctx.shadowOffsetX = 0 // 阴影水平偏移
|
ctx.shadowOffsetY = 0 // 阴影垂直偏移
|
}
|
|
// 绘制填充区域(三角形)
|
ctx.beginPath()
|
ctx.moveTo(6, 6)
|
ctx.lineTo(42, 6)
|
ctx.lineTo(24, 36)
|
ctx.closePath()
|
ctx.fillStyle = fillColor // 填充颜色
|
ctx.fill()
|
|
// 如果选中,绘制边框
|
if (isHighlighted) {
|
ctx.strokeStyle = 'white' // 边框颜色(#61d396)
|
ctx.lineWidth = 2 // 边框宽度
|
ctx.stroke()
|
}
|
|
// 绘制文字
|
ctx.font = '14px serif'
|
ctx.fillStyle = '#ffffff'
|
ctx.fillText(title, 20, 20)
|
|
return billboard.toDataURL() // 返回图片的DataURL
|
}
|
|
// 检查位置是否在合理范围内
|
export function isValidPosition (cartographic) {
|
// 检查经纬度是否在合理范围内
|
return (
|
cartographic.longitude >= -Math.PI &&
|
cartographic.longitude <= Math.PI &&
|
cartographic.latitude >= -Math.PI / 2 &&
|
cartographic.latitude <= Math.PI / 2
|
)
|
}
|
|
// 计算射线与固定高度平面的交点
|
export function findIntersectionWithHeight (viewer, ray, height, lastGoodPosition) {
|
const globe = viewer.scene.globe
|
|
// 创建一个函数来检查某点是否在目标高度
|
const checkHeight = position => {
|
const cartographic = Cesium.Cartographic.fromCartesian(position)
|
return cartographic.height - height
|
}
|
|
// 如果有上一个有效位置,使用它来优化搜索范围
|
let start = 0
|
let end = 20000000 // 增加搜索范围
|
|
if (lastGoodPosition) {
|
// 计算射线原点到上一个位置的距离作为参考
|
const distance = Cesium.Cartesian3.distance(ray.origin, lastGoodPosition)
|
start = Math.max(0, distance * 0.5)
|
end = distance * 1.5
|
}
|
|
let iterations = 0
|
const maxIterations = 100 // 增加最大迭代次数
|
|
let startPoint = Cesium.Cartesian3.fromElements(
|
ray.origin.x + ray.direction.x * start,
|
ray.origin.y + ray.direction.y * start,
|
ray.origin.z + ray.direction.z * start
|
)
|
|
let endPoint = Cesium.Cartesian3.fromElements(
|
ray.origin.x + ray.direction.x * end,
|
ray.origin.y + ray.direction.y * end,
|
ray.origin.z + ray.direction.z * end
|
)
|
|
let startHeight = checkHeight(startPoint)
|
let endHeight = checkHeight(endPoint)
|
|
// 如果起点和终点高度差异方向不对,调整搜索范围
|
if (startHeight * endHeight > 0) {
|
if (Math.abs(startHeight) < Math.abs(endHeight)) {
|
end = start + (end - start) * 0.1
|
} else {
|
start = end - (end - start) * 0.1
|
}
|
startPoint = Cesium.Cartesian3.fromElements(
|
ray.origin.x + ray.direction.x * start,
|
ray.origin.y + ray.direction.y * start,
|
ray.origin.z + ray.direction.z * start
|
)
|
endPoint = Cesium.Cartesian3.fromElements(
|
ray.origin.x + ray.direction.x * end,
|
ray.origin.y + ray.direction.y * end,
|
ray.origin.z + ray.direction.z * end
|
)
|
startHeight = checkHeight(startPoint)
|
endHeight = checkHeight(endPoint)
|
}
|
|
while (iterations < maxIterations) {
|
const mid = (start + end) / 2
|
const midPoint = Cesium.Cartesian3.fromElements(
|
ray.origin.x + ray.direction.x * mid,
|
ray.origin.y + ray.direction.y * mid,
|
ray.origin.z + ray.direction.z * mid
|
)
|
|
const midHeight = checkHeight(midPoint)
|
|
if (Math.abs(midHeight) < 1.0) {
|
// 增加容差
|
return midPoint
|
}
|
|
if (midHeight * startHeight < 0) {
|
end = mid
|
endPoint = midPoint
|
endHeight = midHeight
|
} else {
|
start = mid
|
startPoint = midPoint
|
startHeight = midHeight
|
}
|
|
iterations++
|
}
|
|
// 如果二分法失败,返回最接近目标高度的点
|
if (Math.abs(startHeight) < Math.abs(endHeight)) {
|
return startPoint
|
} else {
|
return endPoint
|
}
|
}
|
|
// 获取连接地面线
|
export function getPolyLine (viewer, pointList, index) {
|
return {
|
positions: new Cesium.CallbackProperty(() => {
|
const point = pointList.value[index]
|
const pointPosition = Cesium.Cartesian3.fromDegrees(point.longitude, point.latitude, point.height)
|
if (!pointPosition) return []
|
// 获取点的位置
|
const cartographic = Cesium.Cartographic.fromCartesian(pointPosition)
|
// 获取地形高度
|
const terrainHeight = viewer.scene.globe.getHeight(cartographic) || 0
|
// 创建地面点位置
|
const groundPosition = Cesium.Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, terrainHeight)
|
|
return [pointPosition, groundPosition]
|
}, false),
|
width: 0.5,
|
material: Cesium.Color.WHITE,
|
}
|
}
|