utils/Coordinate.js

import { Cesium } from '../../namespace'
import { interPointsByDistance } from './Turf'
/**
 * 空间位置相关方法集
 * @module Coordinate
 */

/**
 * 根据像素px值拾取位置点
 * @method
 * @param {Object} viewer 地图场景
 * @param {Cartesian2} px 屏幕坐标
 * @returns {Cartesian3 | null} 位置点笛卡尔坐标
 */
export const getCatesian3FromPX = (viewer, px) => {
  const scene = viewer.scene
  let picks = scene.drillPick(px)
  let cartesian = undefined
  let isOn3dtiles = false
  for (let i in picks) {
    let pick = picks[i]
    const primitive = pick && pick.primitive
    if (
      primitive instanceof Cesium.Cesium3DTileFeature ||
      primitive instanceof Cesium.Cesium3DTileset ||
      primitive instanceof Cesium.Model
    ) {
      isOn3dtiles = true
    }
    if (isOn3dtiles) {
      scene.pick(px)
      cartesian = scene.pickPosition(px)
      if (cartesian) {
        let cartographic = Cesium.Cartographic.fromCartesian(cartesian)
        let x = Cesium.Math.toDegrees(cartographic.longitude),
          y = Cesium.Math.toDegrees(cartographic.latitude),
          z = cartographic.height
        cartesian = transformWGS84ToCartesian({ x, y, z })
      }
    }
  }
  let isOnTerrain = false // 地形

  let boolTerrain =
    viewer.terrainProvider instanceof Cesium.EllipsoidTerrainProvider
  if (!isOn3dtiles && !boolTerrain) {
    let ray = scene.camera.getPickRay(px)
    if (!ray) return null
    cartesian = scene.globe.pick(ray, scene)
    isOnTerrain = true
  }
  // 地球
  if (!isOn3dtiles && !isOnTerrain && boolTerrain) {
    cartesian = scene.camera.pickEllipsoid(px, scene.globe.ellipsoid)
  }
  if (cartesian) {
    let position = transformCartesianToWGS84(cartesian)
    if (position.z < 0) {
      position.z = 0.1
      cartesian = transformWGS84ToCartesian(position)
    }
    return cartesian
  }
  return null
}

/**
 * WGS84坐标转笛卡尔坐标
 * @method
 * @param {DegreePosZ} position WGS84坐标
 * @returns {Cartesian3} 笛卡尔坐标
 */
export const transformWGS84ToCartesian = (position) => {
  return position
    ? Cesium.Cartesian3.fromDegrees(
        position.x,
        position.y,
        position.z,
        Cesium.Ellipsoid.WGS84
      )
    : Cesium.Cartesian3.ZERO
}

/**
 * 笛卡尔坐标转WGS84
 * @method
 * @param {Cartesian3} cartesian3 笛卡尔坐标
 * @return {DegreePosZ} WGS84坐标
 */
export const transformCartesianToWGS84 = (cartesian3) => {
  let cartographic = Cesium.Ellipsoid.WGS84.cartesianToCartographic(cartesian3)
  return {
    x: Cesium.Math.toDegrees(cartographic.longitude),
    y: Cesium.Math.toDegrees(cartographic.latitude),
    z: cartographic.height,
  }
}

/**
 * 笛卡尔坐标点集合转WGS84点集合
 * @method
 * @param {Array<Cartesian3>} cartesianArr 笛卡尔坐标点集合
 * @returns {Array<DegreePosZ>} WGS84坐标点集合
 */
export const transformCartesianArrayToWGS84Array = (cartesianArr) => {
  return cartesianArr
    ? cartesianArr.map((item) => {
        return transformCartesianToWGS84(item)
      })
    : []
}

/**
 * WGS84坐标转弧度坐标
 * @method
 * @param {DegreePosZ} position WGS84坐标点
 * @return {Cartographic} 弧度坐标点
 */
export const transformWGS84ToCartographic = (position) => {
  return position
    ? Cesium.Cartographic.fromDegrees(position.x, position.y, position.z)
    : Cesium.Cartographic.ZERO
}

/**
 * 线段插值点(地表)
 * @method
 * @param {Object} [viewer] 地图场景
 * @param {String} [type] 插值类型,project-投影点,高度为0;onground-地表点
 * @param {Array<Cartesian3>} [positions] 待插值点集合,笛卡尔坐标数组
 * @param {Array<Object>} [objectsToExclude=null] 高度采集时排除的对象集合
 * @returns {Array<DegreePosZ>} 经纬度点集合
 */
