Vue + Cesium 实现自定义地图底图切换组件

814 阅读5分钟

在现代地理信息系统应用中,底图的选择对于提升用户体验至关重要。本文介绍了一个基于Vue和Cesium的底图切换组件的实现细节,该组件允许用户动态地在多种地图风格间自由切换,涵盖了从卫星影像到矢量地图等多种类型。通过精心设计的UI交互和灵活的地图服务接入,实现了高效且友好的地图底图管理方案。

image.png

技术选型与环境搭建

  • 前端框架:Vue.js,利用其响应式数据绑定与组件化特性简化界面逻辑开发。
  • 3D地球引擎:Cesium,提供强大的WebGL驱动的三维地球展示能力。
  • 地图资源:集成天地图、高德地图以及Bing Maps等多源地图服务。

组件设计与功能

用户界面设计

  • 组件结构:组件通过.baseMap容器固定于页面右下角,内含多个.baseMap-item以图标加文字形式展示不同的地图类型。

  • 交互逻辑

    • 鼠标悬停显示所有地图选项;
    • 点击任意地图选项切换至对应地图;
    • 当前激活的地图项拥有高亮边框,提升视觉反馈。

功能实现要点

  1. 动态底图加载:根据用户选择,通过changeMapType方法动态更改Cesium.ViewerimageryLayers,支持即时加载不同地图服务提供的底图图层。
  2. 底图数据配置:维护一个包含地图名称、类型、图标等信息的baseMapList数组,便于管理和扩展底图种类。
  3. 跨源请求处理:使用Token(如天地图的tk参数)解决跨域访问地图服务的问题。
  4. 自适应瓦片模式:针对不同地图服务商的坐标系,如高德地图需采用特定的AmapMercatorTilingScheme适配其切片规则。
  5. 地形显示控制:当切换到网格图层时,提供removeTerrainrestoreTerrain方法动态管理地形展示,确保底图展示的完整性与兼容性。

使用

引入组件,在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