core/analysis/HeatMap.js

import { Cesium } from '../../../namespace'
import heatmapFactory from '../heatmap/Heatmap'

/**
 * 人力图分析主类
 * @class
 */
class HeatMap {
  /**
   * 构造函数
   * @param {Viewer} viewer 地图场景对象
   */
  constructor(viewer) {
    this._viewer = viewer
    this.CustomType = 'HeatMap'

    this.dom = undefined
    this.id =
      new Date().getTime() + '' + Number(Math.random() * 1000).toFixed(0)
    this._updateId = this.id
    this.canvasw = 200

    this.polygon = undefined

    this.primitive = undefined

    this.bound = undefined // 四角坐标
    this.rect = {} // 经纬度范围

    this.x_axios = undefined // x 轴
    this.y_axios = undefined // y 轴
    this.girthX = 0 // x轴长度
    this.girthY = 0 // y轴长度
    this._createDom()
  }
  /**
   * 热力图数据,支持动态设定
   * @type {Array<HeatmapDatas>}
   */
  get datas() {
    return this._list || []
  }
  set datas(list) {
    if (this._type === 3) return
    this._initParams(list)
    this._list = list
    this._updateId =
      new Date().getTime() + '' + Number(Math.random() * 1000).toFixed(0)
  }

  /**
   * 热力图类型,支持设置切换,2-二维热力图,3-三维热力图
   * @type {Number}
   */
  get type() {
    return this._type
  }

  set type(val) {
    this._type = val
    if (val === 2) {
      this.primitive && this._showPrimitive(false)
      this.polygon ? this._showPolygon(true) : this._drawMap(this._list)
    }
    if (val === 3) {
      this.polygon && this._showPolygon(false)
      this.primitive && this._updateId === this.id
        ? this._showPrimitive(true)
        : this._drawPrimitive(this._list)
    }
  }
  _showPolygon(bool) {
    this.polygon.show = bool
  }
  _showPrimitive(bool) {
    this.primitive.show = bool
  }
  /**
   * 初始化热力图
   * @param {Array<HeatmapDatas>} datas 待绘制热力图数据集
   * @param {Number} [type=2] 热力图类型,2-2D热力图;3-3D热力图
   * @param {HeatmapOptions} [options={}] 可选,热力图绘制参数,默认为{}
   */
  initMap(datas, type = 2, options = {}) {
    this._type = type
    this.options = options
    this._list = datas
    this.heatmapInstance = this._initHeatmapInstance()
    switch (this._type) {
      case 2:
        this._drawMap(datas)
        break
      case 3:
        this._drawPrimitive(datas)
        break
      default:
        break
    }
  }
  _initHeatmapInstance() {
    while (this.dom.hasChildNodes()) {
      this.dom.removeChild(this.dom.firstChild)
    }
    const { radius, gradient, baseHeight, primitiveType } = this.options
    let config = {
      container: this.dom,
      radius: radius || 10,
      maxOpacity: 0.7,
      minOpacity: 0,
      blur: 0.75,
      gradient: gradient || {
        '.3': 'blue',
        '.5': 'green',
        '.7': 'yellow',
        '.95': 'red',
      },
    }

    //3D热力图
    this.baseHeight = baseHeight || 0
    this.primitiveType = primitiveType || 'TRIANGLES'

    return heatmapFactory.create(config)
  }
  _computeBound(positions) {
    this.rect = this._calXY(positions)
    const { minLat, maxLat, minLng, maxLng } = this.rect

    this.bound = {
      leftTop: Cesium.Cartesian3.fromDegrees(minLng, maxLat),
      leftBottom: Cesium.Cartesian3.fromDegrees(minLng, minLat),
      rightTop: Cesium.Cartesian3.fromDegrees(maxLng, maxLat),
      rightBottom: Cesium.Cartesian3.fromDegrees(maxLng, minLat),
    }

    this.x_axios = Cesium.Cartesian3.subtract(
      this.bound.rightTop,
      this.bound.leftTop,
      new Cesium.Cartesian3()
    )
    this.x_axios = Cesium.Cartesian3.normalize(
      this.x_axios,
      new Cesium.Cartesian3()
    )
    this.y_axios = Cesium.Cartesian3.subtract(
      this.bound.leftBottom,
      this.bound.leftTop,
      new Cesium.Cartesian3()
    )
    this.y_axios = Cesium.Cartesian3.normalize(
      this.y_axios,
      new Cesium.Cartesian3()
    )
    this.girthX = Cesium.Cartesian3.distance(
      this.bound.rightTop,
      this.bound.leftTop
    )
    this.girthY = Cesium.Cartesian3.distance(
      this.bound.leftBottom,
      this.bound.leftTop
    )
  }

