OpenLayers点击矢量图标透明部分无法弹出Overlay的优化

一、概要

当使用 OpenLayers 进行鼠标点击、悬停获取矢量时,它是根据触发图标的像素点来响应的。然而,如果图标是 PNG 格式并且具有透明部分,这些透明部分不具有像素点,将不会触发响应事件,导致无法获取到矢量。

解决方案:

  • 使用空间查询 “forEachFeatureIntersectingExtent” 代替 “getFeaturesAtPixe” ,将触发点扩大为触发范围
  • 触发位置可能位于图标实际范围之外。根据触发位置到查询到的矢量的形心的距离,筛选真正内部被触发的矢量。

二、完整代码:矢量加载、挂载Overlay弹出事件

<template>
  <!--地图-->
  <div ref="mapContainer" class="mapContainer" id="mapContainer"></div>
  <!--弹窗面板-->
  <template v-for="element of overlayArray">
    <div :id="(element.id as string)" class='overlay'>
      <el-icon size="12" class="closer" @click="() => { closeOverlay(map!, element) }">
        <closeBold />
      </el-icon>
      <!--信息展示-->
      <div class="content">
        <span class='overlay-Message' :style="{ color: element.color as string }">{{ '编号:' + element.name }}</span>
      </div>
    </div>
  </template>
</template>

<script lang="ts" setup>
import { nextTick, onBeforeUnmount, onMounted, ref, shallowRef } from 'vue'
import { View, Map as OLMap, Feature, MapBrowserEvent, Overlay } from "ol"
import { fromLonLat } from 'ol/proj'
import TileLayer from 'ol/layer/Tile'
import { XYZ } from 'ol/source'
import { defaults as defaultControls } from "ol/control"
import { Vector as VectorLayer } from 'ol/layer'
import VectorSource from 'ol/source/Vector'
import { Circle as CircleStyle, Style, Stroke } from "ol/style";
import { FeatureLike } from 'ol/Feature'
import { Geometry, SimpleGeometry } from 'ol/geom'
import GeoJSON from 'ol/format/GeoJSON'
import { Coordinate } from 'ol/coordinate'
import { unByKey } from 'ol/Observable'
import { EventsKey } from 'ol/events'
import WebGLPointsLayer from 'ol/layer/WebGLPoints'

// 改为你自己的GeoJson数据地址
const polygonDataURL = './geoJson/1W'
// 地图容器
const mapContainer = shallowRef<HTMLDivElement>()
// 地图对象
const map = shallowRef<OLMap>()
// 点击事件
let mapClickEvent: EventsKey

// 弹窗类型
interface overlayArrayItem {
  [key: string]: string | number | Coordinate;
}
// 弹窗数组
const overlayArray = ref<overlayArrayItem[]>([])

/**
 * @description 创建地图实例
 * @param {Document | DocumentId} target 地图容器
 * @returns 地图对象
 */

const createMap = function (target: HTMLElement | string,): OLMap {
  // 创建地图
  const map = new OLMap({
    target,
    layers: [
      new TileLayer({
        source: new XYZ({
          url: "http://t0.tianditu.gov.cn/DataServer?T=vec_w&x={x}&y={y}&l={z}&tk=99a8ea4a53c8553f6f3c565f7ffc15ec",
          crossOrigin: 'anonymous',
          wrapX: true
        })
      })
    ],
    controls: defaultControls({
      zoom: false, // 不显示放大放小按钮
      rotate: false, // 不显示指北针控件
      attribution: false, // 不显示右下角的地图信息控件
    }).extend([]),
    view: new View({
      projection: 'EPSG:3857', // 坐标系EPSG:4326或EPSG:3857
      zoom: 6, // 打开页面时默认地图缩放级别
      maxZoom: 20,
      minZoom: 1, // 地图缩放最小级别
      center: fromLonLat([121.5, 25]), // 需要转到墨卡托坐标系
      constrainResolution: true, // 自动缩放到距离最近的一个整数级别,因为当缩放在非整数级别时地图会糊
    })
  })
  return map
}

/**
 * @description 新建图层(检测到同名图层直接获取)
 * @param map 地图实例
 * @param layerName 图层名
 * @param getStyle feature样式
 * @returns 
 */

function getVectorLayer(map: OLMap, layerName: String, getStyle: Function): VectorLayer<VectorSource> {
  let vectorLayer = map.getLayers().getArray().find(layer => layer.get('name') === layerName) as VectorLayer<VectorSource>;
  if (!vectorLayer) {
    vectorLayer = new VectorLayer({
      source: new VectorSource({ wrapX: true, features: [] }),
      style: function (feature: FeatureLike) {
        return getStyle(feature)
      }
    });
    vectorLayer.set('name', layerName);
    map.addLayer(vectorLayer);
  }
  return vectorLayer;
}

