效果图
使用vue3 + OpenTiny UI + cesium 实现三维地球
-
node.js >= v16.0
-
opentiny vue3 ui安装指南 https://opentiny.design/tiny-vue/zh-CN/os-theme/docs/installation yarn add @opentiny/vue@3
项目依赖
"dependencies": {
"@opentiny/vue": "3",
"core-js": "^3.8.3",
"vue": "^3.2.13",
"vue-router": "4",
"cesium": "^1.99.0",
"cesium-navigation-es6": "^3.0.8"
}
模块化代码
main.js
import { createApp } from 'vue'
import App from './App.vue'
// 引入 @opentiny/vue 组件
import TinyVue from '@opentiny/vue'
import Cesium from 'cesium'
// 创建并挂载根实例
const app = createApp(App)
// 注册 @opentiny/vue 组件
app.use(TinyVue)
app.use(Cesium)
app.mount('#app')
App.vue
<template>
<gis></gis>
</template>
<script>
import gis from './components/EarthGis.vue'
export default {
name: 'App',
components: {
gis
},
data() {
return {
}
},
mounted(){
// 在“about:blank”中阻止脚本执行,因为文档的框架已被沙盒化并且未设置“allow-scripts”权限。
let iframe = document.getElementsByClassName('cesium-infoBox-iframe')[0];
iframe.setAttribute('sandbox', 'allow-same-origin allow-scripts allow-popups allow-forms');
iframe.setAttribute('src', ''); // 必须设置src为空 否则不会生效。
}
}
</script>
<style>
body {
margin: 0;
padding: 0;
background-color: #e9edfa;
}
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
background-color: transparent;
}
#loading {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%);
z-index: 1;
}
.progressText {
text-align: center;
}
</style>
EarthGis.vue
<template>
<div class="content">
<tiny-layout :cols="12">
<tiny-row :gutter="10">
<tiny-col :span="12">
<div class="col" style="position: relative;">
<div class="select-menu">
<tiny-dropdown title="影像来源" size="medium" split-button @item-click="selectChange">
<template #dropdown>
<tiny-dropdown-menu popper-class="select-item">
<tiny-dropdown-item
v-for="(item, index) in imageryLayersOptions"
:key="index"
:label="item.label"
:item-data="item.value"
></tiny-dropdown-item>
</tiny-dropdown-menu>
</template>
</tiny-dropdown>
</div>
<div id="cesium-container"></div>
</div>
</tiny-col>
</tiny-row>
</tiny-layout>
</div>
</template>
<script type="allow-scripts">
import 'cesium/Source/Widgets/widgets.css'
import { Layout, Row, Col, Dropdown, DropdownMenu, DropdownItem } from '@opentiny/vue'
import { World } from './js/World/World.js'
export default {
name: 'EarthGis',
props: {
msg: String
},
components: {
TinyDropdown: Dropdown,
TinyDropdownMenu: DropdownMenu,
TinyDropdownItem: DropdownItem,
TinyLayout: Layout,
TinyRow: Row,
TinyCol: Col
},
watch: {
isLoading: function (val) {
document.getElementById('loading').style.display = val ? 'black' : 'none'
}
},
data() {
return {
isLoading: true,
imageryLayersOptions: [{
value: 'SingleTile',
label: 'SingleTileImageryProvider'
}],
world: null
}
},
methods: {
selectChange(data) {
let item = data.itemData
switch (item) {
case 'SingleTile':
this.world.changeImagery()
break
default:
break
}
}
},
mounted() {
// 1. Create an instance of the World app
this.world = new World('cesium-container');
this.$nextTick(() => {
this.selectChange({itemData: 'SingleTile'})
})
},
destroy(){
this.removeNavigation()
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
#cesium-container{
width: 100vw;
height: 100vh;
}
.select-menu{
position: absolute;
top: 1rem;
left: 1rem;
background: #ffffff66;
border-radius: 5px;
padding: 10px;
}
.select-item{
top: 1rem;
left: 1rem;
background: #ffffff66;
border-radius: 5px;
padding: 10px;
}
</style>
World.js
import { createViewer } from './components/viewer.js'
import { createImageryLayer } from './components/imageryLayer.js'
import { createTerrain } from './components/terrain.js'
import { createNavigation } from './components/navigation.js'
import { createCamera } from './components/camera.js';
import darkEarth from '@/assets/images/darkEarth.jpg'
import { FeatureDetection,
DirectionalLight,
Cartesian3,
JulianDate,
Math,
CameraEventType,
KeyboardEventModifier,
Cesium3DTileset,
HeadingPitchRange,
Matrix4,
Cartographic,
ScreenSpaceEventType,
SingleTileImageryProvider,
WebMercatorTilingScheme,
ScreenSpaceEventHandler,
WebMapTileServiceImageryProvider,
GeographicTilingScheme,
ImageryLayer,
UrlTemplateImageryProvider,
OpenStreetMapImageryProvider,
IonImageryProvider,
createOsmBuildings,
Rectangle } from 'cesium'
// These variables are module-scoped: we cannot access them
// from outside the module
let viewer;
// 添加主题图层相关配置
let layerOption = {
show: true, // 图像层是否可见
alpha: 0.6, // 透明度
nightAlpha: 1, // 地球夜晚一侧的透明度
dayAlpha: 1, // 地球白天一侧的透明度
brightness: 1, // 亮度
contrast: 1, // 对比度
hue: 0, // 色调
saturation: 1, // 饱和度
gamma: 1, // 伽马校正
}
class World {
// 1. Create an instance of the World app
constructor(id) {
viewer = createViewer(id)
viewer.imageryLayers.removeAll(true)
}
init(){
// 修改场景环境,关闭相关特效
viewer.scene.debugShowFramesPerSecond = true// 显示fps
viewer.scene.moon.show = true// 月亮
viewer.scene.fog.enabled = true// 雾
viewer.scene.sun.show = true// 太阳
viewer.scene.skyBox.show = true// 天空盒
viewer.scene.globe.enableLighting = true // 激活基于太阳位置的光照(场景光照)
viewer.resolutionScale = 1// 画面细度,默认值为1.0
// 不显示cesium icon版权信息
viewer._cesiumWidget._creditContainer.style.display="none"
viewer.geocoder._form.children[0].placeholder = "请输入关键字"
// DirectionalLight 表示 从无限远的地方向单一方向发射的光。解决模型光照问题
viewer.scene.light = new DirectionalLight({
direction: new Cartesian3(0.354925, -0.890918, -0.283358)
})
viewer.clock.currentTime = JulianDate.addHours(
JulianDate.now(new Date()),
12,
new JulianDate()
)
// 启用深度测试,使地形后面的东西消失。
viewer.scene.globe.depthTestAgainstTerrain = true
viewer.scene.fxaa = false
viewer.scene.postProcessStages.fxaa.enabled = true
// 水雾特效
viewer.scene.globe.showGroundAtmosphere = true
// 设置最大俯仰角,[-90,0]区间内,默认为-30,单位弧度
viewer.scene.screenSpaceCameraController.constrainedPitch = Math.toRadians(-20)
viewer.scene.screenSpaceCameraController.autoResetHeadingPitch = false
viewer.scene.screenSpaceCameraController.inertiaZoom = 0.5
viewer.scene.screenSpaceCameraController.minimumZoomDistance = 50
viewer.scene.screenSpaceCameraController.maximumZoomDistance = 20000000
viewer.scene.screenSpaceCameraController.zoomEventTypes = [
CameraEventType.RIGHT_DRAG,
CameraEventType.WHEEL,
CameraEventType.PINCH
]
viewer.scene.screenSpaceCameraController.tiltEventTypes = [
CameraEventType.MIDDLE_DRAG,
CameraEventType.PINCH,
{
eventType: CameraEventType.LEFT_DRAG,
modifier: KeyboardEventModifier.CTRL
},
{
eventType: CameraEventType.RIGHT_DRAG,
modifier: KeyboardEventModifier.CTRL
}
]
// 开启抗锯齿
if (FeatureDetection.supportsImageRenderingPixelated()) {
// 判断是否支持图像渲染像素化处理
viewer.resolutionScale = window.devicePixelRatio
}
// 添加默认图层
createImageryLayer()
// 开启Navigation导航插件
createNavigation(viewer)
// 添加cesium自带的地形
createTerrain(viewer)
// 将三维球定位到中国
viewer.camera.flyTo({
destination: Cartesian3.fromDegrees(103.84, 31.15, 17860000),
orientation: {
heading: Math.toRadians(348.4202942851978),
pitch: Math.toRadians(-89.74026687972041),
roll: Math.toRadians(0)
},
complete: () => {
// 定位完成之后的回调函数
console.log('定位完成')
document.getElementById('loading').style.display = 'none'
}
})
// 设置默认的视角为中国
createCamera.DEFAULT_VIEW_RECTANGLE = Rectangle.fromDegrees(
// 西边经度
89.5,
// 南边维度
20.4,
// 东边经度
110.4,
// 北边维度
61.2
)
//this.cion(layerOption)
//this.osm(layerOption)
this.hot(layerOption)
//this.cartoVoyager(layerOption)
//this.cartoDark(layerOption)
//this.stamen(layerOption)
//this.wmtsImages(layerOption)
//this.osmBuildings()
// const tilesetOption = {
// skipLevelOfDetail: true,
// baseScreenSpaceError: 1024,
// skipScreenSpaceErrorFactor: 16,
// skipLevels: 1,
// immediatelyLoadDesiredLevelOfDetail: false,
// loadSiblings: false,
// cullWithChildrenBounds: true
// }
// const modelPromise = this.addThreeDTiles(69380, tilesetOption);
// modelPromise.then(model => {
// console.log('tileset: ', model)
// this.setPosition(model, 113.27, 23.13, 10) // 调整位置到,高度10米
// this.setPosition(model, undefined, undefined, 500) // 仅修改高度至500米
// this.serMatrix(model) // 使用默认变换矩阵(单位向量),实现回到默认位置的效果
// })
}
// 切换图层
changeImagery() {
viewer.imageryLayers.removeAll(true)
this.roaming()
}
async addThreeDTiles(url, option) {
// 开启地形深度检测:
// 控制在渲染场景时,相机是否进行深度测试以避免将被遮挡的物体绘制在前景
// true: 相机会根据地形高度信息进行深度测试,避免将低于地面的物体绘制在地面之上
viewer.scene.globe.depthTestAgainstTerrain = true
let tileset = {}
if (typeof url == 'number') {
tileset = await Cesium3DTileset.fromIonAssetId(url, option);
} else {
tileset = await Cesium3DTileset.fromUrl(url, option);
}
viewer.scene.primitives.add(tileset);
// 定位到模型
viewer.zoomTo(
tileset,
new HeadingPitchRange(
0.0,
-0.5,
tileset.boundingSphere.radius * 2.0 // 模型的包围球半径的2倍
)
)
return tileset // 返回模型对象
}
setPosition(tileset, lng, lat, h) {
// 计算出模型包围球的中心点(弧度制),从世界坐标转弧度制
const cartographic = Cartographic.fromCartesian(
tileset.boundingSphere.center
)
const { longitude, latitude, height } = cartographic
// 模型包围球的中心点坐标,输出以笛卡尔坐标系表示的三维坐标点
const current = Cartesian3.fromRadians(
longitude,
latitude,
height
)
// 新的位置的中心点坐标,输出以笛卡尔坐标系表示的三维坐标点
const offset = Cartesian3.fromDegrees(
lng || Math.toDegrees(longitude),
lat || Math.toDegrees(latitude),
h || height
);
// 计算差向量:计算tileset的平移量,并将其应用到modelMatrix中
const translation = Cartesian3.subtract(
offset,
current,
new Cartesian3()
)
// 修改模型的变换矩阵,通过四维矩阵
tileset.modelMatrix = Matrix4.fromTranslation(translation);
viewer.zoomTo(tileset);
}
//Resets the position of a tileset to a specified model matrix or the identity matrix if none is provided.
serMatrix(tileset, matrix) {
tileset.modelMatrix = matrix || Matrix4.IDENTITY
viewer.zoomTo(tileset);
}
showAllImagery(boolean = true) {
// 获取图像图层集合
const imageryLayers = viewer.imageryLayers;
// 遍历图像图层并隐藏它们
let numLayers = imageryLayers.length;
for (let i = 0; i < numLayers; i++) {
const layer = imageryLayers.get(i); // 获取图像图层对象
layer.show = boolean; // 设置图像图层隐藏
}
}
async roaming() {
let isRoaming = true; // 漫游标志位
const DEFAULT_LIGHTING = viewer.scene.globe.enableLighting; // 默认光照状态
const DEFAULT_SKY_ATMOSPHERE = viewer.scene.skyAtmosphere.show; // 默认光照状态
let bgImglayer; // 地球底图
this.showAllImagery(false); // 隐藏所有图层
viewer.clock.multiplier = -2000.0; // 时间加速
const provider = await SingleTileImageryProvider.fromUrl(darkEarth);
provider._tilingScheme = new WebMercatorTilingScheme()
bgImglayer = viewer.imageryLayers.addImageryProvider(provider); // 加载背景底图
if (!DEFAULT_LIGHTING) {
viewer.scene.globe.enableLighting = true; // 开启光照
}
if (!DEFAULT_SKY_ATMOSPHERE) {
viewer.scene.skyAtmosphere.show = true; // 开启天空大气,能在一定程度上降低地球轮廓锯齿
}
// 添加鼠标事件,触发后停止漫游效果
const handler = new ScreenSpaceEventHandler(viewer.scene.canvas); // 交互句柄
handler.setInputAction(() => {
isRoaming = false // 停止旋转
this.showAllImagery(true) // 显示图层
if (!DEFAULT_LIGHTING) {
viewer.scene.globe.enableLighting = false; // 关闭光照
}
if (!DEFAULT_SKY_ATMOSPHERE) {
viewer.scene.skyAtmosphere.show = false; // 关闭光照
}
viewer.imageryLayers.remove(bgImglayer, true); // 移除图层
viewer.clock.multiplier = 1; // 正常时间流速
handler.removeInputAction(ScreenSpaceEventType.LEFT_CLICK); // 移除鼠标事件监听
this.init()
}, ScreenSpaceEventType.LEFT_CLICK);
(function roamingEvent() {
if (isRoaming) {
// 控制相机围绕 Z 轴旋转
viewer.camera.rotate(Cartesian3.UNIT_Z, Math.toRadians(0.1));
requestAnimationFrame(roamingEvent);
}
})()
}
// Cesium ION 服務
cion (layerOption, id = 3812) {
const layer = new ImageryLayer(
new IonImageryProvider({ assetId: id }),
layerOption
)
viewer.imageryLayers.add(layer)
return layer
}
// 加载osm地图
osm (layerOption) {
const layer = new ImageryLayer(
new OpenStreetMapImageryProvider({ url: 'https://a.tile.openstreetmap.org/' }),
layerOption
)
viewer.imageryLayers.add(layer)
return layer
}
// 加载Humanitarian OpenStreetMap Team style地图
hot (layerOption) {
const layer = new ImageryLayer(
new UrlTemplateImageryProvider({ url: 'https://tile-{s}.openstreetmap.fr/hot/{z}/{x}/{y}.png', subdomains: ['a', 'b', 'c'] }),
layerOption
)
viewer.imageryLayers.add(layer)
return layer
}
// 加载carto Basemaps 航海风格地图
cartoVoyager (layerOption) {
const layer = new ImageryLayer(
new UrlTemplateImageryProvider({ url: 'https://basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png' }),
layerOption
)
viewer.imageryLayers.add(layer)
return layer
}
// 加载carto Basemaps 黑暗风格地图
cartoDark (layerOption) {
const layer = new ImageryLayer(
new UrlTemplateImageryProvider({ url: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png', subdomains: ['a', 'b', 'c', 'd'] }),
layerOption
)
viewer.imageryLayers.add(layer)
return layer
}
// 加载Stamen地图
stamen (layerOption) {
const layer = new ImageryLayer(
new UrlTemplateImageryProvider({ url: 'https://stamen-tiles.a.ssl.fastly.net/watercolor/{z}/{x}/{y}.png' }),
layerOption
)
viewer.imageryLayers.add(layer)
return layer
}
// 加载WMTS瓦片地图服务
wmtsImages () {
// EPSG:900913(标准名为EPSG:3875)网格切分的瓦片。当将tileMatrixSetID设置为 'EPSG:4326',访问常用的EPSG:4326网络的瓦片
// 访问GeoServer发布的地图瓦片服务 WebMapTileServiceImageryProvider的切片方案tilingScheme默认使用EPSG:3875投影,即伪墨卡托网格访问切片,与EPSG:4326网格的切片方案存在较大差异
// Tiling是一种椭球体表面上的几何图形或图像的平铺方案。在详细级别为0,即最粗、最不详细的级别上,瓦片的数量是可配置的。在详细级别为1级以上,每个是一级级瓦片经纬两个方向上扩展为两个瓦片,共有四个子瓦片。如此扩展到最大的缩放级别,这也构成了一个图像瓦片的金字塔。TillingScheme有一个参数ellipsoid用来决定切处时使用的椭球,另外两个比较重要的参数numberOfLevelZeroTilesX和numberOfLevelZeroTilesY,用来决定0级瓦片的数量。
// TilingSchemee有两个子类,为WebMercatorTilingScheme和GeographicTilingScheme。其中WebMercatorTilingScheme对应于EPSG:3857切片方案,常见于谷歌地图、微软必应地图以及大多数的ArcGIS在线地图,也是Cesium中默认的切片方案。
// GeographicTilingScheme对应于EPSG:4326切片方案,是一个简单的地理投影方案,可直接将经纬度映射为X和Y,这种投影通常被称为地理投影、等矩形投影、等距圆柱形投影等。
// 由于在X方向上,WebMercatorTilingScheme只有一个0级瓦片,而GeographicTilingScheme却有2个,这就导致了默认的EPSG:3857切片方案不能正确加载EPSG:4326切片方案的瓦片图像。
let layer = new WebMapTileServiceImageryProvider({
url : '/map/gwc/service/wmts/rest/xian:satellite/{style}/{TileMatrixSet}/{TileMatrixSet}:{TileMatrix}/{TileRow}/{TileCol}?format=image/png',
style : 'raster',
tileMatrixSetID : 'EPSG:4326',
tilingScheme: new GeographicTilingScheme(),
});
viewer.imageryLayers.addImageryProvider(layer);
return layer
}
// 载入OSM建筑物
osmBuildings = () => {
// 突出显示所有的商业和住宅建筑,以查看整个城市不同社区的模式
// Cesium OSM 建筑物通过3D Tiles,它可以在web上高效地流式传输和可视化。
// 3D Tiles是一个开放的标准,所以Cesium OSM建筑可以在任何兼容它的查看器中使用,不仅仅是开源的Cesium。
// Cesium全球3.5亿做建筑物,数据来源openStreetMap地图
//OpenStreetMap(简称OSM,中文是公开地图)是一个网上地图协作计划,目标是创造一个内容自由且能让所有人编辑的世界地图。
//其包含图层主要有高速公路、铁路、水系、水域、建筑、边界、建筑物等图层。我们不仅能够免费下载城市数据还可以下载全球数据。
//网址为https://www.openstreetmap.org/
//Cesium中支持使用OSM在线的建筑矢量三维模型,但目前OSM数据在国外较为细致,国内只支持几个大城市。
//由于OSM建筑数据量大,加载较慢,用户在使用建筑白膜时,可根据需求,在OSM官网或百度、高德等地图服务商中下载建筑矢量数据,
//使用ArcGIS等GIS软件和SketchUP等建模软件,生成建筑白膜,并使用建模软件对白膜进行贴图修改等操作,以实现城市建筑的美化,
//使用CesiumLab等软件对建模的三维建筑数据 进行切片生成3Dtiles等Cesium支持的数据类型,对其进行加载使用。
const addOSMAsync = () => {
try {
// 突出显示所有的商业和住宅建筑,以查看整个城市不同社区的模式
viewer.scene.primitives.add(createOsmBuildings())
} catch (error) {
console.log(`Failed to add world imagery: ${error}`);
}
};
addOSMAsync()
}
}
export { World };
viewer.js
import { Viewer, Ion } from 'cesium'
function createViewer(id) {
const cesiumToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJmNDVhMjQ0Yi05MDg4LTRlNDYtYTIxMi00YmI4ZWYxOWMyNTgiLCJpZCI6MTQ5MzYwLCJpYXQiOjE2ODc3ODQ0OTh9.KlhT_LCnsEtYyawpEmJ_wx44_qTUbgze-SxGMRavbAM'
Ion.defaultAccessToken = cesiumToken
const viewerOption = {
// 默认隐藏
infoBox: true, //是否显示信息框
animation:false, //左下角的动画仪表盘
baseLayerPicker:true, //右上角的图层选择按钮
geocoder:true, //搜索框
homeButton:true, //home按钮
sceneModePicker:true, //模式切换按钮
timeline:true, //底部的时间轴
navigationHelpButton:true, //右上角的帮助按钮
fullscreenButton:true, //右下角的全屏按钮
contextOptions:{
webgl:{
alpha:true
}
}
}
const instance = new Viewer(id, viewerOption);
return instance;
}
export { createViewer }
imageryLayer.js
import { ImageryLayer, UrlTemplateImageryProvider } from 'cesium'
// 添加主题图层相关配置
let layerOption = {
show: true, // 图像层是否可见
alpha: 0.6, // 透明度
nightAlpha: 1, // 地球夜晚一侧的透明度
dayAlpha: 1, // 地球白天一侧的透明度
brightness: 1, // 亮度
contrast: 1, // 对比度
hue: 0, // 色调
saturation: 1, // 饱和度
gamma: 1, // 伽马校正
}
function createImageryLayer( option=layerOption ) {
// 添加主题图层相关配置
const instance = new ImageryLayer(
new UrlTemplateImageryProvider({ url: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png', subdomains: ['a', 'b', 'c', 'd'] }),
option
)
return instance;
}
export { createImageryLayer }
camera.js
import { Camera } from 'cesium'
function createCamera() {
const instance = Camera;
return instance;
}
export { createCamera }
terrain.js
import { createWorldTerrainAsync } from 'cesium'
function createTerrain(viewer) {
// 添加地形数据
const addWorldTerrainAsync = async () => {
try {
const terrainProvider = await createWorldTerrainAsync({
requestWaterMask: false, // 水面特效
requestVertexNormals: true // 地形照明
});
viewer.terrainProvider = terrainProvider;
} catch (error) {
console.log(`Failed to add world imagery: ${error}`);
}
};
addWorldTerrainAsync()
}
export { createTerrain }
navigation.js
import CesiumNavigation from 'cesium-navigation-es6'
import { Cartographic } from 'cesium'
let instance;
function createNavigation(viewer) {
let navigationOptions = {};
// 用于在使用重置导航重置地图视图时设置默认视图控制。接受的值是Cesium.Cartographic 和Cesium.Rectangle.
navigationOptions.defaultResetView = Cartographic.fromDegrees(103.84, 31.15, 17860000);
// 用于启用或禁用罗盘。true是启用罗盘,false是禁用罗盘。默认值为true。如果将选项设置为false,则罗盘将不会添加到地图中。
navigationOptions.enableCompass= true;
// 用于启用或禁用缩放控件。true是启用,false是禁用。默认值为true。如果将选项设置为false,则缩放控件 将不会添加到地图中。
navigationOptions.enableZoomControls= true;
// 用于启用或禁用距离图例。true是启用,false是禁用。默认值为true。如果将选项设置为false,距离图例将不会添加到地图中。
navigationOptions.enableDistanceLegend= true;
// 用于启用或禁用指南针外环。true是启用,false是禁用。默认值为true。如果将选项设置为false,则该环将可见但无效。
navigationOptions.enableCompassOuterRing= true;
navigationOptions.resetTooltip = "重置";
navigationOptions.zoomInTooltip = "放大";
navigationOptions.zoomOutTooltip = "缩小";
// 开启Navigation 罗盘、图例、指南针等导航插件
instance = new CesiumNavigation(viewer, navigationOptions);
return instance;
}
function removeNavigation(){
instance.destroy();
}
export { createNavigation, removeNavigation }
夜色中的地球图片