export const interPoints = (
  viewer,
  type,
  positions,
  objectsToExclude = null
) => {
  let positionsCartographic = []
  let terrainSamplePositions = []
  for (let index = 0; index < positions.length; index++) {
    const element = positions[index]
    let ellipsoid = viewer.scene.globe.ellipsoid
    let cartographic = ellipsoid.cartesianToCartographic(element)
    positionsCartographic.push(cartographic)
  }
  for (let i = 0; i < positionsCartographic.length; i++) {
    const m_Cartographic0 = positionsCartographic[i]
    const m_Cartographic1 = positionsCartographic[i + 1]
    let lng0 = m_Cartographic0.longitude,
      lat0 = m_Cartographic0.latitude

    if (m_Cartographic1) {
      let lng1 = m_Cartographic1.longitude,
        lat1 = m_Cartographic1.latitude
      let a = Math.abs(lng0 - lng1) * 10000000
      let b = Math.abs(lat0 - lat1) * 10000000
      //等距采样
      if (a > b) b = a
      let length = parseInt((b / 2).toString())
      if (length > 150) length = 150
      if (length < 2) length = 2
      for (let j = 0; j < length; j++) {
        terrainSamplePositions.push(
          new Cesium.Cartographic(
            Cesium.Math.lerp(lng0, lng1, j / (length - 1)),
            Cesium.Math.lerp(lat0, lat1, j / (length - 1))
          )
        )
      }
      terrainSamplePositions.pop()
    } else {
      terrainSamplePositions.push(m_Cartographic0)
    }
  }
  let _result = []
  for (let n = 0; n < terrainSamplePositions.length; n++) {
    //地理坐标(弧度)转经纬度坐标
    let curCartographic = terrainSamplePositions[n]
    const z =
      type === 'project'
        ? 0
        : viewer.scene.sampleHeight(curCartographic, objectsToExclude)
    const x = (curCartographic.longitude / Math.PI) * 180
    const y = (curCartographic.latitude / Math.PI) * 180
    _result.push({ x, y, z })
  }
  return _result
}

/**
 * 计算多边形质点
 * @method
 * @param {Array<Cartesian3>} positions 多边形节点,笛卡尔坐标集合
 * @returns {Array<Number>} 多边形质点坐标值 => [x,y]
 */
export const getPolygonCentroid = (positions) => {
  let centroidX = 0,
    centroidY = 0,
    signedArea = 0
  for (let index = 0; index < positions.length; index++) {
    const pos1 = transformCartesianToWGS84(positions[index])
    const pos2 = transformCartesianToWGS84(
      positions[(index + 1) % positions.length]
    )
    const crossProduct = pos1.x * pos2.y - pos2.x * pos1.y
    signedArea += crossProduct // 计算多边形的有向面积
    centroidX += (pos1.x + pos2.x) * crossProduct
    centroidY += (pos1.y + pos2.y) * crossProduct
  }

  signedArea *= 0.5
  centroidX /= 6 * signedArea
  centroidY /= 6 * signedArea

  return [centroidX, centroidY]
}

/**
 * 弧度转换为角度
 * @method
 * @param {Number} radians 弧度值
 * @returns {Number} 角度值
 */
export const radiansToDegrees = (radians) => {
  const degrees = radians % (2 * Math.PI)
  return (degrees * 180) / Math.PI < 0
    ? 360 + (degrees * 180) / Math.PI
    : (degrees * 180) / Math.PI
}

/**
 * 角度转弧度
 * @method
 * @param {Number} degrees 角度值
 * @returns {Number} 弧度值
 */
export const degreesToRadians = (degrees) => {
  const radians = Cesium.Math.toRadians(degrees)
  return radians
}

/**
 * 已知点根据角度和距离求取另一点坐标(二维),注:WGS84坐标系
 * @method
 * @param {Number} lng 经度X
 * @param {Number} lat 纬度Y
 * @param {Number} angle 角度 (0~360度)
 * @param {Number} distance 距离 (米)
 * @returns {DegreePos} 目标点坐标
 */