/**
 * @description >>>>>>>> 添加矢量点(可追加数据)
 * @param { Map } map 地图对象
 * @param { string} layerName 图层名
 * @param { any } pointData 点据集合
 */

function addPointVector(
  map: OLMap,
  layerName: string,
  pointData: any) {
  if (!map || !pointData) { return }
  // 获取矢量图层
  let vectorLayer: VectorLayer<VectorSource> | null = getVectorLayer(map, layerName, getStyle)
  // 添加数据源
  let features = new GeoJSON({ dataProjection: 'EPSG:4326', featureProjection: 'EPSG:3857' }).readFeatures(pointData)
  vectorLayer?.getSource()!.addFeatures(features)
}

/**
 * @description >>>>>>>> 全局样式函数
 * @param { Feature<Geometry> } feature 矢量要素
 */

// 全局样式缓存
const styleCache: Map<string, Style> = new Map();

// 全局样式函数,这里使用了样式缓存,使用Map数据结构,创建缓存键来减少style创建的数量
function getStyle(feature: Feature<Geometry>) {
  const baseColor = feature.get('color') ?? 'rgba(0, 0, 0, 1)'; // 默认颜色为黑色
  const opacity = feature.get('opacity') ?? 1; // 默认透明度为1
  const rotation = feature.get('rotation') ?? 0; // 默认旋转为0
  // 将透明度添加到颜色字符串中
  const color = `${baseColor.slice(0, -1)}, ${opacity})`;

  // 构建缓存键
  const cacheKey = `${color}-${opacity}`;

  // 如果样式已经在缓存中,则直接返回缓存中的样式
  if (styleCache.has(cacheKey)) {
    return styleCache.get(cacheKey);
  }

  // 如果样式不在缓存中,则创建新的样式对象并将其添加到缓存中
  const style = new Style({
    image: new CircleStyle({
      radius: 10,
      rotation,
      stroke: new Stroke({
        color,
        width: 5,
      }),
    }),
  });

  styleCache.set(cacheKey, style);

  return style;
};

/**
 * 根据点击事件获取矢量集合
 * @param map 地图对象
 * @param evt 地图点击事件
 * @param radius 点击半径,默认为10像素
 * @returns 点击事件周围的矢量集合
 */
function clickGetFeatures(
  map: OLMap,
  evt: MapBrowserEvent<UIEvent>,
  radius: number = 10
): FeatureLike[] {
  // 用于存储矢量集合的 Set
  const featureSet: Set<FeatureLike> = new Set();

  // 如果正在拖拽地图或地图图层不存在,则返回空数组
  if (evt.dragging || !map.getLayers()) return [];

  // 获取点击像素处的矢量集合
  const featureAtClick = getFeaturesAtClick(map, evt);

  // 将点击像素处的矢量添加到矢量集合中
  featureAtClick.forEach((feature) => featureSet.add(feature));

  // 重新获取点击的像素坐标
  const pixel = evt.pixel;
  // 过滤矢量集合,仅保留距离点击位置在矢量半径范围内的矢量
  const filteredFeatures = Array.from(featureSet).filter((feature) => {
    // 获取矢量的几何对象
    const geometry = feature.getGeometry() as SimpleGeometry;
    // 获取矢量几何对象的坐标
    const coordinates = geometry.getCoordinates() as number[];
    // 将矢量几何对象的坐标转换为像素坐标
    const centerPixel = map.getPixelFromCoordinate(coordinates);
    // 将像素坐标的浮点数值取整,以确保得到整数像素坐标
    const centerPixelRound = [Math.round(centerPixel[0]), Math.round(centerPixel[1])];
    // 计算点击点与矢量中心点的距离的平方
    const distance2 = getDistanceSquared(centerPixelRound[0], centerPixelRound[1], pixel[0], pixel[1]);
    // 判断距离的平方是否小于等于半径的平方,以确定点击是否在矢量的半径范围内
    return distance2 <= radius * radius;
  });

  return filteredFeatures;
}

/**
 * 获取点击像素处的矢量集合
 * @param map 地图对象
 * @param evt 地图点击事件
 * @returns 点击像素处的矢量集合
 */
