一、概要
当使用 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"
}
}
]
}
- 矢量点数据(GeoJson)生成工具:https://blog.csdn.net/qq_40236953/article/details/134954087
五、写在最后
- 触发位置位于图标实际范围之外时,根据触发位置到矢量的形心距离,筛选真正内部被触发的矢量,实际选择的是圆形范围,方形图标需要重写计算范围的筛选函数(当然圆形图标就没有这个问题啦!)
- bufferPixelSize长度应根据图标形心到最远透明区域的像素长度确定,如果图标不存在透明区域,设置为0即可。
最后一点很重要:
- 由于在加载海量矢量数据时,getFeaturesAtPixel 需要遍历图层的所有要素,可能导致明显卡顿。因此,即使图标不存在透明区域,我们也应该考虑使用 forEachFeatureIntersectingExtent 来代替 getFeaturesAtPixel(已测试)。