跪拜 Guibai
← Back to the summary

Hollowing Out a Map: Reverse Masking an Administrative Region in Cesium

Preface

Cesium implements a reverse mask effect for administrative regions, based on GeoJSON boundary data. In this example, the GeoJSON data is the geographical data of Songjiang District, Shanghai. The effect displays the map within the Songjiang District area, with areas outside Songjiang District covered by a semi-transparent dark canvas. The base map in this example also supports one-click switching between a vector tile base map and an imagery base map with annotations.

The dynamic demonstration is shown below:

mask.gif

Implementation Effects:

Core Principle Explanation:

1. Cesium Reverse Mask Implementation Principle:

Achieves the 'map hollowing' effect via polygon -> hierarchy -> holes:

  1. Draws a super-large rectangular polygon covering the entire globe as the base mask.
  2. Uses the target administrative region's GeoJSON boundary coordinates as the holes array for this polygon.
  3. The base is filled with semi-transparent black, and the hole area is transparent, achieving an 'outside dark, inside bright' effect.

2. GeoJSON Multi-dimensional Array Flattening:

Standard administrative region GeoJSON coordinates are multi-level nested arrays. Cesium cannot parse them directly and requires recursive flattening into a one-dimensional longitude-latitude array.

3. Dual Base Map Switching Logic:

The imagery mode uses a dual-layer overlay of 'imagery layer + text annotation layer' to ensure roads and place names are visible on the satellite map. The vector mode renders a single layer for a clean and concise look. When switching, all layers are cleared before redrawing to avoid residual layer overlap.

Complete Code

1. Template Structure:

<template>
    <div class="main">

        <!-- Main map rendering container -->
        <div class="content" ref="content" id="earth"></div>

        <!-- Function button area, displayed after the map is loaded -->
        <div class="btn-border" v-if="isLoading">
            <!-- Camera Reset: Fly to the initial viewpoint -->
            <el-button type="primary" size="default" class="btn" @click="flyTo">Initial Position</el-button>
            <!-- Base Map Switch Button: Toggle between Vector Base Map / Imagery Base Map -->
            <el-button type="primary" size="default" class="btn" @click="changeMap">{{isImagery ? 'Switch to Vector Base Map' : 'Switch to Imagery Base Map'}}</el-button>
        </div>

    </div>
</template>

2. Script Code:

