Cesium 实现行政区反向遮罩镂空效果(自定义暗色蒙层+矢量/影像底图切换)

前言

Cesium 实现行政区反向遮罩效果,基于 GeoJSON 边界数据,本例中GeoJSON数据为上海市松江区的地理数据。本例效果展示上海松江区范围内地图,地图上超出上海松江区的区域使用半透明暗色画布遮盖;并且本例中的底图支持一键切换矢量瓦片底图与带标注影像底图。

演示动态图示如下:

实现效果:

  • 全局大范围暗色蒙层覆盖地图。
  • 读取 GeoJSON 行政区边界,在对应区域「镂空挖洞」。
  • 仅目标行政区正常显示底图,视觉焦点高度集中。
  • 支持一键切换「矢量底图 / 带注记影像底图」。

核心原理讲解:

1、Cesium 反向遮罩实现原理:

通过 polygon -> hierarchy -> holes 实现 "地图挖洞":

  1. 绘制一个覆盖全球范围的超大矩形面作为底层遮罩。
  2. 将目标行政区 GeoJSON 边界坐标,作为该面的孔洞数组 holes。
  3. 底层填充半透明黑色,孔洞区域透明透底,实现「外暗内亮」。

2、GeoJSON 多维数组扁平化:

标准行政区 GeoJSON 坐标是多层嵌套数组,Cesium 无法直接解析,需要递归扁平化处理成一维经纬度数组。

3、双底图切换逻辑:

影像模式采用「影像图层 + 文字注记图层」双层叠加,保证卫星图也能看到道路、地名;矢量模式单一图层渲染,保证简洁清爽。切换时清空所有图层再重绘,避免图层叠加残留。

完整代码

1. template 结构:

xml 复制代码
<template>
    <div class="main">

        <!-- 主地图渲染容器 -->
        <div class="content" ref="content" id="earth"></div>

        <!-- 功能按钮区域,地图加载完成后展示 -->
        <div class="btn-border" v-if="isLoading">
            <!-- 镜头复位:飞行至初始视角 -->
            <el-button type="primary" size="default" class="btn" @click="flyTo">初始位置</el-button>
            <!-- 底图切换按钮:矢量底图 / 影像底图互切 -->
            <el-button type="primary" size="default" class="btn" @click="changeMap">{{isImagery ? '切换矢量底图' : '切换影像底图'}}</el-button>
        </div>

    </div>
</template>

2. script 代码:

xml 复制代码
<script setup>
    import { onMounted, nextTick, ref, onUnmounted } from 'vue';
    import { token } from '../../utils/common.js';
    import { ElMessage } from 'element-plus';
    // 上海松江区边界GeoJSON地理数据
    import songjiang from '../../assets/songjiang.json';

    // 地图加载完成标识
    let isLoading = ref(false);

    // 底图类型标记:true=影像底图,false=矢量底图
    let isImagery = ref(true);

    // 全局遮罩外框坐标数组,生成反向遮罩(外部区域暗色遮盖,内部行政区镂空显示)
    let maskDataList = [55.40046568, 64.81241339999998, 176.64339637039996, 64.81241339999998, 176.64339637039998, -4.708978609400006, 55.40046568000002, -4.708978609400044];
	
    // 组件销毁生命周期:释放Viewer实例,清理WebGL资源,防止内存泄漏
    onUnmounted(() => {
        if (window.viewer) {
            window.viewer.destroy();
            window.viewer = null;
        }
    });
	
    onMounted(() => {
        nextTick(() => {
            initMap();
        });
    });
	
	
    // 初始化主Cesium三维地图
    const initMap = () => {

        // 设置 Cesium Ion 的token
        Cesium.Ion.defaultAccessToken = token;

        // 设置默认视角范围(中国区域)
        Cesium.Camera.DEFAULT_VIEW_RECTANGLE = Cesium.Rectangle.fromDegrees(89.5, 20.4, 110.4, 61.2);

        // 实例化主地图Viewer
        window.viewer = new Cesium.Viewer('earth', {
            animation: false,  // 时间动画控件
            timeline: false,  // 时间轴
            infoBox: false,  // 点击要素弹窗
            geocoder: false,  // 搜索框
            homeButton: false,  // 复位视角按钮
            sceneModePicker: false,  // 2D/3D切换按钮
            baseLayerPicker: false,  // 底图切换面板
            navigationHelpButton: false,  // 操作帮助弹窗
            fullscreenButton: false,  // 全屏按钮
            selectionIndicator: false,  // 选中要素高亮框
            shouldAnimate: false  // 关闭自动动画渲染,节省性能
        });

        // 初始化影像底图:影像图层 + 道路文字注记图层叠加
        let layer1 = new Cesium.UrlTemplateImageryProvider({
            url: "https://webst02.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}",
            minimumLevel: 4,
            maximumLevel: 18
        });
        window.viewer.imageryLayers.addImageryProvider(layer1);

        let layer2 = 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",
            minimumLevel: 4,
            maximumLevel: 18
        });
        window.viewer.imageryLayers.addImageryProvider(layer2);

        // 绘制反向遮罩:本例中仅松江区区域正常显示,外部区域暗色遮罩
        drawMask(songjiang, maskDataList);

        isLoading.value = true;
        flyTo();
    };
	
    const drawMask = (dataHoleList, maskDataList, dataBorderColor = new Cesium.Color.fromBytes(0, 0, 255, 255), maskColor = new Cesium.Color.fromBytes(0, 0, 0, 200)) =>  {

        // 读取GeoJSON内多边形坐标
        let holeList = dataHoleList.features[0].geometry.coordinates;

        // 递归扁平化多维坐标数组
        let holes = dealArr(holeList);

        // 经纬度数组转为笛卡尔坐标
        holes = Cesium.Cartesian3.fromDegreesArray(holes);

        // 创建遮罩多边形,使用hierarchy实现挖洞效果
        let maskPolygon = {
            id: 'maskPolygon',
            name: '遮罩层',
            show: true,
            polygon: {
                hierarchy: {
                    positions: Cesium.Cartesian3.fromDegreesArray(maskDataList),
                    holes: [{ positions: holes }]  // 挖空松江区范围,该区域不被遮挡
                },
                material: maskColor,
                fill: true
            }
        };
        window.viewer.entities.add(maskPolygon);

        // 绘制松江区边界轮廓线
        let maskLine = {
            polyline: {
                positions: holes,
                width: 5,
                material: dataBorderColor,
                clampToGround: true  // 贴地绘制
            }
        };
        window.viewer.entities.add(maskLine);
    };
	
    // 切换底图类型的方法:矢量底图 <==> 影像底图(影像叠加道路注记)
    const changeMap = () => {

        // 防护:地图实例不存在直接退出
        if (!window.viewer) {
            return;
        }

        // 清空当前所有影像图层
        window.viewer.imageryLayers.removeAll();


        if (!isImagery.value) {

            // 切换为影像底图:影像图层 + 道路文字注记图层叠加
            let layer1 = new Cesium.UrlTemplateImageryProvider({
                url: "https://webst02.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}",
                minimumLevel: 4,
                maximumLevel: 18
            });
            window.viewer.imageryLayers.addImageryProvider(layer1);

            let layer2 = 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",
                minimumLevel: 4,
                maximumLevel: 18
            });
            window.viewer.imageryLayers.addImageryProvider(layer2);

        } else {

            // 切换回矢量底图
            let layer = new Cesium.UrlTemplateImageryProvider({
                url: "http://webrd02.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}",
                minimumLevel: 4,
                maximumLevel: 18
            });
            window.viewer.imageryLayers.addImageryProvider(layer);

        }

        // 切换状态标识
        isImagery.value = !isImagery.value;

    };
	
    // 递归扁平化多维坐标数组的方法
    const dealArr = (arr) => {
        let newArrFun = function (arr) {
            return arr.reduce((pre, cur) => {
                return pre.concat(Array.isArray(cur) ? newArrFun(cur) : cur)
            }, [])
        }
        let newArr = newArrFun(arr);
        return newArr;
    }
	
    // 相机飞行至预设初始视角(上海松江区域)的方法
    const flyTo = () => {
        window.viewer.camera.flyTo({
            destination: Cesium.Cartesian3.fromDegrees(121.21709895255839, 31.01351793218674, 66403.24207453332),
            orientation: {
                heading: Cesium.Math.toRadians(359.8090548395541),  // 航向角
                pitch: Cesium.Math.toRadians(-89.63236031882052),  // 俯仰角
                roll: Cesium.Math.toRadians(0)  // 翻滚角
            },
            duration: 3  // 飞行时长3秒
        });
    };