function getFeaturesAtClick(map: OLMap, evt: MapBrowserEvent<UIEvent>): FeatureLike[] {
  const clickedFeatures: FeatureLike[] = []

  // 定义触发区域的大小为5像素正方形区域
  const bufferPixelSize = 10

  // 获取鼠标指针的像素坐标
  const pixel = evt.pixel;

  // 根据鼠标指针的像素坐标计算缩小后的像素区域
  const extentPixel = [
    pixel[0] - bufferPixelSize,
    pixel[1] + bufferPixelSize,
    pixel[0] + bufferPixelSize,
    pixel[1] - bufferPixelSize
  ]

  // 将像素坐标转换为地图坐标
  const extent = map.getCoordinateFromPixel([extentPixel[0], extentPixel[1]])
    .concat(map.getCoordinateFromPixel([extentPixel[2], extentPixel[3]]))


  // 遍历每个图层
  map.getLayers().forEach(layer => {
    // 判断是否为矢量图层
    if (layer instanceof VectorLayer || layer instanceof WebGLPointsLayer) {
      if (!layer.getVisible()) return
      // 获取图层区域内的要素数组
      const source = layer.getSource()

      //检索和鼠标位置相交的要素
      source.forEachFeatureIntersectingExtent(extent, (feature: FeatureLike) => {
        clickedFeatures.push(feature);
      })
    }
  })
  return clickedFeatures
}

/**
 * 计算两点之间的距离的平方
 * @param x1 第一个点的 x 坐标
 * @param y1 第一个点的 y 坐标
 * @param x2 第二个点的 x 坐标
 * @param y2 第二个点的 y 坐标
 * @returns 两点之间的距离的平方
 */
function getDistanceSquared(x1: number, y1: number, x2: number, y2: number): number {
  const dx = x2 - x1;
  const dy = y2 - y1;
  return dx * dx + dy * dy;
}

/**
 * @description>>>>>>点击要素弹窗
 * @param {Map} map 地图对象
 */

function openOverlay(map: OLMap) {
  mapClickEvent = map.on('singleclick', (evt: MapBrowserEvent<any>) => {
    // 获取点击位置的要素集合
    const features = clickGetFeatures(map, evt);
    if (features && features.length > 0) {
      // 创建一个overlay节点数据
      const overlayItem: overlayArrayItem = {
        value: '',
        id: '',
        color: '',
        coordinates: []
      };
      // 遍历要素集合,查找符合条件的要素
      for (const feature of features) {
        if (feature !== undefined) {
          // 获取要素的属性信息
          overlayItem.id = feature.get('color');
          overlayItem.name = feature.get('name');
          overlayItem.color = feature.get('color');
          break; // 获取到信息后立即跳出循环
        }
      }
      // 如果获取到了信息
      if (overlayItem.id) {
        // 获取点击位置的中心点坐标
        overlayItem.coordinates = evt.coordinate;
        // 检查是否已存在相同id的overlay
        let overlay = map.getOverlayById(overlayItem.id as string);
        if (!overlay) {
          // 如果不存在相同id的overlay,则将overlayItem添加到overlayArray.value中
          overlayArray.value.push(overlayItem);
        }
        // 使用nextTick挂载DOM节点
        nextTick(() => {
          let container = document.getElementById(overlayItem.id as string) as HTMLElement;
          // 如果overlay不存在,则创建新的overlay,并添加到地图中
          if (!overlay) {
            overlay = new Overlay({
              id: overlayItem.id as string,
              element: container,
              insertFirst: false
            });
            overlay.setPosition(overlayItem.coordinates as Coordinate);
            map.addOverlay(overlay);
          } else {
            // 如果overlay已存在,则更新其位置
            overlay.setPosition(overlayItem.coordinates as Coordinate);
          }
        });
      }
    }
  });
}


/**
 * @description 关闭弹窗面板
 * @param { Map } map 地图实例
 * @param { overlayArrayItem } item
 */

function closeOverlay(map: OLMap, item: overlayArrayItem) {
  // 清除dom节点
  let dom = document.getElementById(item.id as string)
  if (dom) {
    dom?.remove()
  }
  // 清除overlay
  let overlay = map.getOverlayById(item.id as string)
  if (overlay) {
    overlay.setPosition(undefined)
    map.removeOverlay(overlay)
  }
}

onMounted(async () => {
  map.value = createMap(mapContainer.value!);
  // 打开GeoJson
  const response = await fetch(`${polygonDataURL}.json`);
  const point = await response.json();
  console.log(point)
  // 加载Polygon
  addPointVector(map.value, 'pointLayer', point);
  // 绑定点击事件
  openOverlay(map.value)
});

