使用OpenLayers 实现挖空/镂空遮罩效果
在地图可视化开发中,客户需要指定区域(如佛山市)高亮,其他区域用图案或者半透明的“遮罩”遮住
实现效果如下图所示
实现思路
- 创建图像蒙版图层, 使用 canvasFunction 绘制整幅遮罩图
- 加载需要高亮区域的 geojson
- 按 geojson 区域挖空
具体实现代码如下
import "ol/ol.css";
import type Map from "ol/Map";
import VectorLayer from "ol/layer/Vector";
import VectorSource from "ol/source/Vector";
import GeoJSON from "ol/format/GeoJSON";
import Style from "ol/style/Style";
import Fill from "ol/style/Fill";
import Stroke from "ol/style/Stroke";
import ImageLayer from "ol/layer/Image";
import ImageCanvasSource from "ol/source/ImageCanvas";
import mapBg from "@/assets/map-bg.png";
/**
* 添加蒙版效果并高亮指定的 GeoJSON 区域
* @param map OpenLayers 地图实例
* @param geojsonUrl GeoJSON 路径
* @param highlightColor 高亮区域的颜色(默认透明)
* @param imageUrl 蒙版图片的 URL
*/
export async function addMaskWithHighlight(
map: Map,
geojsonUrl: string,
highlightColor: string = "rgba(255, 0, 0, 0)",
imageUrl: string = mapBg
): Promise<void> {
try {
// 加载 GeoJSON 数据
const response = await fetch(geojsonUrl);
if (!response.ok) {
throw new Error(`Failed to load GeoJSON: ${response.statusText}`);
}
const geojsonData = await response.json();
console.log(geojsonData);
// const targetFeature = geojsonData.features.find(
// f => f.properties.name === "太平川镇"
// );
// const targetFeature = geojsonData.features;
const features = new GeoJSON().readFeatures(geojsonData);
// 创建遮罩图片
const image = new Image();
image.src = imageUrl;
// 等待图片加载完成
await new Promise<void>(resolve => {
if (image.complete) {
resolve();
} else {
image.onload = () => resolve();
}
});
// 镂空遮罩图层
const maskLayer = new ImageLayer({
source: new ImageCanvasSource({
canvasFunction: (extent, resolution, pixelRatio, size, projection) => {
const canvas = document.createElement("canvas");
canvas.width = size[0];
canvas.height = size[1];
const ctx = canvas.getContext("2d");
if (!ctx) return canvas;
// 1. 先用图片铺满整个canvas
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
// 2. 设置“挖空”模式
ctx.globalCompositeOperation = "destination-out";
// 3. 坐标转换函数:地理坐标转canvas像素
// const transform = getTransform(projection, projection); // 这里其实是单位变换
const coordToPixel = (coord: number[]) => [
((coord[0] - extent[0]) / (extent[2] - extent[0])) * canvas.width,
((extent[3] - coord[1]) / (extent[3] - extent[1])) * canvas.height
];
// 4. 按 geojson 区域挖空
features.forEach(feature => {
const geom = feature.getGeometry();
if (!geom) return;
const type = geom.getType();
const coords = geom.getCoordinates();
ctx.beginPath();
if (type === "Polygon") {
coords[0].forEach((c: number[]) => {
const pixel = coordToPixel(c);
ctx.lineTo(pixel[0], pixel[1]);
});
} else if (type === "MultiPolygon") {
coords.forEach((poly: any) => {
poly[0].forEach((c: number[]) => {
const pixel = coordToPixel(c);
ctx.lineTo(pixel[0], pixel[1]);
});
});
}
ctx.closePath();
ctx.fill();
});
ctx.globalCompositeOperation = "source-over";
return canvas;
},
projection: map.getView().getProjection()
})
});
// 高亮边界图层
const highlightSource = new VectorSource({ features });
const highlightLayer = new VectorLayer({
source: highlightSource,
style: new Style({
fill: new Fill({ color: highlightColor }),
stroke: new Stroke({ color: "transparent", width: 2 })
})
});
// if (features.length > 0) {
// const extent = features[0].getGeometry().getExtent();
// map.getView().fit(extent, { duration: 800 });
// }
map.addLayer(maskLayer);
map.addLayer(highlightLayer);
map.on("postrender", () => {
maskLayer.getSource()?.refresh();
});
} catch (error) {
console.error("Error adding mask with highlight:", error);
}
}
使用方法
addMaskWithHighlight(map, "xxxx.geojson");
特别提醒,我使用的坐标系是EPSG:4326,如果挖空位置出现偏移,需要核对自己的坐标系是否一致