<script setup>
    import { onMounted, nextTick, ref, onUnmounted } from 'vue';
    import { token } from '../../utils/common.js';
    import { ElMessage } from 'element-plus';
    // GeoJSON data for the boundary of Songjiang District, Shanghai
    import songjiang from '../../assets/songjiang.json';

    // Flag indicating the map has finished loading
    let isLoading = ref(false);

    // Base map type flag: true=Imagery Base Map, false=Vector Base Map
    let isImagery = ref(true);

    // Global mask outer frame coordinate array, used to generate the reverse mask (outer area darkly covered, inner administrative region hollowed out)
    let maskDataList = [55.40046568, 64.81241339999998, 176.64339637039996, 64.81241339999998, 176.64339637039996, -4.708978609400006, 55.40046568000002, -4.708978609400044];

    // Component unmount lifecycle: Release the Viewer instance, clean up WebGL resources to prevent memory leaks
    onUnmounted(() => {
        if (window.viewer) {
            window.viewer.destroy();
            window.viewer = null;
        }
    });

    onMounted(() => {
        nextTick(() => {
            initMap();
        });
    });


    // Initialize the main Cesium 3D map
    const initMap = () => {

        // Set the Cesium Ion token
        Cesium.Ion.defaultAccessToken = token;

        // Set the default view range (China region)
        Cesium.Camera.DEFAULT_VIEW_RECTANGLE = Cesium.Rectangle.fromDegrees(89.5, 20.4, 110.4, 61.2);

        // Instantiate the main map Viewer
        window.viewer = new Cesium.Viewer('earth', {
            animation: false,  // Time animation widget
            timeline: false,  // Timeline
            infoBox: false,  // Click feature popup
            geocoder: false,  // Search box
            homeButton: false,  // Reset view button
            sceneModePicker: false,  // 2D/3D switch button
            baseLayerPicker: false,  // Base map switching panel
            navigationHelpButton: false,  // Operation help popup
            fullscreenButton: false,  // Fullscreen button
            selectionIndicator: false,  // Selected feature highlight box
            shouldAnimate: false  // Disable automatic animation rendering to save performance
        });

        // Initialize the imagery base map: Imagery layer + Road text annotation layer overlay
        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);

        // Draw the reverse mask: In this example, only the Songjiang District area is displayed normally, the outer area is a dark mask
        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)) =>  {

        // Read the polygon coordinates from the GeoJSON
        let holeList = dataHoleList.features[0].geometry.coordinates;

        // Recursively flatten the multi-dimensional coordinate array
        let holes = dealArr(holeList);

        // Convert the longitude-latitude array to Cartesian coordinates
        holes = Cesium.Cartesian3.fromDegreesArray(holes);

        // Create the mask polygon, using hierarchy to achieve the hollowing effect
        let maskPolygon = {
            id: 'maskPolygon',
            name: 'Mask Layer',
            show: true,
            polygon: {
                hierarchy: {
                    positions: Cesium.Cartesian3.fromDegreesArray(maskDataList),
                    holes: [{ positions: holes }]  // Hollow out the Songjiang District area, this area will not be obscured
                },
                material: maskColor,
                fill: true
            }
        };
        window.viewer.entities.add(maskPolygon);

        // Draw the boundary outline for Songjiang District
        let maskLine = {
            polyline: {
                positions: holes,
                width: 5,
                material: dataBorderColor,
                clampToGround: true  // Clamp to the ground
            }
        };
        window.viewer.entities.add(maskLine);
    };

    // Method to switch the base map type: Vector Base Map <==> Imagery Base Map (Imagery overlaid with road annotations)
    const changeMap = () => {

        // Guard: Exit directly if the map instance does not exist
        if (!window.viewer) {
            return;
        }

        // Clear all current imagery layers
        window.viewer.imageryLayers.removeAll();


        if (!isImagery.value) {

            // Switch to Imagery Base Map: Imagery layer + Road text annotation layer overlay
            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 {

            // Switch back to Vector Base Map
            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);

        }

        // Toggle the status flag
        isImagery.value = !isImagery.value;

    };

    // Method to recursively flatten a multi-dimensional coordinate array
    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;
    }

    // Method to fly the camera to a preset initial viewpoint (Shanghai Songjiang area)
    const flyTo = () => {
        window.viewer.camera.flyTo({
            destination: Cesium.Cartesian3.fromDegrees(121.21709895255839, 31.01351793218674, 66403.24207453332),
            orientation: {
                heading: Cesium.Math.toRadians(359.8090548395541),  // Heading angle
                pitch: Cesium.Math.toRadians(-89.63236031882052),  // Pitch angle
                roll: Cesium.Math.toRadians(0)  // Roll angle
            },
            duration: 3  // Flight duration of 3 seconds
        });
    };
</script>

3. CSS Style Code:

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

Key Pitfall Summary

1. Hollowing Out Not Working, Mask Covers Everything

Most likely a problem with the winding direction of the GeoJSON coordinate rings (clockwise/counter-clockwise). Cesium strictly validates the winding direction of hole coordinates; incorrect coordinate order will cause the hollowing to fail.

2. Residual Layers After Base Map Switching

You must use imageryLayers.removeAll() to clear all layers before redrawing. Directly overwriting is not possible, as it will lead to layer overlap and transparency anomalies.

3. GeoJSON Multi-dimensional Coordinate Parsing Failure

Administrative regions and complex polygon GeoJSON data are always multi-level nested. Recursive flattening is mandatory; otherwise, fromDegreesArray will throw a parsing error and the graphic will not display.

The Amap tile resources used in this example are for learning and technical research demonstration purposes only and are not intended for online commercial deployment scenarios. If an enterprise project officially goes live using similar map tile resources, please go to the corresponding map service provider's platform to complete qualification certification and apply for a compliant call key.

Comments

Top 1 of 2 from juejin.cn, machine-translated. The original thread is authoritative.

THEKILL

Ask how to do reverse selection? [grin]

Tony费

Hello, if you want to implement reverse selection, that is, to mask the inside of an administrative region, first read the GeoJSON coordinates, then generate and draw a normal ground-clamped polygon based on the data, then fill the polygon with a semi-transparent mask. The external map can be displayed normally. You can refer to my newly published article, which has a specific implementation method.