export const getPointByAngleDistance = (lng, lat, angle, distance) => {
  let a = 6378137 // 赤道半径
  let b = 6356752.3142 // 短半径
  let f = 1 / 298.257223563 // 扁率

  let alpha1 = angle * (Math.PI / 180)
  let sinAlpha1 = Math.sin(alpha1)
  let cosAlpha1 = Math.cos(alpha1)
  let tanU1 = (1 - f) * Math.tan(lat * (Math.PI / 180))
  let cosU1 = 1 / Math.sqrt(1 + tanU1 * tanU1),
    sinU1 = tanU1 * cosU1
  let sigma1 = Math.atan2(tanU1, cosAlpha1)
  let sinAlpha = cosU1 * sinAlpha1
  let cosSqAlpha = 1 - sinAlpha * sinAlpha
  let uSq = (cosSqAlpha * (a * a - b * b)) / (b * b)
  let A = 1 + (uSq / 16384) * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq)))
  let B = (uSq / 1024) * (256 + uSq * (-128 + uSq * (74 - 47 * uSq)))
  let sigma = distance / (b * A),
    sigmaP = 2 * Math.PI
  let cos2SigmaM = 0,
    sinSigma = 0,
    cosSigma = 0
  while (Math.abs(sigma - sigmaP) > 1e-12) {
    cos2SigmaM = Math.cos(2 * sigma1 + sigma)
    sinSigma = Math.sin(sigma)
    cosSigma = Math.cos(sigma)
    let deltaSigma =
      B *
      sinSigma *
      (cos2SigmaM +
        (B / 4) *
          (cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM) -
            (B / 6) *
              cos2SigmaM *
              (-3 + 4 * sinSigma * sinSigma) *
              (-3 + 4 * cos2SigmaM * cos2SigmaM)))
    sigmaP = sigma
    sigma = distance / (b * A) + deltaSigma
  }

  let tmp = sinU1 * sinSigma - cosU1 * cosSigma * cosAlpha1
  let lat2 = Math.atan2(
    sinU1 * cosSigma + cosU1 * sinSigma * cosAlpha1,
    (1 - f) * Math.sqrt(sinAlpha * sinAlpha + tmp * tmp)
  )
  let lambda = Math.atan2(
    sinSigma * sinAlpha1,
    cosU1 * cosSigma - sinU1 * sinSigma * cosAlpha1
  )
  let C = (f / 16) * cosSqAlpha * (4 + f * (4 - 3 * cosSqAlpha))
  let L =
    lambda -
    (1 - C) *
      f *
      sinAlpha *
      (sigma +
        C *
          sinSigma *
          (cos2SigmaM + C * cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM)))
  return {
    x: lng + L * (180 / Math.PI),
    y: lat2 * (180 / Math.PI),
  }
}

/**
 * 区域内生成三角网
 * @method
 * @param {Array<Cartesian3>} positions 三角网生成的范围点集合,笛卡尔坐标
 * @param {Number} s - 精度,一般为 4/经度or纬度差
 * @returns {Array<DegreePosZ[]>} 三角网集合,WGS84坐标
 */
export const subTriangle = (positions, s) => {
  let granularity = Math.PI / Math.pow(2, 11)
  granularity = granularity / s
  const polygonGeometry = Cesium.PolygonGeometry.fromPositions({
    positions: positions,
    vertexFormat: Cesium.PerInstanceColorAppearance.FLAT_VERTEX_FORMAT,
    granularity: granularity,
  })
  const geom = Cesium.PolygonGeometry.createGeometry(polygonGeometry)
  const triangles = []
  for (let i = 0; i < geom.indices.length; i += 3) {
    const i0 = geom?.indices[i]
    const i1 = geom?.indices[i + 1]
    const i2 = geom?.indices[i + 2]

    const subTrianglePositions = geom?.attributes.position.values
    const pos0 = {
      x: subTrianglePositions[i0 * 3],
      y: subTrianglePositions[i0 * 3 + 1],
      z: subTrianglePositions[i0 * 3 + 2],
    }
    const pos1 = {
      x: subTrianglePositions[i1 * 3],
      y: subTrianglePositions[i1 * 3 + 1],
      z: subTrianglePositions[i1 * 3 + 2],
    }
    const pos2 = {
      x: subTrianglePositions[i2 * 3],
      y: subTrianglePositions[i2 * 3 + 1],
      z: subTrianglePositions[i2 * 3 + 2],
    }

    let points = [pos0, pos1, pos2]
    triangles.push(points)
  }
  return triangles
}

