最近项目涉及到了一些 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 中。
<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/…