</script>

3. css样式代码:

css 复制代码
.main {
    width: 100%;
    height: 100vh;
    position: relative;
}

.content {
    width: 100%;
    height: 100%;
    position: relative;
    z-index: 1;
}

.btn-border {
    position: absolute;
    right: 24px;
    top: 24px;
    z-index: 2;
    display: flex;
    justify-content: start;
    align-items: stretch;
}

.btn {
    margin-left: 20px;
    cursor: pointer;
}

关键踩坑总结

1、挖洞不生效、遮罩全覆盖

大概率是 GeoJSON 坐标环方向问题(顺时针/逆时针)。Cesium 对孔洞坐标环方向严格校验,坐标顺序错误会导致镂空失效。

2、底图切换残留图层

必须使用 imageryLayers.removeAll() 清空所有图层再重绘,不能直接覆盖,否则会出现图层叠加、透明度异常。

3、GeoJSON多维坐标解析失败

行政区域、复杂多边形 GeoJSON 一定是多层嵌套,必须递归扁平化,否则 fromDegreesArray 解析报错、图形不显示。

本文案例中使用的高德瓦片资源仅用于学习、技术研究演示,不作线上商用部署场景使用。若企业项目正式上线使用同类地图瓦片资源,请自行前往对应地图服务商平台完成资质认证并申请合规调用密钥。

相关推荐
张人玉2 小时前
基于 Vue 3 + ECharts + Express + SQLite 构建的鞋类零售数据分析与毛利预测平台——基于深度学习的鞋类销售数据分析系统
vue.js·sqlite·echarts·express
独泪了无痕12 小时前
Vue3 Hooks使用实战解析
前端·vue.js
郝亚军16 小时前
如何安装webstorm、Node.js和vue CLI
前端·javascript·vue.js
孪生前端17 小时前
Three.js 做医疗数字孪生:从空场景到可漫游楼层(附最小骨架)
webgl
an3174217 小时前
6MB 组织树大文件性能优化全流程
前端·javascript·vue.js
GitLqr18 小时前
Vue 3.6 Vapor Mode vs React 19 Compiler:前端架构的两条分叉路
vue.js·react.js·vapor
整點薯條19 小时前
Vue3+vite创建项目架构文件说明总结
前端·javascript·vue.js
Cobyte1 天前
模板 DSL 解析器中的状态机设计
前端·javascript·vue.js
郝亚军1 天前
How to create a vue project via webstorm?
前端·vue.js·webstorm