  _initParams(list) {
    const hierarchy = []
    // 添加标签验证
    for (let ind = 0; ind < list.length; ind++) {
      let position = Cesium.Cartesian3.fromDegrees(
        list[ind].lnglat[0],
        list[ind].lnglat[1]
      )
      hierarchy.push(position)
    }

    const points =
      this._type === 2
        ? this._calPoints2D(hierarchy, list)
        : this._calPoints3D(hierarchy, list)

    this.hierarchy = hierarchy
    this.heatmapInstance = this._initHeatmapInstance()
    this.heatmapInstance.addData(points)
  }

  _calPoints3D(hierarchy, list) {
    this._computeBound(hierarchy)
    let points = []
    for (let i = 0; i < hierarchy.length; i++) {
      let p1 = hierarchy[i]
      const rete = this._computeRateInBound(p1)
      rete &&
        points.push({
          x: rete.x,
          y: rete.y,
          value: list[i].value,
        })
    }
    return points
  }

  _calPoints2D(hierarchy, list) {
    const sw = this.canvasw
    const bound = this._getBound(hierarchy)
    if (!bound) return
    const { leftTop, leftBottom, rightTop } = bound
    let points = []
    let x_axios = Cesium.Cartesian3.subtract(
      rightTop,
      leftTop,
      new Cesium.Cartesian3()
    )
    x_axios = Cesium.Cartesian3.normalize(x_axios, new Cesium.Cartesian3())
    let y_axios = Cesium.Cartesian3.subtract(
      leftBottom,
      leftTop,
      new Cesium.Cartesian3()
    )
    y_axios = Cesium.Cartesian3.normalize(y_axios, new Cesium.Cartesian3())
    const girthX = Cesium.Cartesian3.distance(rightTop, leftTop)
    const girthY = Cesium.Cartesian3.distance(leftBottom, leftTop)
    for (let i = 0; i < hierarchy.length; i++) {
      const p1 = hierarchy[i]
      const p_origin = Cesium.Cartesian3.subtract(
        p1,
        leftTop,
        new Cesium.Cartesian3()
      )
      const diffX = Cesium.Cartesian3.dot(p_origin, x_axios)
      const diffY = Cesium.Cartesian3.dot(p_origin, y_axios)
      points.push({
        x: Number((diffX / girthX) * sw).toFixed(0),
        y: Number((diffY / girthY) * sw).toFixed(0),
        value: list[i].value,
      })
    }
    return points
  }

  _drawMap(list) {
    this._initParams(list)
    this._createPolygon()
  }

  _drawPrimitive(list) {
    this._initParams(list)
    this._createPrimitive()
  }

  _createPrimitive() {
    this.primitive && this._viewer.scene.primitives.remove(this.primitive)
    this.primitive = undefined
    let instance = new Cesium.GeometryInstance({
      geometry: this._createGeometry(),
    })

    this.primitive = this._viewer.scene.primitives.add(
      new Cesium.Primitive({
        geometryInstances: instance,
        appearance: new Cesium.MaterialAppearance({
          material: new Cesium.Material({
            fabric: {
              type: 'Image',
              uniforms: {
                image: this.heatmapInstance.getDataURL(),
              },
            },
          }),
          translucent: true,
          flat: true,
        }),
        asynchronous: false,
      })
    )
    this.primitive.id = 'DEJA_VU3D_HEATMAP3D'
    this._updateId = this.id
  }

