Cesium项目开发笔记

Cesium项目开发笔记

2021年08月20日 阅读:182 字数:3967 阅读时长:9 分钟

Cesium是一个跨平台、跨浏览器的展示三维地球和地图的javascript库,本文整理项目中使用cesium的笔记。

Cesium是一个跨平台、跨浏览器的展示三维地球和地图的javascript库。

使用WebGL来进行硬件加速图形,使用时不需要任何插件支持,但是浏览器必须支持WebGL。

基于Apache2.0许可的开源程序,它可以免费的用于商业和非商业用途

e4151abd3fd7310d.png

通过Cesium提供的Javascript API,可以实现以下功能:

  • 支持 3d 地球、 2d 地图、 2.5d 哥伦布模式的地理(地图)数据展示。
  • 支持鼠标和触摸操作的三维空间 渲染、缩放、惯性平移、飞行、任意视角漫游。
  • 支持各种几何体:点、 线、面、走廊、管径、墙体、立方体、圆柱、球体等。
  • 支持标准的矢量格式 KML 、GeoJSON、TopoJSON, 以及矢量的贴地效果。
  • 支持多种资源的图像层,包括 WMS,TMS, WMTS以及时序图像。支持透明度叠加, 亮度等参数的调整。
  • 使用gtlf和3dtiles格式加载各种不同的 3d 数据,包含 倾斜摄影、人工模型、 BIM,点云数据等。

涉及三个知识领域 : Web前端、计算机图形学、地理信息系统(GIS)。所以想要学好Cesium,需要对Web前端、计算机图形学、GIS相关的基础知识有所掌握。

50884684398e94a9.png

Cesium中常用的坐标:

1、屏幕坐标

即二维笛卡尔平面坐标,我们通过鼠标点击直接获取的坐标就是屏幕坐标了,单位是像素值

2.笛卡尔空间直角坐标(常用)

笛卡尔空间直角坐标又称为世界坐标,主要是用来做空间位置的变化如平移、旋转和缩放等等

1、开发环境搭建

以vue-cli举例子,包括拷贝配置、cesium静态资源,按需引入,模块打包等配置

const webpack = require('webpack')
const CopyWebpackPlugin = require('copy-webpack-plugin')

// The path to the cesium source code
const cesiumSource = 'node_modules/cesium/Source'
const cesiumWorkers = '../Build/Cesium/Workers'
// Configure your module bundler to copy the following four directories and serve them as static files
const cesiumBaseUrl = 'static/cesium/'

module.exports = {
  configureWebpack: {
    plugins: [
      // Copy Cesium Assets, Widgets, and Workers to a static directory
      new CopyWebpackPlugin([{ from: path.join(cesiumSource, cesiumWorkers), to: cesiumBaseUrl + 'Workers' }]),
      new CopyWebpackPlugin([{ from: path.join(cesiumSource, 'Assets'), to: cesiumBaseUrl + 'Assets' }]),
      new CopyWebpackPlugin([{ from: path.join(cesiumSource, 'Widgets'), to: cesiumBaseUrl + 'Widgets' }]),
      // Define relative base path in cesium for loading assets
      new webpack.DefinePlugin({
        // example http://localhost:8080/static/cesium/Assets/, then you would set the base URL as follows:
        CESIUM_BASE_URL: JSON.stringify(cesiumBaseUrl)
      })
    ],
    module: {
      // 解决:Critical dependency: require function is used in a way in which dependencies cannot be statically extracted
      unknownContextCritical: false,
      rules: [
        {
          // Strip cesium pragmas
          test: /\.js$/,
          enforce: 'pre',
          include: path.resolve(__dirname, cesiumSource),
          sideEffects: false,
          use: [{
            loader: 'strip-pragma-loader',
            options: {
              pragmas: {
                debug: false
              }
            }
          }]
        }
      ]
    }
  },
  chainWebpack(config) {
    config
      .when(process.env.NODE_ENV !== 'development',
        config => {
           // cesium单独打一个包
          config
            .optimization.splitChunks({
              chunks: 'all',
              cacheGroups: {
                cesium: {
                  name: 'chunk-cesium',
                  priority: 20,
                  test: /[\\/]node_modules[\\/]_?cesium(.*)/ // in order to adapt to cnpm
                }
              }
            })
        }
      )
  }
}

