Vue3 Cesium 的一些使用记录

107 阅读8分钟

最近项目涉及到了一些 Cesium 相关的场景,所以花了一些时间研究了一下 Cesium 的基本使用,下面是一些有关 Cesium 基本使用的记录。

快速开始

安装

pnpm add cesium vite-plugin-cesium

vite配置

import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
import cesium from 'vite-plugin-cesium'

// https://vite.dev/config/
export default defineConfig({
  plugins: [vue(), vueDevTools(), cesium()],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
})

初始化Cesium地球

初始化前,需要注意的一点是,Cesium 的 Viewer 对象是一个复杂的 WebGL 实例,包含大量的内部状态和渲染逻辑。而 vue 的响应式会自动追踪数据的变化并触发组件的重新渲染,如果将它作为响应式对象放入 vue 中,就会导致性能问题,甚至在复杂场景中直接卡死。

所以在初始化时,应该使用 let 来声明 Viewer 对象,而不是将其放入 Vue 的 ref 或者 reactive 中。

image.png

<template>
  <div class="cesium">
    <div class="cesium-container" id="cesiumContainer"></div>
  </div>
</template>

<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue'
import * as Cesium from 'cesium'
import 'cesium/Build/Cesium/Widgets/widgets.css'

let viewer: Cesium.Viewer | null = null
//Cesium 平台的 accessToken
const cesiumToken = import.meta.env.VITE_CESIUM_TOKEN

onMounted(() => {
  initCesium()
})

onUnmounted(() => {
  if (viewer && !viewer.isDestroyed()) {
    viewer.destroy()
  }
})

function initCesium() {
  Cesium.Ion.defaultAccessToken = cesiumToken

  const options: { [x: string]: boolean } = {
    geocoder: false, // 地理编码控件不显示
    // homeButton: false, // 默认相机位置控件不显示
    // sceneModePicker: false, // 场景模式控件不显示
    baseLayerPicker: false, // 基础图层控件不显示
    navigationHelpButton: false, // 导航帮助控件不显示
    animation: false, // 动画控件不显示
    timeline: false, // 时间线控件不显示
    // fullscreenButton: false, // 全屏控件不显示
    // vrButton: false, // VR控件不显示
    // infoBox: false, // 信息框控件不显示,点击要素后不弹出信息栏
    // selectionIndicator: false, // 选择跟踪控件不显示,点击要素后不弹出锁定框
  }
  viewer = new Cesium.Viewer('cesiumContainer', options)

  //隐藏 iframe 警告
  const iframe = document.getElementsByClassName('cesium-infoBox-iframe')[0]
  iframe?.setAttribute('sandbox', 'allow-same-origin allow-scripts allow-popups allow-forms')
  iframe?.setAttribute('src', '')
}
</script>

<style lang="scss" scoped>
.cesium {
  position: relative;
  height: 100%;
  width: 100%;
  display: flex;
}
.cesium-container {
  position: relative;
  height: 100vh;
  width: 100%;
  :deep(.cesium-viewer-bottom) {
    display: none;
  }
}
</style>

地图瓦片底图

在Cesium中,可以通过Cesium.UrlTemplateImageryProvider对象,利用URL模板加载地图瓦片数据,随后将该对象传递给Cesium.ImageryLayer放置在 Cesium 的图层上,从而实现地图瓦片的加载与显示。

高德瓦片

电子地图

const gaodeLayer = new Cesium.ImageryLayer(
  new Cesium.UrlTemplateImageryProvider({
    url: "https://webrd02.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}",
    minimumLevel: 1,
    maximumLevel: 18,
  })
);
viewer.imageryLayers.add(gaodeLayer);

卫星影像+标签

const gdBasicLayer = new Cesium.ImageryLayer(
  new Cesium.UrlTemplateImageryProvider({
    url: "https://webst02.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}",
    style: "default",
    format: "image/png",
    tileMatrixSetID: "GoogleMapsCompatible",
  })
);

// 加载高德地图影像地理标签
const gdLabelLayer = new Cesium.ImageryLayer(
  new Cesium.UrlTemplateImageryProvider({
    url: "http://webst02.is.autonavi.com/appmaptile?x={x}&y={y}&z={z}&lang=zh_cn&size=1&scale=1&style=8",
    style: "default",
    format: "image/jpeg",
    tileMatrixSetID: "GoogleMapsCompatible",
  })
);

viewer.imageryLayers.add(gdBasicLayer);
viewer.imageryLayers.add(gdLabelLayer);

卫星路网

const gaodeLayer = new Cesium.ImageryLayer(
  new Cesium.UrlTemplateImageryProvider({
    url: "https://wprd01.is.autonavi.com/appmaptile?x={x}&y={y}&z={z}&lang=zh_cn&size=1&scl=2&style=8&type=11",
    minimumLevel: 1,
    maximumLevel: 18,
  })
);
viewer.imageryLayers.add(gaodeLayer);

卫星路网+标签

const gaodeLayer = new Cesium.ImageryLayer(
  new Cesium.UrlTemplateImageryProvider({
    url: "https://wprd01.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}",
    minimumLevel: 1,
    maximumLevel: 18,
  })
);
viewer.imageryLayers.add(gaodeLayer);

