Cesium 实现行政区内部遮罩

133 阅读2分钟

前言:

在上一篇《Cesium 实现行政区反向遮罩镂空效果》中,我们实现了全局暗色蒙层 + 行政区镂空高亮的效果:外部地图变暗、目标行政区正常显示。 本文针对上一篇文章的进阶,实现相关的反向需求:外部地图保持正常清晰显示,仅目标行政区内部叠加暗色遮罩弱化

演示动态图示如下:

songjiang.gif

上一篇和本篇的两种效果对比:

上一篇: 全球大矩形 + holes 挖洞 → 外暗、内亮(Geojson数据的区域高亮)

本篇: 直接绘制Geojson数据的区域的Polygon填充 → 外亮、内暗(Geojson数据的区域弱化遮罩)

核心实现原理:

  1. 读取行政区 GeoJSON 边界坐标。
  2. 扁平化多维嵌套坐标,转为 Cesium 可识别笛卡尔坐标。
  3. 直接基于行政区轮廓生成贴地多边形,填充半透明黑色遮罩。
  4. 叠加蓝色边界描边,保证区域轮廓清晰。

完整代码

1. template 结构:

<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 代码:

<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);

    // 组件销毁生命周期:释放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);

        drawInnerMask(songjiang);

        isLoading.value = true;
        flyTo();
    };
	
    const drawInnerMask = (dataHoleList, 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);

        // 直接绘制Geojson数据的区面,填充遮罩
        let districtMask = {
            id: 'districtMask',
            name: '行政区内部遮罩',
            polygon: {
                hierarchy: {
                    positions: holes
                },
                material: maskColor,
                fill: true,
                clampToGround: true // 贴地
            }
        };
        window.viewer.entities.add(districtMask);

        // 绘制Geojson数据的边界轮廓线
        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样式代码:

.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;
}

为什么不用 holes 挖洞做反向效果?

Cesium 的 PolygonHierarchy.holes 机制逻辑固定:外层坐标为实体面,holes 数组永远是挖空区域,不存在“反转镂空”参数。想要区内变暗,直接绘制Geojson数据的区域面是唯一优雅、无bug的方案。

总结:

本文基于上一篇《Cesium 实现行政区反向遮罩镂空效果》完成了绘制Geojson数据的区域的内部遮罩的进阶改造,补齐了 Cesium 行政区可视化的两种主流遮罩方案。

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