vite可以直接使用vite-plugin-cesium插件

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import cesium from 'vite-plugin-cesium'
const path = require('path')

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue(), cesium()],
  resolve: {
    // 配置路径别名
    alias: {
      '@': path.resolve(__dirname, './src')
    }
  }
})

2、初始化

任何Cesium应用程序的基础都是Viewer,Viewer是一个带有多种功能的可交互的三位数字地球的容器。

初始化界面也默认自带了一些组件,在初始化的时候进行隐藏,也可以通过CSS隐藏。

import { Viewer, Ion, Color, createWorldTerrain } from 'cesium'
import 'cesium/Build/Cesium/Widgets/widgets.css'

const ION_TOKEN = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI4MDRjNjY3Mi0wOWFlLTQ0ZWMtYTBmMC1jOWM2NjQ3MzA3NWQiLCJpZCI6MTE2NDEsInNjb3BlcyI6WyJhc3IiLCJnYyJdLCJpYXQiOjE1NTk1MzI0MzJ9.-KqKMl5H6TLPNwZJUZzwkz7_lwm0QN3UtLZ8Ko3z7dY'
Ion.defaultAccessToken = ION_TOKEN

export default class CesiumCluster {
/**
   * @param {Object} ops Cesium配置项
   */
  constructor(ops) {
    super()

    const options = { ...defaultOptions, ...ops }
    this.viewer = null

    this.init(options)
  }

  /**
   * 初始化Viewer
   * @param {String} element Dom节点
   * @param {Number} sceneMode 默认视图类型
   * @param {Object} imageryLayer 默认底图
   * @param {Object} center 初始位置
   * @see https://cesium.com/learn/cesiumjs/ref-doc/Viewer.html
   */
  init({ element, sceneMode, imageryLayer, center, callback }) {
    this.viewer = new Viewer(element, {
      sceneMode,
      orderIndependentTranslucency: false,
      contextOptions: {
        webgl: {
          alpha: true
        }
      },
      selectionIndicator: false, // 用于在选定对象上显示指示器的小部件
      animation: false, // 动画小组件
      baseLayerPicker: false, // 底图组件,选择三维数字地球的底图(imagery and terrain)
      fullscreenButton: false, // 全屏组件
      geocoder: false, // 地理编码(搜索)组件
      homeButton: false, // 首页,点击之后将视图跳转到默认视角
      infoBox: false, // 信息框
      sceneModePicker: false, // 场景模式,切换2D、3D 和 Columbus View (CV) 模式
      timeline: false, // 时间轴
      navigationHelpButton: false, // 帮助提示,如何操作数字地球
      terrainProvider: createWorldTerrain() // 加载地形数据
    })

    // 隐藏天空盒子、太阳、月亮
    this.viewer.scene.skyBox.show = false
    this.viewer.scene.sun.show = false
    this.viewer.scene.moon.show = false
    this.viewer.scene.backgroundColor = new Color(0.0, 0.0, 0.0, 0.0)
    // 隐藏logo
    this.viewer._cesiumWidget._creditContainer.style.display = 'none'
    // 渲染效果和性能调优
    // this.viewer.extend(viewerCesiumInspectorMixin)
    // 开启地形深度测试
    // this.viewer.scene.globe.depthTestAgainstTerrain = true
  }

  // 销毁Viewer
  destroy() {
    this.viewer.destroy()
    this.viewer = null
  }
}

2.1、添加影像数据

栅格瓦片就是我们浏览三维能感知的"皮肤"了,通常我们叠加的是各种卫星影像或瓦片数据。

添加底图影像或电子地图,以天地图卫星影像为例

// 添加影像底图
addImageryProvider() {
  this.viewer.scene.globe.maximumScreenSpaceError = 1.8
  const imageryProvider = this.setImageryProvider('img_w', 'img', 'tdtBasicLayer')
  const imageryAnnotation = language === 'en' ? this.setImageryProvider('eia_w', 'eia') : this.setImageryProvider('cia_w', 'cia')
  this.viewer.imageryLayers.addImageryProvider(imageryProvider)
  this.viewer.imageryLayers.addImageryProvider(imageryAnnotation)
}

// 天地图 Provider
setImageryProvider(lyr, layer, layername = 'tdtLayer') {
  const tk = 'xxxxxxxx'
  return new WebMapTileServiceImageryProvider({
    url: `http://t{s}.tianditu.com/${lyr}/wmts?service=wmts&request=GetTile&version=1.0.0&LAYER=${layer}&tileMatrixSet=w&TileMatrix={TileMatrix}&TileRow={TileRow}&TileCol={TileCol}&style=default&format=tiles&tk=${tk}`,
    layer: layername,
    style: 'default',
    subdomains: ['0', '1', '2', '3', '4', '5', '6', '7'],
    format: 'tiles',
    tileMatrixSetID: 'c',
    maximumLevel: 18
  })
}