onBeforeUnmount(() => {
  unByKey(mapClickEvent)
})

</script>
<style lang="scss">
#mapContainer {
  position: absolute;
  top: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
}

.overlay {
  background-color: white;
  border-radius: 6px;
  color: white;
  width: 100%;
  padding: 0px 10px 0px 10px;
  display: flex;
  justify-content: space-around;
  align-items: flex-start;
  flex-direction: column;

  .closer {
    width: 15px;
    height: 15px;
    position: absolute;
    right: -16px;
    top: 2px;
    padding: 1px;
    gap: 4px;
    border-radius: 2px;
    background-color: rgba(0, 0, 0, 0.6);
    opacity: 0;
    transition: opacity 0.5s;
    cursor: pointer;
  }

  .content {
    .overlay-Message {
      text-align: center;
    }
  }
}

.overlay:hover .closer {
  opacity: 1
}
</style>

效果(点击圆圈空心部分触发overlay弹窗):
请添加图片描述

三、核心代码:缓冲范围触发Overlay弹出事件

/**
 * 获取点击像素处的矢量集合
 * @param map 地图对象
 * @param evt 地图点击事件
 * @returns 点击像素处的矢量集合
 */
function getFeaturesAtClick(map: OLMap, evt: MapBrowserEvent<UIEvent>): FeatureLike[] {
  const clickedFeatures: FeatureLike[] = []

  // 定义触发区域的大小为5像素正方形区域
  const bufferPixelSize = 10

  // 获取鼠标指针的像素坐标
  const pixel = evt.pixel;

  // 根据鼠标指针的像素坐标计算缩小后的像素区域
  const extentPixel = [
    pixel[0] - bufferPixelSize,
    pixel[1] + bufferPixelSize,
    pixel[0] + bufferPixelSize,
    pixel[1] - bufferPixelSize
  ]

  // 将像素坐标转换为地图坐标
  const extent = map.getCoordinateFromPixel([extentPixel[0], extentPixel[1]])
    .concat(map.getCoordinateFromPixel([extentPixel[2], extentPixel[3]]))


  // 遍历每个图层
  map.getLayers().forEach(layer => {
    // 判断是否为矢量图层
    if (layer instanceof VectorLayer || layer instanceof WebGLPointsLayer) {
      if (!layer.getVisible()) return
      // 获取图层区域内的要素数组
      const source = layer.getSource()

      //检索和鼠标位置相交的要素
      source.forEachFeatureIntersectingExtent(extent, (feature: FeatureLike) => {
        clickedFeatures.push(feature);
      })
    }
  })
  return clickedFeatures
}

主要思路是通过扩大点击位置的区域,然后在该区域内对所有的矢量图层进行空间查询,找到与之相交的地理要素。具体如下:

  • 获取点击位置的像素坐标(pixel),然后根据定义的缓冲区大小(bufferPixelSize)计算出像素区域(extentPixel)。

  • 将像素区域转换为地图坐标(extent)。

  • 对每个矢量图层执行空间查询,并将其添加到结果数组中(clickedFeatures)。

四、使用的矢量点数据

本文使用的矢量点数据为标准的GeoJson数据,数据及制作工具下方自取,属性字段包括:

  • name:名称/编码,类型为 string | number
  • rotationx:旋转角度,范围0~1
  • opacity:透明度,范围0~1
  • color:rgba颜色

数据样例:

 {
    "type": "FeatureCollection",
    "features": [
        {
            "type": "Feature",
            "properties": {
                "rotation": 0.43,
                "name": "1000",
                "opacity": 0.4,
                "color": "rgba(232, 248, 242)"
            },
            "geometry": {
                "coordinates": [
                    126.08871972341268,
                    79.9640846145881
                ],
                "type": "Point"
            }
        }
    ]
 }

五、写在最后

  • 触发位置位于图标实际范围之外时,根据触发位置到矢量的形心距离,筛选真正内部被触发的矢量,实际选择的是圆形范围,方形图标需要重写计算范围的筛选函数(当然圆形图标就没有这个问题啦!)
    请添加图片描述
  • bufferPixelSize长度应根据图标形心最远透明区域的像素长度确定,如果图标不存在透明区域,设置为0即可。

最后一点很重要:

  • 由于在加载海量矢量数据时,getFeaturesAtPixel 需要遍历图层的所有要素,可能导致明显卡顿。因此,即使图标不存在透明区域,我们也应该考虑使用 forEachFeatureIntersectingExtent 来代替 getFeaturesAtPixel(已测试)。
  • 33
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

無可言喻

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值