/**
 * 自动进行插值并计算获取区域内的极值点(最高点和最低点)
 * @method
 * @param {Object} [viewer] 地图场景
 * @param {Array<Cartesian3>} [positions] 坐标点集合,笛卡尔坐标
 * @param {Number} [num=25] 精度按照经纬度1°大约100公里,默认按25等分来计算
 * @returns {Object} 最高和最低点的高度值和位置 => {minH:0,maxH:0,minPos:{x,y}, maxPos:{x,y}}
 */
export const getRegionExtreme = (viewer, positions, num = 25) => {
  let minX = 1000,
    maxX = -1000,
    minY = 1000,
    maxY = -1000
  const points = positions.map((pos) => {
    const { x, y } = transformCartesianToWGS84(pos)
    minX = Math.min(x, minX)
    maxX = Math.max(x, maxX)
    minY = Math.min(y, minY)
    maxY = Math.max(y, maxY)
    return [x, y]
  })
  points.push(points[0])
  const box = [minX, minY, maxX, maxY]
  const _d = Math.min(maxX - minX, maxY - minY)
  // 精度按照经纬度1°大约100公里,默认按25等分来计算
  const grid = interPointsByDistance(box, points, (_d * 100) / num)
  const features = grid.features
  const posList = []
  const cartographics = []
  for (let index = 0; index < features.length; index++) {
    const feature = features[index]
    const coord = feature.geometry.coordinates
    posList.push({ x: coord[0], y: coord[1] })
    cartographics.push(Cesium.Cartographic.fromDegrees(coord[0], coord[1]))
  }
  let minH = 1000000
  let maxH = -1000000
  let minPos = posList[0]
  let maxPos = posList[0]
  for (let i = 0; i < cartographics.length; i++) {
    const graphic = cartographics[i]
    let h = viewer.scene.sampleHeightSupported
      ? viewer.scene.sampleHeight(graphic)
      : viewer.scene.globe.getHeight(graphic)
    if (h < minH) {
      minH = h
      minPos = posList[i]
    }
    if (h > maxH) {
      maxH = h
      maxPos = posList[i]
    }
  }
  return { minH, maxH, minPos, maxPos }
}

/**
 * 二维状态下计算两点对于正北方向的朝向角度,范围[0,360]
 * @method
 * @param {DegreePos} start 起始点,经纬度点
 * @param {DegreePos} end 目标点,经纬度点
 * @returns {Number} 角度值
 */
export const calculateAngle = (start, end) => {
  let rad = Math.PI / 180,
    lat1 = start.y * rad,
    lat2 = end.y * rad,
    lon1 = start.x * rad,
    lon2 = end.x * rad
  const a = Math.sin(lon2 - lon1) * Math.cos(lat2)
  const b =
    Math.cos(lat1) * Math.sin(lat2) -
    Math.sin(lat1) * Math.cos(lat2) * Math.cos(lon2 - lon1)

  return radiansToDegrees(Math.atan2(a, b))
}

/**
 * 经纬度坐标数组转笛卡尔坐标数组
 * @method
 * @param {Array<Number[]>} positions 经纬度坐标数组,如:[[120,34],[121,35]]
 * @returns {Array<Cartesian3>} 笛卡尔坐标数组
 */
export const PosFromDegreeArray = (positions) => {
  let result = []
  for (let index = 0; index < positions.length; index++) {
    const pos = positions[index]
    result.push(pos[0], pos[1])
  }
  const points = Cesium.Cartesian3.fromDegreesArray(result)
  return points
}

/**
 * 根据经纬度数组获取地表位置点数组
 * @method
 * @param {Object} terrain 当前场景地形对象
 * @param {Array<Number[]>} points 经纬度数组,如:[[120,34],[121,35]]
 * @returns {Promise<Array<Cartesian3>>} 异步返回,地表点集合,笛卡尔坐标数组
 */
export const PosOnTerrainFromDegreeArray = async (terrain, points) => {
  const positions = points.map((point) => {
    return Cesium.Cartographic.fromDegrees(point[0], point[1])
  })
  const updatedPositions = await Cesium.sampleTerrainMostDetailed(
    terrain,
    positions
  )
  const result = updatedPositions.map((pos) => {
    return Cesium.Cartographic.toCartesian(pos)
  })
  return result
}

