OpenLayers 实现挖空遮罩

250 阅读2分钟

使用OpenLayers 实现挖空/镂空遮罩效果

在地图可视化开发中,客户需要指定区域(如佛山市)高亮,其他区域用图案或者半透明的“遮罩”遮住

实现效果如下图所示

image.png

实现思路

  1. 创建图像蒙版图层, 使用 canvasFunction 绘制整幅遮罩图
  2. 加载需要高亮区域的 geojson
  3. 按 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,如果挖空位置出现偏移,需要核对自己的坐标系是否一致