3、添加空间可视化数据

三维场景中,地形和栅格来组成了三维的基础,但更多的业务还是需要点线面等矢量数据来充实, 这就是我们的矢量数据图层,数据是GIS的开始。

矢量数据是用经度、纬度、高度坐标来表示地图图形或地理实体位置的数据,常见的矢量数据有:点、线、面、体等格式。

Cesium在空间数据可视化方面提供了两种类型的API,一种是面向图形开发人员的低级(原始)API,通过Primitive类实现,另一种是用于数据驱动的高级(实体)API,通过Entity类实现。

import {
  Cartesian2,
  Cartesian3,
  Color,
  LabelStyle,
  HorizontalOrigin,
  VerticalOrigin,
  HeightReference,
  HeadingPitchRoll,
  Transforms
} from 'cesium'

const defaultColor = '#fff'

/**
 * 添加实体
 * @summary 包括广告牌、标签、点、线等
 * @see https://zhuanlan.zhihu.com/p/348807058
 * @see https://cesium.com/learn/cesiumjs/ref-doc/Entity.html
 */
export default class Entities {
  /**
   * 添加广告牌
   * @param {String} name 实体名称
   * @param {Array} degrees 经纬度
   * @param {*} image 广告牌图片
   */
  addBillboard(options = {}) {
    const { id, name = 'billboard', degrees, show = true, image, width, height, scale = 1, color, pixelOffset = [0, 0], clampToGround = true } = options

    const params = {
      id,
      name,
      position: this.fromDegrees(degrees),
      billboard: {
        show,
        image: image || 'static/cesium/Assets/Images/cesium_credit.png',
        pixelOffset: new Cartesian2(...pixelOffset), // 广告牌偏移
        color: Color.fromCssColorString(color || defaultColor),
        horizontalOrigin: HorizontalOrigin.CENTER,
        verticalOrigin: VerticalOrigin.CENTER,
        heightReference: clampToGround ? HeightReference.CLAMP_TO_GROUND : HeightReference.NONE,
        disableDepthTestDistance: Number.POSITIVE_INFINITY,
        width,
        height,
        scale
      }
    }

   this.viewer.entities.add(params)
  }

  /**
   * 添加标签
   * @param {Array} degrees 经纬度
   * @param {String} text 标签内容
   */
  addLabel(options = {}) {
    const {
      id, name = 'label', degrees, text, show = true, font = '12px', color = '#fff',
      outlineColor = '#000', outlineWidth = 2, pixelOffset = [0, 18]
    } = options

    const params = {
      id,
      name,
      position: this.fromDegrees(degrees),
      label: {
        show,
        text, // 标签内容
        font,
        style: LabelStyle.FILL_AND_OUTLINE,
        fillColor: Color.fromCssColorString(color), // 文本填充色
        outlineColor: Color.fromCssColorString(outlineColor), // 文本轮廓线颜色
        outlineWidth: outlineWidth, // 文本轮廓线宽
        pixelOffset: new Cartesian2(...pixelOffset), // 偏移
        horizontalOrigin: HorizontalOrigin.CENTER,
        verticalOrigin: VerticalOrigin.CENTER,
        eyeOffset: Cartesian3.ZERO
      }
    }

    this.viewer.entities.add(params)
  }

  /**
   * 添加点
   * @param {Array} degrees 经纬度
   */
  addPoint(options) {
    const {
      id, name = 'point', degrees, pixelSize = 10, show = true, color,
      outlineColor = '#000', outlineWidth = 0, pixelOffset = [0, 0]
    } = options

    const params = {
      id,
      name,
      position: this.fromDegrees(degrees),
      point: {
        show,
        pixelSize, // 像素大小
        // heightReference: Cesium.HeightReference.NONE,
        color: Color.fromCssColorString(color || defaultColor),
        outlineColor: Color.fromCssColorString(outlineColor),
        outlineWidth: outlineWidth,
        pixelOffset: new Cartesian2(...pixelOffset)
      }
    }

    this.viewer.entities.add(params)
  }

