在现代地理信息系统应用中,底图的选择对于提升用户体验至关重要。本文介绍了一个基于Vue和Cesium的底图切换组件的实现细节,该组件允许用户动态地在多种地图风格间自由切换,涵盖了从卫星影像到矢量地图等多种类型。通过精心设计的UI交互和灵活的地图服务接入,实现了高效且友好的地图底图管理方案。
技术选型与环境搭建
- 前端框架:Vue.js,利用其响应式数据绑定与组件化特性简化界面逻辑开发。
- 3D地球引擎:Cesium,提供强大的WebGL驱动的三维地球展示能力。
- 地图资源:集成天地图、高德地图以及Bing Maps等多源地图服务。
组件设计与功能
用户界面设计
-
组件结构:组件通过
.baseMap
容器固定于页面右下角,内含多个.baseMap-item
以图标加文字形式展示不同的地图类型。 -
交互逻辑:
- 鼠标悬停显示所有地图选项;
- 点击任意地图选项切换至对应地图;
- 当前激活的地图项拥有高亮边框,提升视觉反馈。
功能实现要点
- 动态底图加载:根据用户选择,通过
changeMapType
方法动态更改Cesium.Viewer
的imageryLayers
,支持即时加载不同地图服务提供的底图图层。 - 底图数据配置:维护一个包含地图名称、类型、图标等信息的
baseMapList
数组,便于管理和扩展底图种类。 - 跨源请求处理:使用Token(如天地图的tk参数)解决跨域访问地图服务的问题。
- 自适应瓦片模式:针对不同地图服务商的坐标系,如高德地图需采用特定的
AmapMercatorTilingScheme
适配其切片规则。 - 地形显示控制:当切换到网格图层时,提供
removeTerrain
与restoreTerrain
方法动态管理地形展示,确保底图展示的完整性与兼容性。
使用
引入组件,在cesium初始化完成后再进行加载渲染,一定要注意这一点,viewer
是cesium的viewer实例对象,这个是非常关键的必传的参数。当然,你也可以用其他方式传参;
还支持传入地图类型数据 mapType
,传入后可根地图类型在初始化加载影像时加载你想要的地图类型。默认加载地图影像的可选的类型有天地图影像
、高德影像
、天地图矢量
、高德矢量
、微软矢量
、微软影像
以及网格地图
。
加载高德地图还用到了一个定位纠偏的插件,不用的话定位不准确,会有偏差。
天地图需要Token,你可以自己去申请一个。必应(微软)地图需要有cesium的token,只要是你注册了cesium的账号,微软的影像资源都是免费试用的。
<template>
...
<base-layer v-if="mapLoaded" :viewer="viewer" :map-type="props.mapType"></base-layer>
...
</template>
关键代码段解析
// 切换底图逻辑
const changeMapType = async (map) => {
// 更新当前选中地图的信息
mapData.mapType = map.type;
mapData.mapName = map.name;
mapData.mapIcon = map.icon;
// 根据地图类型调用不同的加载逻辑
await changeBaseMap(mapData.mapType);
}
这段代码体现了组件的核心功能逻辑,通过异步操作确保地图切换过程的平滑性和响应性。changeBaseMap
方法内部根据不同的地图类型实例化对应的ImageryProvider
,并通过Cesium API动态替换或添加至场景,实现了底图的即时更换。
示例代码
<template>
<div class="baseMap" @mouseenter="showMap = true" @mouseleave="showMap = false">
<div class="baseMap-item active" v-if="!showMap">
<img class="icon" :src="mapData.mapIcon" alt="">
<div class="mapname">{{ mapData.mapName }}</div>
</div>
<div v-else :class="mapData.mapType === item.type ? 'baseMap-item active' : 'baseMap-item'"
@click="changeMapType(item)" v-for="item in baseMapList" :key="item.id">
<img class="icon" :src="item.icon" alt="">
<div class="mapname">{{ item.name }}</div>
</div>
</div>
</template>
<script setup lang="ts">
import { nextTick, onMounted, reactive, ref, defineEmits } from "vue";
import baseMapIcon from '@/static/baseMap/index.js';
import AmapMercatorTilingScheme from '@/modules/AmapMercatorTilingScheme/AmapMercatorTilingScheme';
import * as Cesium from "cesium";
const showMap = ref(false);
const props = defineProps({
viewer: {
type: Object as () => Cesium.Viewer,
required: true
},
// 默认地图类型
mapType: {
type: String,
default: 'tdt'
}
});
var viewer: Cesium.Viewer | undefined = props.viewer;
const mapData = reactive({
mapType: props.mapType,
mapName: '',
mapIcon: ''
});
const baseMapList = [
{ id: 1, name: '天地图影像', type: 'tdt', icon: baseMapIcon.tdt_img },
{ id: 2, name: '高德影像', type: 'gd', icon: baseMapIcon.gaode_img },
{ id: 3, name: '天地图矢量', type: 'tdt_v', icon: baseMapIcon.tdt_vec },
{ id: 4, name: '高德矢量', type: 'gd_v', icon: baseMapIcon.gaode_vec },
{ id: 5, name: 'Bing路网', type: 'BingRoad', icon: baseMapIcon.bing_vec },
{ id: 6, name: 'Bing影像', type: 'BingAerial', icon: baseMapIcon.bing_img },
{ id: 7, name: '网格', type: 'grid', icon: baseMapIcon.grid },
];
const changeMapType = (map) => {
mapData.mapType = map.type;
mapData.mapName = map.name;
mapData.mapIcon = map.icon;
changeBaseMap(mapData.mapType)
};
const changeBaseMap = async (type) => {
if (viewer && viewer.imageryLayers.length > 0)
viewer.imageryLayers.removeAll();
restoreTerrain()
switch (type) {
case 'tdt':
loadTdtMap();
break;
case 'gd':
loadGdMap();
break;
case 'gd_v':
loadGdVectorMap();
break;
case 'tdt_v':
loadTdtVectorMap();
break;
case 'BingRoad':
loadBingRoadMap();
break;
case 'BingAerial':
loadBingAerialMap();
break;
case 'grid':
loadGridMap();
break;
default:
console.error('Unsupported map type:', type);
}
};
const loadTdtMap = () => {
let tdtMap = new Cesium.WebMapTileServiceImageryProvider({
url: 'https://t{s}.tianditu.gov.cn/img_w/wmts?service=WMTS&request=GetTile&version=1.0.0&layer=img&tileMatrixSet=w&TileMatrix={TileMatrix}&TileRow={TileRow}&TileCol={TileCol}&style=default&format=tiles&tk=你的Token',
subdomains: ['1', '2', '3', '4', '5', '6', '7'],
layer: 'tdt_imgLayer',
style: 'default',
format: 'image/jpeg',
tileWidth: 256,
tileHeight: 256,
tileMatrixSetID: 'GoogleMapsCompatible',
maximumLevel: 18,
});
viewer.imageryLayers.addImageryProvider(tdtMap);
};
const loadGdMap = () => {
let gdMap = new Cesium.UrlTemplateImageryProvider({
url: 'https://webst02.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}&lang=zh_cn',
tileWidth: 256,
tileHeight: 256,
tilingScheme: new AmapMercatorTilingScheme(),
maximumLevel: 18,
});
viewer.imageryLayers.addImageryProvider(gdMap);
};
const loadGdVectorMap = () => {
let gdvMap = new Cesium.UrlTemplateImageryProvider({
url: 'https://webrd02.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=2&style=8&x={x}&y={y}&z={z}',
tileWidth: 256,
tileHeight: 256,
tilingScheme: new AmapMercatorTilingScheme(),
maximumLevel: 18,
});
viewer.imageryLayers.addImageryProvider(gdvMap);
};
const loadTdtVectorMap = () => {
let tdtMap = new Cesium.WebMapTileServiceImageryProvider({
url: 'https://t{s}.tianditu.gov.cn/vec_w/wmts?service=WMTS&request=GetTile&version=1.0.0&layer=vec&tileMatrixSet=w&TileMatrix={TileMatrix}&TileRow={TileRow}&TileCol={TileCol}&style=default&format=tiles&tk=你的Token',
subdomains: ['1', '2', '3', '4', '5', '6', '7'],
layer: 'tdt_imgLayer',
style: 'default',
tileWidth: 256,
tileHeight: 256,
tileMatrixSetID: 'GoogleMapsCompatible',
maximumLevel: 24,
});
viewer.imageryLayers.addImageryProvider(tdtMap);
};
const loadBingRoadMap = () => {
viewer.imageryLayers.addImageryProvider(
await Cesium.IonImageryProvider.fromAssetId(4),
);
};
const loadBingAerialMap = () => {
viewer.imageryLayers.addImageryProvider(
await Cesium.IonImageryProvider.fromAssetId(2),
);
};
const loadGridMap = () => {
let gridOptions = {
color: Cesium.Color.fromCssColorString('#ccc'),
backgroundColor: Cesium.Color.fromCssColorString('#00000000'),
glowColor: Cesium.Color.fromCssColorString('#666'),
glowWidth: 1,
cells: 2
};
var GridImagery = new Cesium.GridImageryProvider(gridOptions);
viewer.imageryLayers.addImageryProvider(GridImagery);
viewer.scene.globe.baseColor = Cesium.Color.BLACK;
removeTerrain()
};
const currentTerrainProvider = ref(null)
function removeTerrain() {
// 获取当前地形提供者
currentTerrainProvider.value = viewer.scene.terrainProvider;
if (currentTerrainProvider.value) {
// 如果当前地形提供者存在,则移除它
viewer.scene.terrainProvider = undefined;
}
// 或者替换为一个空的地形提供者
viewer.scene.terrainProvider = new Cesium.EllipsoidTerrainProvider();
}
function restoreTerrain() {
if (currentTerrainProvider.value) {
viewer.scene.terrainProvider = currentTerrainProvider.value;
}
}
const findMapDetails = () => {
const map = baseMapList.find((item) => item.type === mapData.mapType);
if (map) {
mapData.mapName = map.name;
mapData.mapIcon = map.icon;
}
};
onMounted(() => {
viewer = props.viewer
findMapDetails()
changeBaseMap(mapData.mapType)
})
</script>
<style lang="scss" scoped>
.baseMap {
position: fixed;
right: 10px;
bottom: 30px;
z-index: 999;
display: flex;
.baseMap-item {
width: 80px;
height: 80px;
text-align: center;
// line-height: 60px;
color: #fff;
border: 1px solid #092131cc;
cursor: pointer;
&:hover {
border: 1px solid #9c7a1d8e;
}
&.active {
border: 1px solid #ffbb00;
}
.icon {
width: 80px;
height: 60px;
}
.mapname {
line-height: 18px;
background-color: #00b8ffad;
width: 80px;
height: 18px;
}
}
}
</style>
结论
通过上述实现,我们获得了一个既美观又实用的底图切换组件,不仅提升了地理信息系统应用的用户体验,也展示了Vue与Cesium结合的强大定制能力。此外,组件的设计考虑了地图服务的多样性,保证了系统的灵活性和扩展性,为后续集成更多地图资源打下了坚实基础。
源码
完整源码和使用案例 请看这里,里面还有其他一些我写案例东西,可以的话请给个star