  // 计算当前坐标在范围中位置 换算为canvas中的像素坐标
  _computeRateInBound(position) {
    if (!position) return
    let ctgc = Cesium.Cartographic.fromCartesian(position.clone())
    ctgc.height = 0
    position = Cesium.Cartographic.toCartesian(ctgc.clone())

    const p_origin = Cesium.Cartesian3.subtract(
      position.clone(),
      this.bound.leftTop,
      new Cesium.Cartesian3()
    )
    const diffX = Cesium.Cartesian3.dot(p_origin, this.x_axios)
    const diffY = Cesium.Cartesian3.dot(p_origin, this.y_axios)
    return {
      x: Math.round(Number((diffX / this.girthX) * this.canvasw)),
      y: Math.round(Number((diffY / this.girthY) * this.canvasw)),
    }
  }

  _createGeometry() {
    let opt = this._getGrain()
    let geometry = new Cesium.Geometry({
      attributes: new Cesium.GeometryAttributes({
        position: new Cesium.GeometryAttribute({
          componentDatatype: Cesium.ComponentDatatype.DOUBLE,
          componentsPerAttribute: 3,
          values: opt.positions,
        }),
        st: new Cesium.GeometryAttribute({
          componentDatatype: Cesium.ComponentDatatype.FLOAT,
          componentsPerAttribute: 2,
          values: new Float32Array(opt.st),
        }),
      }),
      indices: new Uint16Array(opt.indices),
      primitiveType: Cesium.PrimitiveType[this.primitiveType],
      boundingSphere: Cesium.BoundingSphere.fromVertices(opt.positions),
    })
    return geometry
  }

  // 根据经纬度跨度和canvas的宽高 来计算顶点坐标及顶点法向量
  _getGrain() {
    let canvasw = this.canvasw
    let canvasH = this.canvasw
    let { maxLng, maxLat, minLng, minLat } = this.rect

    const granLng_w = (maxLng - minLng) / canvasw // 经度粒度
    const granLat_H = (maxLat - minLat) / canvasH // 经度粒度
    let positions = []
    let st = []
    let indices = []
    for (let i = 0; i < canvasw; i++) {
      let nowLng = minLng + granLng_w * i

      for (let j = 0; j < canvasH; j++) {
        let nowLat = minLat + granLat_H * j
        const value = this.heatmapInstance.getValueAt({
          x: i,
          y: j,
        })
        let cartesian3 = Cesium.Cartesian3.fromDegrees(
          nowLng,
          nowLat,
          this.baseHeight + value
        )
        positions.push(cartesian3.x, cartesian3.y, cartesian3.z)
        st.push(i / canvasw, j / canvasH)
        if (j != canvasH - 1 && i != canvasw - 1) {
          indices.push(
            i * canvasH + j,
            i * canvasH + j + 1,
            (i + 1) * canvasH + j
          )
          indices.push(
            (i + 1) * canvasH + j,
            (i + 1) * canvasH + j + 1,
            i * canvasH + j + 1
          )
        }
      }
    }

    return {
      positions: positions,
      st: st,
      indices: indices,
    }
  }

  _createPolygon() {
    this.polygon = this._viewer.entities.add({
      polygon: {
        hierarchy: new Cesium.CallbackProperty(() => {
          const bound = this._getBound(this.hierarchy)
          const { leftTop, leftBottom, rightBottom, rightTop } = bound
          return new Cesium.PolygonHierarchy([
            leftTop,
            leftBottom,
            rightBottom,
            rightTop,
          ])
        }, false),
        material: new Cesium.ImageMaterialProperty({
          image: new Cesium.CallbackProperty(() => {
            return this.heatmapInstance.getDataURL()
          }, false),
        }),
        heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
      },
    })
  }