  /**
   * 添加多线段
   * @param {Array} degrees 经纬度
   * @example { degrees: [-75, 35, -125, 35] }
   */
  addPolyline(options = {}) {
    const {
      id, name = 'polyline', degrees, width = 6, show = true, color, clampToGround = true, zIndex
    } = options

    const params = {
      id,
      name,
      polyline: {
        show,
        positions: this.fromDegreesArray(degrees), // 定义线条的 Cartesian3 位置的数组
        clampToGround: clampToGround, // 是否贴地
        width: width,
        material: Color.fromCssColorString(color || defaultColor), // 线低于地形时用于绘制折线的材质
        zIndex // 指定用于订购地面几何形状的z索引
      }
    }

    return this.viewer.entities.add(params)
  }

  /**
   * 添加多边形
   * @param {Array} degrees 经纬度
   * @example { degrees: [-115, 37, -115, 32, -107, 33, -102, 31, -102, 35] }
   */
  addPolygon(options = {}) {
    const {
      id, name = 'polygon', degrees, height, show = true, color, outline, outlineColor, outlineWidth = 1, clampToGround = true, zIndex
    } = options

    const params = {
      id,
      name,
      polygon: {
        show,
        hierarchy: this.fromDegreesArray(degrees), // 指定PolygonHierarchy
        height, // 多边形相对于椭球面的高度
        heightReference: clampToGround ? HeightReference.CLAMP_TO_GROUND : HeightReference.RELATIVE_TO_GROUND, // 是否贴地
        material: Color.fromCssColorString(color || defaultColor),
        outline,
        outlineColor: Color.fromCssColorString(outlineColor || defaultColor),
        outlineWidth,
        zIndex // 指定用于订购地面几何形状的z索引
      }
    }

    return this.viewer.entities.add(params)
  }

  /**
   * 添加模型
   * @param {Array} lngLatHeight 经纬度高程
   * @param {String} url 模型地址
   */
  addModel(options = {}) {
    const { id, name = 'model', lngLatHeight, url, color, scale = 1, heading = 0, pitch = 0, roll = 0, clampToGround } = options

    const position = this.fromDegrees(lngLatHeight)

    const hpr = new HeadingPitchRoll(this.toRadians(heading), this.toRadians(pitch), this.toRadians(roll))
    const orientation = Transforms.headingPitchRollQuaternion(
      position,
      hpr
    )

    const params = {
      id,
      name,
      position,
      orientation,
      model: {
        show: true,
        uri: url,
        scale: scale,
        // minimumPixelSize: 128, // 模型的最小最小像素大小,而不考虑缩放
        // maximumScale: 20000, // 模型的最大比例尺大小。 minimumPixelSize的上限
        incrementallyLoadTextures: true, // 确定在加载模型后纹理是否可以继续流入
        runAnimations: true, // 是否应启动模型中指定的glTF动画
        clampAnimations: true, // glTF动画是否应在没有关键帧的持续时间内保持最后一个姿势
        heightReference: clampToGround ? HeightReference.CLAMP_TO_GROUND : HeightReference.RELATIVE_TO_GROUND,
        // silhouetteColor: Color.RED, // 轮廓的颜色
        // silhouetteSize: 0, // 轮廓的宽度
        color: Color.fromCssColorString(color || defaultColor) // 模型的颜色
      }
    }

    return this.viewer.entities.add(params)
  }
}

4、工具集

主要是坐标转换等公共方法。

屏幕坐标(像素)即二维笛卡尔平面坐标,我们通过鼠标点击直接获取的坐标就是屏幕坐标了,单位是像素值。

笛卡尔空间直角坐标又称为世界坐标,主要是用来做空间位置的变化如平移、旋转和缩放等等,它的坐标原点在椭球的中心。

经纬度坐标,即测绘中的地理经纬度坐标,默认是WGS84坐标系,坐标原点在椭球的质心。

import {
  Cartesian3,
  Math as CesiumMath,
  Cartographic,
  SceneTransforms
} from 'cesium'

/**
 * @summary 包括坐标转换...
 * @see https://zhuanlan.zhihu.com/p/334540571
 */
export default class Utils {
  /**
   * 经纬度转弧度
   * @param {*} degrees 经、纬度
   */
  toRadians(degrees) {
    return CesiumMath.toRadians(degrees)
  }

  /**
   * 弧度转经纬度
   * @param {*} radians 弧度
   */
  toDegrees(radians) {
    return CesiumMath.toDegrees(radians)
  }

