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, } }