/**
 * 根据经纬度获取地表位置点
 * @method
 * @param {Object} terrain 当前场景地形对象
 * @param {Array<Number>} position 经纬度坐标,如[120,35]
 * @returns {Promise<Cartesian3>} 异步返回:地表坐标点,笛卡尔坐标
 */
export const PosOnTerrainFromDegree = async (terrain, position) => {
  const cartographic = Cesium.Cartographic.fromDegrees(position[0], position[1])
  const updatedPositions = await Cesium.sampleTerrainMostDetailed(terrain, [
    cartographic,
  ])
  const result = Cesium.Cartographic.toCartesian(updatedPositions[0])
  return result
}

/**
 * 获取指定经纬度点地表高度
 * @method
 * @param {Object} terrain 当前场景地形对象
 * @param {Array<Number>} position 经纬度坐标,如[120,35]
 * @returns {Promise<Number>} 异步返回:当前点的地表高度值
 */
export const getHeight = async (terrain, position) => {
  const cartographic = Cesium.Cartographic.fromDegrees(position[0], position[1])
  const updatedPositions = await Cesium.sampleTerrainMostDetailed(terrain, [
    cartographic,
  ])
  const height = updatedPositions[0].height
  return height
}

/**
 * 获取经纬度点集高度
 * @method
 * @param {Object} terrain 当前场景地形对象
 * @param {Array<Number[]>} points 经纬度坐标数组,如[[120,35],[121,36]]
 * @returns {Promise<Array<Number>>} 异步返回:高度值数组
 */
export const getHeightArray = async (terrain, points) => {
  const positions = points.map((point) => {
    return Cesium.Cartographic.fromDegrees(point[0], point[1])
  })
  const updatedPositions = await Cesium.sampleTerrainMostDetailed(
    terrain,
    positions
  )
  const result = updatedPositions.map((pos) => {
    return pos.height
  })
  return result
}

/**
 * 按倍数调整笛卡尔坐标点的高度,并返回更新后的笛卡尔坐标
 * @method
 * @param {Cartesian3} [pos] 笛卡尔坐标
 * @param {Number} [scale] 高度放大倍数
 * @param {Number} [min=0] 限定最小高度值,默认值为0,设置后,如果计算后的高度值小于min值,则高度直接取min值
 * @returns {Cartesian3} 更新后的笛卡尔坐标
 */
export const scaleHeight = (pos, scale, min = 0) => {
  const cartographic = Cesium.Cartographic.fromCartesian(pos)
  let { longitude, latitude, height } = cartographic
  const h = Math.max(height * scale, min)
  const cartographic1 = new Cesium.Cartographic(longitude, latitude, h)
  return Cesium.Cartographic.toCartesian(cartographic1)
}

/**
 * 调整点高度为指定高度,并返回更新后的笛卡尔坐标
 * @method
 * @param {Cartesian3} pos 笛卡尔坐标
 * @param {Number} height 高度值
 * @returns {Cartesian3} 更新后的笛卡尔坐标
 */
export const changeHeight = (pos, height) => {
  const cartographic = Cesium.Cartographic.fromCartesian(pos)
  let { longitude, latitude } = cartographic
  const cartographic1 = new Cesium.Cartographic(longitude, latitude, height)
  return Cesium.Cartographic.toCartesian(cartographic1)
}

/**
 * 经纬度四至转弧度四至
 * @method
 * @param {Array<Number>} extent 经纬度四至,如:[112,23,120,30]
 * @returns {Array<Number>}  转换后的弧度四至
 */
export const extentToRadians = (extent) => {
  return extent.map((val) => degreesToRadians(val))
}

/**
 * 弧度四至转经纬度四至
 * @method
 * @param {Array<Number>} extent 弧度四至
 * @returns {Array<Number>} 转换后的经纬度四至
 */
export const extentToDegrees = (extent) => {
  return extent.map((val) => radiansToDegrees(val))
}

/**
 * 判断点在地球的正面还是反面
 * @method
 * @param {Cartesian3} position 点位,笛卡尔坐标
 * @param {Cartesian3} cameraPosition 相机位置,笛卡尔坐标
 * @returns true-正面,false-反面
 */
export const isVisible = (position, cameraPosition) => {
  let cameraOccluder = new Cesium.EllipsoidalOccluder(
    Cesium.Ellipsoid.WGS84,
    cameraPosition
  )
  let viewerVisible = cameraOccluder.isPointVisible(position)
  return viewerVisible
}