  /**
   * 单个经纬度(带高程)转笛卡尔坐标
   * @param {Array} lnglat 经纬高
   */
  fromDegrees(lnglat) {
    return Cartesian3.fromDegrees(...lnglat)
  }

  /**
   * 多个经纬度(不带高程)转笛卡尔坐标
   * @param {Array} lnglat 经纬度
   * @example lnglat = [113.33, 23.33, 113.45, 24.34]
   */
  fromDegreesArray(lnglat) {
    return Cartesian3.fromDegreesArray(lnglat)
  }

  /**
   * 笛卡尔坐标转为经纬度
   * @param {Object} cartesian3 笛卡尔坐标
   */
  cartesianTodegrees(cartesian3) {
    const cartographic = Cartographic.fromCartesian(cartesian3)
    if (!cartographic) return []
    const lat = this.toDegrees(cartographic.latitude)
    const lng = this.toDegrees(cartographic.longitude)
    const height = cartographic.height
    return [lng, lat, height]
  }

  /**
   * 屏幕坐标转笛卡尔坐标
   * @param {Object} position 屏幕坐标
   */
  windowCoordinatesToCartesian(position) {
    let cartesian3 = this.getGlobeClickCartesian(position)

    const pick = this.viewer.scene.pickPosition(position)
    const pickModel = this.viewer.scene.pick(position)
    if (pickModel && pick) {
      cartesian3 = this.getModelClickCartesian(pick)
    }

    return cartesian3
  }

  // 获取倾斜摄影或模型点击处的笛卡尔坐标
  getModelClickCartesian(pick) {
    const { longitude, latitude, height } = Cartographic.fromCartesian(pick)
    const lat = CesiumMath.toDegrees(latitude)
    const lng = CesiumMath.toDegrees(longitude)
    const cartesian3 = Cartesian3.fromDegrees(lng, lat, height)
    return cartesian3
  }

  /**
   * 加载地形后, 获取点击处对应的笛卡尔坐标
   * @param {Array} position 屏幕坐标
   */
  getGlobeClickCartesian(position) {
    const pickRay = this.viewer.camera.getPickRay(position)
    if (!pickRay) return
    const cartesian3 = this.viewer.scene.globe.pick(pickRay, this.viewer.scene)
    return cartesian3
  }

  /**
   * 笛卡尔坐标转屏幕坐标
   * @param {*} cartesian3 三维笛卡尔空间直角坐标
   * @summary 结果是Cartesian2对象,取出X, Y即为屏幕坐标
   * @returns {Object} {x, y}
   */
  wgs84ToWindowCoordinates(cartesian3) {
    return SceneTransforms.wgs84ToWindowCoordinates(this.viewer.scene, cartesian3)
  }

  /**
   * 计算区域中心点
   * @param geoCoordinateList {Array<Array<Object>>} [[{lat, lon}]]
   * @return { Object } {lat lon}
   */
  getBoundsCenter(geoCoordinateList, height = 5000000) {
    const geoCoordinateListFlat = geoCoordinateList.reduce((s, v) => {
      return (s = s.concat(v))
    }, [])
    const total = geoCoordinateListFlat.length
    let X = 0
    let Y = 0
    let Z = 0
    for (const g of geoCoordinateListFlat) {
      const lat = g.lat * Math.PI / 180
      const lon = g.lon * Math.PI / 180
      const x = Math.cos(lat) * Math.cos(lon)
      const y = Math.cos(lat) * Math.sin(lon)
      const z = Math.sin(lat)
      X += x
      Y += y
      Z += z
    }

    X = X / total
    Y = Y / total
    Z = Z / total
    const Lon = Math.atan2(Y, X)
    const Hyp = Math.sqrt(X * X + Y * Y)
    const Lat = Math.atan2(Z, Hyp)

    return { lon: Lon * 180 / Math.PI, lat: Lat * 180 / Math.PI, height }
  }
}

5、飞行、定位

Camera相机控制了三维场景的视图。旋转(rotate)、缩放(zoom)和飞到目的地(flyTo)

heading:偏航角(弧度),绕负Z轴旋转,顺时针为正,默认为正北方向0,可简单理解成左右方向的改变。

pitch:俯仰角(弧度),绕负Y轴旋转,顺时针为正,默认为俯视-90,可简单理解成前空翻、后空翻。