地图纠偏

在 Cesium 中,使用的是 世界大地坐标系(WGS-84),在一些第三方地图中则使用了不同的坐标系,这就会在引入地图瓦片的时候导致地图地图的贴图不准确,这个时候我们就需要对地图瓦片进行纠偏。

UrlTemplateImageryProvider用于加载基于URL模板的影像数据,而tilingScheme定义了影像的分块方式。WebMercatorTilingScheme是Cesium中常用的分块方案,用于Web Mercator投影的影像数据。

通过重载其内部的_projection.project_projection.unproject方法,就可以实现从世界大地坐标系(WGS-84)到火星坐标系(GCJ-02)的转换,以及从火星坐标系到世界大地坐标系的转换。

火星坐标系和世界大地坐标系的相关说明:

  • 世界大地坐标系(WGS-84) :GPS使用的坐标系,是国际上通用的地理坐标系。
  • 火星坐标系(GCJ-02) :中国使用的加密坐标系,用于地图数据的偏移处理。
  • 纠偏:将火星坐标系的坐标转换回世界大地坐标系,或者将世界大地坐标系的坐标转换为火星坐标系。

高德纠偏

高德地图采用中国国家测绘局制订的地理信息系统的坐标系GCJ-02,即火星坐标系,它是在WGS84坐标系的基础上进行一次加密。因此Cesium在加载高德地图服务底图时,会存在偏差和纠偏的问题。

首先我们需要引入用于坐标系转换的 gcoord 库

pnpm add gcoord

然后通过 AmapMercatorTilingScheme 重载类继承 WebMercatorTilingScheme 类,在_projection.project_projection.unproject方法中重写坐标, 完成对坐标的纠偏

import {
  WebMercatorProjection,
  WebMercatorTilingScheme,
  Math,
  Cartographic,
  Cartesian2,
  Cartesian3,
} from 'cesium'
import gcoord from 'gcoord'

class AmapMercatorTilingScheme extends WebMercatorTilingScheme {
  constructor() {
    super()
    const projection = new WebMercatorProjection()
    this._projection.project = function (
      cartographic: { longitude: number; latitude: number },
      result,
    ) {
      //WGS84转GCJ02坐标
      result = gcoord.transform(
        [Math.toDegrees(cartographic.longitude), Math.toDegrees(cartographic.latitude)],
        gcoord.WGS84,
        gcoord.GCJ02,
      )
      result = projection.project(
        new Cartographic(Math.toRadians(result[0]), Math.toRadians(result[1])),
      )
      return new Cartesian2(result.x, result.y)
    }
    this._projection.unproject = function (cartesian: Cartesian3, result) {
      const cartographic = projection.unproject(cartesian)
      //GCJ02转WGS84坐标
      result = gcoord.transform(
        [Math.toDegrees(cartographic.longitude), Math.toDegrees(cartographic.latitude)],
        gcoord.GCJ02,
        gcoord.WGS84,
      )

      return new Cartographic(Math.toRadians(result[0]), Math.toRadians(result[1]))
    }
  }
}

export default AmapMercatorTilingScheme
const gaodeLayer = new Cesium.ImageryLayer(
  new Cesium.UrlTemplateImageryProvider({
    url: gaodeUrl,
    minimumLevel: 1,
    maximumLevel: 18,
    // tilingScheme 传入 AmapMercatorTilingScheme 重载类
    tilingScheme: new AmapMercatorTilingScheme(),
  })
);

纠偏与坐标转换

需要注意的是,虽然 Cesium 瓦片地图通过坐标系转换的方式完成了纠偏,但是这是在视觉上把地图贴到地球的对应位置了,但在实际获取 Cesium 的经纬度坐标时,我们得到还是 世界大地坐标系(WGS-84) 的坐标。

同时,我们的想要的地理位置信息是通过高德坐标获取的,所以还得这个坐标转换回 火星坐标系(GCJ-02) 去进行地址逆编码。这里我们可以使用上述中提到的 gcoord 库进行转换

const wgs84Coord = [116.397695, 39.904655];
const gcj02Coord = gcoord.transform(wgs84Coord, gcoord.GCJ02, gcoord.WGS84);

相对的,如果我们从数据接口获取到的高德坐标,如果需要在 Cesium 中标记出来,也需要先转换为 世界大地坐标系(WGS-84)

const gcj02Coord = [116.397695, 39.904655];
const wgs84Coord = gcoord.transform(gcj02Coord, gcoord.GCJ02, gcoord.WGS84);

是不是看着有点晕?没事,我一个头两个大。一开始我还以为实现的重载类能够自行处理偏移问题,但是实际使用才知道还得自己转换。在处理业务的时候很可能会忘记转换,所以建议把这层转换逻辑内部化,封装一层方法处理。

或者...换个 WGS-84 的地图底图吧

飞行漫游

在Cesium中,通过Cesium.Camera.flyTo()函数可实现从当前位置平滑飞行至目标点。其中,destination参数用于指定目标点的坐标位置,而orientation参数则用于定义相机在到达目标点后的摆放姿态。

