| | |
| | | import * as Cesium from 'cesium' |
| | | import * as turf from '@turf/turf' |
| | | const { VITE_APP_BASE, VITE_APP_ENV, VITE_APP_REGION_URL } = import.meta.env |
| | | import store from '@/store' |
| | | |
| | | const { VITE_APP_BASE, VITE_APP_ENV, VITE_APP_REGION_URL } = import.meta.env |
| | | export const getLngLatDistance = (lat1, lng1, lat2, lng2) => { |
| | | const radLat1 = (lat1 * Math.PI) / 180.0 |
| | | const radLat2 = (lat2 * Math.PI) / 180.0 |
| | |
| | | }) |
| | | const newArr = arr.sort((a, b) => a.distance - b.distance) |
| | | return newArr[0] |
| | | } |
| | | // 获取多边形内部数据 |
| | | export const samplePolygonInterior = (positions, samplingDistance) => { |
| | | if (!positions.length) return [] |
| | | let minLng = Infinity, |
| | | maxLng = -Infinity |
| | | let minLat = Infinity, |
| | | maxLat = -Infinity |
| | | positions.forEach(pos => { |
| | | minLng = Math.min(minLng, pos.lng) |
| | | maxLng = Math.max(maxLng, pos.lng) |
| | | minLat = Math.min(minLat, pos.lat) |
| | | maxLat = Math.max(maxLat, pos.lat) |
| | | }) |
| | | |
| | | // 将 Cesium 多边形格式转为 Turf 格式 |
| | | const turfPolygonCoords = positions.map(pos => [pos.lng, pos.lat]) |
| | | turfPolygonCoords.push([positions[0].lng, positions[0].lat]) // 闭合多边形 |
| | | const turfPolygon = turf.polygon([turfPolygonCoords]) |
| | | |
| | | // 生成网格点并判断是否在多边形内 |
| | | const sampledPoints = [] |
| | | for (let lng = minLng; lng <= maxLng; lng += samplingDistance) { |
| | | for (let lat = minLat; lat <= maxLat; lat += samplingDistance) { |
| | | // 创建 Turf 点 |
| | | const turfPoint = turf.point([lng, lat]) |
| | | // Turf 原生方法判断点是否在多边形内 |
| | | if (turf.booleanPointInPolygon(turfPoint, turfPolygon)) { |
| | | sampledPoints.push({ |
| | | lng: Number(lng.toFixed(6)), |
| | | lat: Number(lat.toFixed(6)), |
| | | height: 0, |
| | | }) |
| | | } |
| | | } |
| | | } |
| | | |
| | | return sampledPoints |
| | | } |
| | | // 获取多边形面内的最高点(相对地形高度) |
| | | export const getPolygonHighestPoint = async (positions, viewer, samplingDistance = 0.0002) => { |
| | | try { |
| | | const interiorPoints = samplePolygonInterior(positions, samplingDistance, viewer) |
| | | const allPoints = [...positions, ...interiorPoints] |
| | | // 批量获取所有点的地形高度 |
| | | const pointsWithTerrain = await getPointPositionsHeight(allPoints, viewer) |
| | | // console.log('所有点的地形高度', pointsWithTerrain) |
| | | |
| | | // 地形高度(ASL)最高的点 |
| | | const highestTerrainPoint = pointsWithTerrain.reduce( |
| | | (max, curr) => (curr.ASL > max.ASL ? curr : max), |
| | | pointsWithTerrain[0] |
| | | ) |
| | | |
| | | // console.log('地形最高点信息:', highestTerrainPoint) |
| | | return highestTerrainPoint |
| | | } catch (error) { |
| | | console.error('获取地形最高点失败:', error) |
| | | return null |
| | | } |
| | | } |
| | | |
| | | // 获取当前经纬度地形数据 |
| | |
| | | if (!Array.isArray(positionsData) || positionsData.length === 0) return |
| | | |
| | | // 如果是一个点,加两个点方便后续生成外包围盒 |
| | | if (positionsData.length === 1){ |
| | | const [lon,lat,height] = positionsData[0] |
| | | positionsData = [[lon+0.001,lat+0.001,height],[lon-0.001,lat-0.001,height],[lon,lat,height]] |
| | | if (positionsData.length === 1) { |
| | | const [lon, lat, height = 0] = positionsData[0] |
| | | positionsData = [ |
| | | [lon + 0.001, lat + 0.001, height], |
| | | [lon - 0.001, lat - 0.001, height], |
| | | [lon, lat, height], |
| | | ] |
| | | } |
| | | const positions = positionsData.map(([lon, lat, height]) => |
| | | const positions = positionsData.map(([lon, lat, height = 0]) => |
| | | Cesium.Cartesian3.fromDegrees(Number(lon), Number(lat), Number(height || 0)) |
| | | ) |
| | | |
| | |
| | | range: boundingSphere.radius * multiple, |
| | | }, |
| | | complete: () => { |
| | | // 飞行完成后检查相机高度是否小于最高点,如果小于直接飞到最高点 |
| | | // 飞行完成后检查相机高度是否小于最高点 |
| | | const cameraHeight = Cesium.Cartographic.fromCartesian(viewer.camera.position).height |
| | | if (cameraHeight < maxHeight) { |
| | | viewer.camera.flyTo({ |
| | |
| | | |
| | | // 辅助函数:创建旋转文字的画布 |
| | | export function createRotatedTextCanvas(time) { |
| | | const prefix = '预计' |
| | | const prefix = '预计到达' |
| | | const suffix = '分钟' |
| | | const canvas = document.createElement('canvas') |
| | | const context = canvas.getContext('2d') |
| | |
| | | context.font = 'bold 16px Source Han Sans CN' |
| | | // 测量各部分文本宽度 |
| | | const prefixWidth = context.measureText(prefix).width |
| | | const timeWidth = context.measureText(time).width |
| | | let timeWidth = context.measureText(time).width |
| | | const timeWidthMore = `${time >= 10 ? 6 : 0}` |
| | | timeWidth = timeWidth + Number(timeWidthMore) |
| | | const suffixWidth = context.measureText(suffix).width |
| | | const totalWidth = prefixWidth + timeWidth + suffixWidth |
| | | const textHeight = 20 // 估算高度 |
| | |
| | | context.textAlign = 'left' // 改为左对齐,方便分段绘制 |
| | | // 清除画布 |
| | | context.clearRect(0, 0, canvas.width, canvas.height) |
| | | // 绘制“预计”(白色) |
| | | // 绘制“预计到达”(白色) |
| | | context.fillStyle = '#FFFFFF' |
| | | context.fillText(prefix, 10, 15) |
| | | // 绘制时间(蓝色) |
| | | context.font = 'bold 30px Source Han Sans CN' |
| | | context.font = 'bold 24px Source Han Sans CN' |
| | | context.fillStyle = '#1EE7E7' // 亮蓝色 |
| | | context.fillText(time, 10 + prefixWidth, 12) |
| | | // 设置宽度 |
| | | context.textBaseline = 'middle' |
| | | context.fillText(time, 10 + prefixWidth, 14) |
| | | // 绘制“分钟”(白色) |
| | | context.font = 'bold 16px Source Han Sans CN' |
| | | context.fillStyle = '#FFFFFF' |
| | | context.fillText(suffix, 20 + prefixWidth + timeWidth, 15) |
| | | context.fillText(suffix, 14 + prefixWidth + timeWidth, 15) |
| | | |
| | | return canvas |
| | | } |
| | | |
| | | |
| | | export const saveCurrentCameraPosition = viewer => { |
| | | const position = viewer?.camera.position.clone() // 相机位置(Cartesian3) |
| | | const heading = viewer?.camera.heading // 航向角(弧度) |
| | | const pitch = viewer?.camera.pitch // 俯仰角(弧度) |
| | | const roll = viewer?.camera.roll // 翻滚角(弧度) |
| | | store.commit('setCameraPosition', { |
| | | duration: 0, |
| | | destination: position, |
| | | orientation: { heading, pitch, roll }, |
| | | }) |
| | | } |
| | | |
| | | // 连接地面线 |
| | | export function getDockPolyLine(data, viewer) { |
| | | if (!data.longitude || !data.latitude) return {} |
| | | return { |
| | | positions: new Cesium.CallbackProperty(() => { |
| | | const pointPosition = Cesium.Cartesian3.fromDegrees(+data.longitude, +data.latitude, +data.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, |
| | | } |
| | | } |
| | | |
| | | // 飞向一个机巢中心 |
| | | export const flyDockCenter = (viewer, info) => { |
| | | const { longitude = 115, latitude = 28, height = 0 } = info |
| | | const position = Cesium.Cartesian3.fromDegrees(+longitude, +latitude, +height) |
| | | const droneEntity = viewer?.entities.add({ position: position, point: { pixelSize: 0 } }) |
| | | viewer?.flyTo(droneEntity, { |
| | | offset: new Cesium.HeadingPitchRange(0, Cesium.Math.toRadians(-60), 22000), |
| | | duration: 0.5, |
| | | complete: () => { |
| | | viewer?.entities.remove(droneEntity) |
| | | }, |
| | | }) |
| | | } |
| | | |
| | | // 在固定的经度上获得高度 |
| | | export function getHeightOnFixedLonLat(ray, pointParams, ellipsoid = Cesium.Ellipsoid.WGS84) { |
| | | const { longitude: lon, latitude: lat } = pointParams |
| | | // 1. 地表点(高度=0) |
| | | const p0 = Cesium.Cartesian3.fromDegrees(lon, lat, 0, ellipsoid) |
| | | |
| | | // 2. 该点处的法线方向(up 向量,单位向量) |
| | | const up = ellipsoid.geodeticSurfaceNormal(p0, new Cesium.Cartesian3()) |
| | | |
| | | // 3. 射线起点和方向 |
| | | const r0 = ray.origin // 相机位置 |
| | | const rd = ray.direction // 射线方向(已归一化?不一定,但不影响) |
| | | |
| | | // 4. 解:求直线 L(t) = p0 + t * up 与射线 R(s) = r0 + s * rd 的最近点 |
| | | // 使用向量公式求两条直线的最近点参数 t |
| | | const w0 = Cesium.Cartesian3.subtract(p0, r0, new Cesium.Cartesian3()) |
| | | |
| | | const a = Cesium.Cartesian3.dot(up, up) // = 1(因为 up 是单位向量) |
| | | const b = Cesium.Cartesian3.dot(up, rd) |
| | | const c = Cesium.Cartesian3.dot(rd, rd) // = |rd|^2 |
| | | const d = Cesium.Cartesian3.dot(up, w0) |
| | | const e = Cesium.Cartesian3.dot(rd, w0) |
| | | |
| | | const denom = a * c - b * b |
| | | let t |
| | | |
| | | if (Math.abs(denom) < Cesium.Math.EPSILON10) { |
| | | // 两直线平行,取 t = 0 |
| | | t = 0 |
| | | } else { |
| | | t = (b * e - c * d) / denom |
| | | } |
| | | |
| | | // 5. 得到高度线上对应的点 |
| | | const resultPoint = Cesium.Cartesian3.add( |
| | | p0, |
| | | Cesium.Cartesian3.multiplyByScalar(up, t, new Cesium.Cartesian3()), |
| | | new Cesium.Cartesian3() |
| | | ) |
| | | |
| | | // 6. 转回 Cartographic 获取精确高度 |
| | | const carto = Cesium.Cartographic.fromCartesian(resultPoint, ellipsoid) |
| | | return { |
| | | cartesian: resultPoint, |
| | | height: carto.height, |
| | | longitude: Cesium.Math.toDegrees(carto.longitude), |
| | | latitude: Cesium.Math.toDegrees(carto.latitude), |
| | | } |
| | | } |