roll:翻滚角(弧度),绕正x轴旋转,顺时针为正,默认为0,可简单理解成侧空翻。

import {
  HeadingPitchRoll
} from 'cesium'

/**
 * 相机操作
 * @summary 包括飞行、定位
 * @see https://zhuanlan.zhihu.com/p/351731187
 * @see https://cesium.com/learn/cesiumjs/ref-doc/Camera.html
 */
export default class Camera {
  /**
   * 飞行定位到Entity、DataSource、ImageryLayer、Cesium3DTileset等
   * @param {Object} target 目标 如Entity
   */
  flyTo(options = {}) {
    const { target, duration, heading = 0, pitch = -90, roll = 0 } = options

    this.viewer.flyTo(target, {
      duration,
      offset: new HeadingPitchRoll(heading, pitch, roll)
    })
  }

  /**
   * 快速定位
   * @param {Number} lng 经度
   * @param {Number} lat 纬度
   * @param {Number} height 高程
   */
  setView({ lng = 113.281023, lat = 23.129487, height = 300000 }) {
    this.viewer.camera.setView({
      destination: this.fromDegrees([lng, lat, height])
    })
  }

  /**
   * 飞行定位到指定位置
   * @param {Number} lng 经度
   * @param {Number} lat 纬度
   * @param {Number} height
   */
  flyToPosition(position = {}, options) {
    const { lng = 113.281023, lat = 23.129487, height = 300000, heading = 0, pitch = -90, roll = 0 } = position

    const _lat = pitch === -50 ? lat - 2 : lat

    this.viewer.camera.flyTo({
      destination: this.fromDegrees([lng, _lat, height]),
      orientation: {
        heading: this.toRadians(heading),
        pitch: this.toRadians(pitch),
        roll: this.toRadians(roll)
      },
      ...options
    })
  }
}

6、3D Tiles加载

切换二维视图、哥伦布视图、三维视图

加载3D Tiles(Cesium 加载海量三维模型数据的一种数据格式)

import {
  Cesium3DTileset,
  Cartesian3,
  Cartographic,
  Matrix4
} from 'cesium'

/**
 * 场景操作
 * @see https://cesium.com/learn/cesiumjs/ref-doc/Scene.html
 */
export default class Scene {
  get sceneMode() {
    return this.viewer.scene.mode
  }

  /**
   * 切换场景视图
   * @param {Number} mode 视图类型 2:二维视图,2.5:哥伦布视图,3:三维视图
   * @param {Number} duration 过渡时间
   */
  changeSceneMode(mode, duration) {
    switch (mode) {
      case 2:
        return this.viewer.scene.morphTo2D(duration)
      case 2.5:
        return this.viewer.scene.morphToColumbusView(duration)
      default:
        return this.viewer.scene.morphTo3D(duration)
    }
  }

  /**
   * 加载三维模型
   * @param {String} url 三维模型地址
   * @param {Number} height 三维模型高度
   */
  add3DTiles({ url, height = 40 }) {
    this.tileset = this.viewer.scene.primitives.add(
      new Cesium3DTileset({
        url
        // maximumScreenSpaceError: 2, //最大的屏幕空间误差
        // maximumNumberOfLoadedTiles: 1000, //最大加载瓦片个数
      })
    )

    this.tileset.readyPromise.then(tileset => {
      this.set3DTilesHeight(tileset, height)
    })

    return this.tileset
  }

  remove3DTiles() {
    if (this.tileset) {
      this.tileset.destroy()
      this.viewer.scene.primitives.remove(this.tileset)
    }
  }

  /**
   * 设置3DTiles高度
   * @param {Object} tileset 3DTiles
   * @param {Number} height 高度
   */
  set3DTilesHeight(tileset, height) {
    const boundingSphere = tileset.boundingSphere
    console.log('boundingSphere', boundingSphere)
    const cartographic = Cartographic.fromCartesian(boundingSphere.center)
    // 调整模型高度
    const surface = Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, 0.0)
    const offset = Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, height)
    const translation = Cartesian3.subtract(offset, surface, new Cartesian3())
    tileset.modelMatrix = Matrix4.fromTranslation(translation)
  }
}

7、Event 事件

无论是前端系统,还是二维/三维GIS应用系统,都离不开各种事件的应用,尤其是鼠标的单击、双击事件。

鼠标事件:点击地图上的某一个矢量数据,并获取其属性信息。

场景渲染事件:

scene.preUpdate: 更新或呈现场景之前将引发的事件