  _calXY(positions) {
    let rect = this._toRectangle(positions) // 转为正方形
    let lnglats = cartesiansToLnglats(rect)
    let minLat = Number.MAX_VALUE,
      maxLat = Number.MIN_VALUE,
      minLng = Number.MAX_VALUE,
      maxLng = Number.MIN_VALUE
    const length = rect.length
    for (let i = 0; i < length; i++) {
      const lnglat = lnglats[i]
      if (lnglat[0] < minLng) {
        minLng = lnglat[0]
      }
      if (lnglat[0] > maxLng) {
        maxLng = lnglat[0]
      }

      if (lnglat[1] < minLat) {
        minLat = lnglat[1]
      }
      if (lnglat[1] > maxLat) {
        maxLat = lnglat[1]
      }
    }

    const diff_lat = maxLat - minLat
    const diff_lng = maxLng - minLng

    minLat = minLat - diff_lat / length
    maxLat = maxLat + diff_lat / length
    minLng = minLng - diff_lng / length
    maxLng = maxLng + diff_lng / length
    return { minLat, maxLat, minLng, maxLng }
  }
  // 扩展边界 防止出现热力图被分割
  _getBound(positions) {
    const { minLat, maxLat, minLng, maxLng } = this._calXY(positions)
    return {
      leftTop: Cesium.Cartesian3.fromDegrees(minLng, maxLat),
      leftBottom: Cesium.Cartesian3.fromDegrees(minLng, minLat),
      rightTop: Cesium.Cartesian3.fromDegrees(maxLng, maxLat),
      rightBottom: Cesium.Cartesian3.fromDegrees(maxLng, minLat),
    }
  }

  // 任何图形均转化为正方形
  _toRectangle(hierarchy) {
    if (!hierarchy) return
    let boundingSphere = Cesium.BoundingSphere.fromPoints(
      hierarchy,
      new Cesium.BoundingSphere()
    )
    let center = boundingSphere.center
    const radius = boundingSphere.radius

    let modelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(center.clone())
    let roate_y = new Cesium.Cartesian3(0, 1, 0)

    let arr = []
    for (let i = 45; i <= 360; i += 90) {
      let roateZ_mtx = Cesium.Matrix3.fromRotationZ(
        Cesium.Math.toRadians(i),
        new Cesium.Matrix3()
      )
      let yaix_roate = Cesium.Matrix3.multiplyByVector(
        roateZ_mtx,
        roate_y,
        new Cesium.Cartesian3()
      )
      yaix_roate = Cesium.Cartesian3.normalize(
        yaix_roate,
        new Cesium.Cartesian3()
      )
      let third = Cesium.Cartesian3.multiplyByScalar(
        yaix_roate,
        radius,
        new Cesium.Cartesian3()
      )
      let poi = Cesium.Matrix4.multiplyByPoint(
        modelMatrix,
        third.clone(),
        new Cesium.Cartesian3()
      )

      arr.push(poi)
    }

    return arr
  }

  _createDom() {
    this.dom = window.document.createElement('div')
    this.dom.id = `deja_vu3d-heatmap-${this.id}`
    this.dom.className = `deja_vu3d-heatmap`
    this.dom.style.width = this.canvasw + 'px'
    this.dom.style.height = this.canvasw + 'px'
    this.dom.style.position = 'absolute'
    this.dom.style.display = 'none'
    this.dom.style.zIndex = '-99'
    this._viewer.container.appendChild(this.dom)
  }

  /**
   * 注销热力图对象
   */
  destroy() {
    this.dom && this.dom.remove()
    this.dom = undefined
    this.polygon && this._viewer.entities.remove(this.polygon)
    this.polygon = undefined
    this.primitive && this._viewer.scene.primitives.remove(this.primitive)
    this.primitive = undefined
    this.list = []
    this.heatmapInstance = undefined
  }
}

function cartesiansToLnglats(cartesians) {
  if (!cartesians || cartesians.length < 1) return
  let arr = []
  for (let i = 0; i < cartesians.length; i++) {
    const car3 = cartesians[i]
    let lnglat = Cesium.Cartographic.fromCartesian(car3)
    let lat = Cesium.Math.toDegrees(lnglat.latitude)
    let lng = Cesium.Math.toDegrees(lnglat.longitude)
    let hei = lnglat.height
    arr.push([lng, lat, hei])
  }
  return arr
}

export default HeatMap