orientation 对象中,能够指定相机的三个参数

  • Heading:表示相机绕Up轴(Z轴)旋转,这里可以简单理解为头往左右看
  • Pitch:表示相机绕 Right 轴(Y轴)旋转,这里可以简单理解为头往上下看
  • Roll:表示相机绕Direction轴(X轴)旋转,,这里可以简单理解为歪着头看

此外,如果不指定 orientation 对象,则会默认保持(0.0,-90.0,0.0)的相机摆放姿势,效果就是垂直拍摄地面。

飞行至坐标点

这里以上海市的人民政府坐标点为例,通过 Cesium.Cartesian3.fromDegrees() 方法传入 x,y,z 坐标轴,地图会在载入后逐渐飞往这个位置。此时俯仰角(pitch)设置为-90度,视角与地面垂直,效果就和平时看到的二维地图一样。

viewer.camera.flyTo({
  destination: Cesium.Cartesian3.fromDegrees(121.473734, 31.23078, 5000.0),
  orientation: {
    heading: Cesium.Math.toRadians(0.0),
    pitch: Cesium.Math.toRadians(-90.0),
    roll: Cesium.Math.toRadians(0.0),
  },
});

飞行至某区域

如果需要飞至指定区域,那么可以使用 Cesium.Rectangle.fromDegrees 方法,通过传入该区域的两个对角坐标点来定义目标区域。

viewer.camera.flyTo({
  destination: Cesium.Rectangle.fromDegrees(
    113.683333,
    29.966667,
    115.083333,
    31.366667
  ),
});

飞行至某区域后,再次飞行

上述飞行参数中,实际上我们传入的是一个 option 对象,如果我们将这个参数对象单独拿出来,就是下面的形式

const flyOptions: {
  destination: Cesium.Rectangle | Cesium.Cartesian3,
  complete?: Cesium.Camera.FlightCompleteCallback,
} = {
  destination: Cesium.Cartesian3.fromDegrees(121.473734, 31.23078, 5000.0),
};

viewer.camera.flyTo(flyOptions);

而在每个 flyOptions 配置项中。还支持绑定一个 complete 事件,这个事件在飞行完成后会被调用。也就是说,我们可以在这个函数中再次执行飞行操作,这样我们就能够达到连续飞行的效果。

//第一次飞行到坐标点
const flyFirstOptions: {
  destination: Cesium.Rectangle | Cesium.Cartesian3,
  complete?: Cesium.Camera.FlightCompleteCallback,
} = {
  destination: Cesium.Cartesian3.fromDegrees(121.473734, 31.23078, 5000.0),
};

//第二次飞行到坐标区域
const flySecondOptions: {
  destination: Cesium.Rectangle | Cesium.Cartesian3,
  complete?: Cesium.Camera.FlightCompleteCallback,
} = {
  destination: Cesium.Rectangle.fromDegrees(
    113.683333,
    29.966667,
    115.083333,
    31.366667
  ),
};

//开始飞行
viewer.camera.flyTo(flyFirstOptions);
//连续飞行
flyFirstOptions.complete = () => {
  setTimeout(() => {
    viewer.camera.flyTo(flySecondOptions);
  }, 1000);
};
// flySecondOptions.complete = () => { ...}

视点移动

在 Cesium 中,Cesium.Camera.setView() 方法用于立即设置相机的视图,没有动画过渡效果,而是瞬间切换到目标位置。这种方法适用于需要快速定位到特定区域的场景,例如用户通过按钮直接跳转到某个地点。

Cesium.Camera.setView() 方法和上述的Cesium.Camera.flyTo()方法的参数和使用方法完全一致。

orientation 对象中,能够指定相机的三个参数

  • Heading:表示相机绕Up轴(Z轴)旋转,这里可以简单理解为头往左右看
  • Pitch:表示相机绕 Right 轴(Y轴)旋转,这里可以简单理解为头往上下看
  • Roll:表示相机绕Direction轴(X轴)旋转,,这里可以简单理解为歪着头看

如果不指定 orientation 对象,则会默认保持(0.0,-90.0,0.0)的相机摆放姿势,效果就是垂直拍摄地面。

显示坐标点

viewer.camera.setView({
  destination: Cesium.Cartesian3.fromDegrees(121.473734, 31.23078, 5000.0),
});

显示某区域

viewer.camera.setView({
  destination: Cesium.Rectangle.fromDegrees(
    113.683333,
    29.966667,
    115.083333,
    31.366667
  ),
});

相关参考

cesium加载高德地图并纠偏:blog.csdn.net/hongxianqia…

Cesium信息框不能执行js语句解决办法: blog.csdn.net/F_efforts/a…

Cesium案例解析(三)——Camera相机:www.cnblogs.com/charlee44/p…

Cesium基础(七):Camera(相机)常用的API及飞行漫游:juejin.cn/post/753347…

Cesium中常用到的5种相机定位方法详解,每种都适用于不同的场景:zhuanlan.zhihu.com/p/713953647

Cesium中的相机—setView&lookAtTransform: blog.csdn.net/u011575168/…