scene.postUpdate: 场景更新后以及渲染场景之前立即引发的事件

scene.preRender: 场景更新后以及渲染场景之前将引发的事件,一般用来获取相机当前的状态,如相机高度、偏航角、俯仰角、翻滚角等

scene.postRender: 渲染场景后立即引发的事件

import {
  ScreenSpaceEventHandler,
  ScreenSpaceEventType
} from 'cesium'

export default class Event {
  constructor() {
    this.popupPosition = null // 弹框位置
    this.clickHandler = null // 鼠标点击事件
    this.scenePostRender = null // 监听cesium渲染

    this.overlayers = []
  }

  /**
   * 添加气泡框
   * @param {String} element 元素id
   * @param {String} positioning 容器基于鼠标点的定位
   * @param {Array} offset 容器的偏移
   */
  addPopup(name, positioning, element = 'Overlay', offset = [0, 0], callback) {
    const $el = document.getElementById(element)

    // 开启监听
    if (!this.scenePostRender) {
      this.listenPostRender(element, positioning, offset)
    }

    // 清空鼠标事件
    if (this.clickHandler) {
      this.clickHandler.destroy()
      this.clickHandler = null
    }

    this.viewer._container.style.cursor = 'default'

    // 添加鼠标事件
    this.clickHandler = new ScreenSpaceEventHandler(this.viewer.scene.canvas)

    // 鼠标左键点击
    this.clickHandler.setInputAction(clickEvent => {
      const pick = this.viewer.scene.pick(clickEvent.position)
      // 判断点击的是否为当前图层的Billboard
      if (pick && pick.collection && pick.collection.name === name) {
        // 转屏幕坐标
        const position = this.wgs84ToWindowCoordinates(pick.primitive.position)

        // 保存当前鼠标点击的位置
        this.popupPosition = pick.primitive.position

        // 计算弹框的位置
        this.updatePopup($el, position, positioning, offset)

        typeof callback === 'function' && callback(pick.id)
      } else {
        this.popupPosition = null
        $el.style.cssText = 'display:none;'
      }
    }, ScreenSpaceEventType.LEFT_CLICK)

    // 鼠标移动
    this.clickHandler.setInputAction((movement) => {
      const pick = this.viewer.scene.pick(movement.endPosition)
      if (pick && pick.collection && pick.collection.name === name) {
        this.viewer._container.style.cursor = 'pointer'
      } else {
        this.viewer._container.style.cursor = 'default'
      }
    }, ScreenSpaceEventType.MOUSE_MOVE)
  }

  /**
   * 隐藏气泡框
   * @param {String} element 元素id
   */
  hidePopup(element) {
    const $el = document.getElementById(element)
    if ($el) {
      this.popupPosition = null
      $el.style.cssText = 'display:none;'
    }
  }

  /**
   * 更新弹窗位置
   * @param {Element} $el 弹窗dom
   * @param {Object} position 屏幕坐标
   * @param {String} positioning 容器基于鼠标点的定位
   * @param {Array} offset 容器的偏移
   */
  updatePopup($el, { x, y }, positioning, [offsetX, offsetY]) {
    // console.log(x, y)
    switch (positioning) {
      case 'left':
        $el.style.cssText += `display:block; top:${y + $el.offsetHeight / 2 + offsetY}px; left:${x - $el.offsetWidth + offsetX}px`
        break
      case 'right':
        $el.style.cssText += `display:block; top:${y - $el.offsetHeight / 2 + offsetY}px; left:${x + offsetX}px`
        break
      case 'bottom':
        $el.style.cssText += `display:block; top:${y + offsetY}px; left:${x - $el.offsetWidth / 2 + offsetX}px`
        break
      default:
        $el.style.cssText += `display:block; top:${y - $el.offsetHeight + offsetY}px; left:${x - $el.offsetWidth / 2 + offsetX}px`
    }
  }

  listenPostRender(element, positioning, offset) {
    // 刷新弹框位置
    this.scenePostRender = this.viewer.scene.postRender.addEventListener(() => {
      if (this.popupPosition) {
        const position = this.wgs84ToWindowCoordinates(this.popupPosition)
        if (!position) return
        const $el = document.getElementById(element)
        // 计算弹框的位置
        this.updatePopup($el, position, positioning, offset)
      }
    })
  }
}

推荐阅读

恰饭区

评论区 (0)

0/500

还没有评论,快来抢第一吧