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:
Implementation Effects:
- A large-scale dark mask covers the entire map.
- Reads the GeoJSON administrative region boundary and 'hollows out' the corresponding area.
- Only the target administrative region displays the base map normally, keeping the visual focus highly concentrated.
- Supports one-click switching between 'Vector Base Map / Imagery Base Map with Annotations'.
Core Principle Explanation:
1. Cesium Reverse Mask Implementation Principle:
Achieves the 'map hollowing' effect via polygon -> hierarchy -> holes:
- Draws a super-large rectangular polygon covering the entire globe as the base mask.
- Uses the target administrative region's GeoJSON boundary coordinates as the holes array for this polygon.
- 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.
Top 1 of 2 from juejin.cn, machine-translated. The original thread is authoritative.
Ask how to do reverse selection? [grin]
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.