useMapClick
简介
在地图要素上处理点击事件(单击、双击、singleclick)。支持按优先级匹配、可见性动态判断、相邻相同 hitTolerance 合并检测,适合"点击要素弹出信息窗"等交互场景。
底层通过 useMapClickHandler 实现事件绑定,useMapClick 是对其的封装,提供按 key 增删点击配置项的能力。
使用示例
点我查看代码
vue
<script lang="ts" setup>
import type { ClickContext, OLMap } from '@summeruse/ol'
import type { Pixel } from 'ol/pixel'
import {
createOpenStreetMapLayer,
createPointFeature,
createVectorLayer,
OlMap,
useMapClick,
} from '@summeruse/ol'
import { NCard, NPopover, NTag } from 'naive-ui'
import { Map } from 'ol'
import { ref } from 'vue'
const olMap = new Map()
olMap.addLayer(createOpenStreetMapLayer())
const beijing = createPointFeature([116.3912, 39.9072], {
id: 'beijing',
styleOptions: {
circleOptions: {
radius: 12,
fillOptions: { color: '#e74c3c' },
strokeOptions: { color: '#fff', width: 2 },
},
},
data: { name: '北京', desc: '中华人民共和国首都', population: '2189万' },
})
const shanghai = createPointFeature([121.4737, 31.2304], {
id: 'shanghai',
styleOptions: {
circleOptions: {
radius: 10,
fillOptions: { color: '#3498db' },
strokeOptions: { color: '#fff', width: 2 },
},
},
data: { name: '上海', desc: '中国最大的经济中心城市', population: '2487万' },
})
const guangzhou = createPointFeature([113.2644, 23.1291], {
id: 'guangzhou',
styleOptions: {
circleOptions: {
radius: 8,
fillOptions: { color: '#2ecc71' },
strokeOptions: { color: '#fff', width: 2 },
},
},
data: { name: '广州', desc: '广东省省会', population: '1868万' },
})
const { source, layer } = createVectorLayer()
source.addFeature(beijing)
source.addFeature(shanghai)
source.addFeature(guangzhou)
olMap.addLayer(layer)
interface CityInfo {
name: string
desc: string
population: string
}
const selectedInfo = ref<CityInfo | null>(null)
const isCapital = ref(false)
const clickPosition = ref({ x: 0, y: 0 })
const showPopover = ref(false)
function showAt(data: CityInfo, pixel: Pixel, map: OLMap) {
selectedInfo.value = data
const rect = map.getTargetElement().getBoundingClientRect()
clickPosition.value = { x: pixel[0] + rect.left, y: pixel[1] + rect.top }
showPopover.value = true
}
const { add } = useMapClick(olMap, 'singleclick')
add('cities', [
{
priority: 10,
// visible 只允许"北京"通过,展示优先匹配
visible: (ctx: ClickContext) => ctx.feature?.get('data')?.name === '北京',
handler: (ctx: ClickContext) => {
isCapital.value = true
showAt(ctx.feature!.get('data'), ctx.pixel, ctx.map)
},
},
{
handler: (ctx: ClickContext) => {
const data = ctx.feature?.get('data')
if (data) {
isCapital.value = false
showAt(data, ctx.pixel, ctx.map)
}
else {
showPopover.value = false
}
},
},
])
olMap.on('movestart', () => {
showPopover.value = false
})
</script>
<template>
<div class="relative">
<OlMap
class="w-100% h-400px"
:center="[116.3912, 39.9072]"
:zoom="5"
projection="EPSG:4326"
:ol-map
/>
<NPopover
trigger="manual"
:show="showPopover"
:x="clickPosition.x"
:y="clickPosition.y"
>
<NCard v-if="selectedInfo" size="small" :title="selectedInfo.name">
<template v-if="isCapital" #header-extra>
<NTag type="error" size="small">
首都
</NTag>
</template>
<p>{{ selectedInfo.desc }}</p>
<NTag :type="isCapital ? 'error' : 'info'">
人口: {{ selectedInfo.population }}
</NTag>
</NCard>
</NPopover>
</div>
</template>泛型扩展
- 支持通过泛型携带自定义配置类型:
useMapClick<T extends Record<string, any>>(mapRef, type)。 ClickConfig<T>中除hitTolerance、priority、visible、handler以外的字段都会保留在配置项中,可在handler中通过闭包访问。
示例:
ts
const { add, remove } = useMapClick<{ id: string }>(olMap, 'singleclick')
add('my-handler', [{
id: 'feature-1',
handler: (ctx) => {
// ctx 为 ClickContext
},
}])API
| 名称 | 类型 |
|---|---|
| useMapClick | <T>(mapRef, type) => UseMapClickReturn |
| useMapClickHandler | <T>(options: UseClickHandlerOptions<T>) => void |
useMapClick
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| mapRef | MaybeRefOrGetter<OLMap | undefined> | - | 地图实例 |
| type | ClickEventType | - | 事件类型 |
UseMapClickReturn
| 名称 | 类型 | 说明 |
|---|---|---|
| add | (key: string, items: ClickConfigList<T>) => void | 添加一组点击配置,相同 key 会覆盖 |
| remove | (key: string) => void | 移除指定 key 的点击配置 |
useMapClickHandler
底层 composable,直接传入配置列表和地图实例。适用于不需要按 key 管理配置项的场景。
UseClickHandlerOptions<T>
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| mapRef | MaybeRefOrGetter<OLMap | undefined> | - | 地图实例 |
| items | MaybeRefOrGetter<ClickConfigList<T>> | - | 点击配置列表 |
| type | ClickEventType | - | 事件类型 |
ClickEventType
ts
type ClickEventType = 'click' | 'dblclick' | 'singleclick'ClickConfig<T>
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| handler | (context: ClickContext) => void | - | 点击回调 |
| hitTolerance | number | 0 | Hit-detection 容差(css 像素) |
| priority | number | 0 | 优先级,数字越大越优先 |
| visible | (context: ClickContext) => boolean | undefined | void | - | 是否响应此点击 |
| 其他字段 | ...args: T | - | 通过泛型扩展的自定义属性 |
ClickContext
| 名称 | 类型 | 说明 |
|---|---|---|
| map | OLMap | 地图实例 |
| coordinate | Coordinate | 点击位置的地图坐标 |
| pixel | Pixel | 点击位置的像素坐标 |
| feature | FeatureLike | undefined | 命中的要素 |
| layer | LayerLike | 要素所在图层 |
类型别名
| 名称 | 类型 | 说明 |
|---|---|---|
ClickConfigList<T> | ClickConfig<T>[] | 点击配置列表 |
使用说明
- 同一
type建议只调用一次useMapClick,通过add/remove管理不同场景的点击配置。 - 当有多个配置项匹配时,按
priority从高到低、hitTolerance相邻相同分组的策略检测。 visible为函数时可动态判断是否响应点击(如根据要素属性决定)。- 支持响应式切换地图实例,
mapRef变化时自动重新绑定事件。
源代码
点我查看代码
ts
import type { MapBrowserEvent } from 'ol'
import type { Coordinate } from 'ol/coordinate'
import type { FeatureLike } from 'ol/Feature'
import type { Pixel } from 'ol/pixel'
import type { MaybeRefOrGetter } from 'vue'
import type { LayerLike, OLMap } from '../../types'
import { computed, onBeforeUnmount, shallowRef, toValue, watch } from 'vue'
/** 点击事件类型 */
export type ClickEventType = 'click' | 'dblclick' | 'singleclick'
/**
* 点击事件处理的上下文参数
*/
export interface ClickContext {
map: OLMap
coordinate: Coordinate
pixel: Pixel
feature?: FeatureLike
layer: LayerLike
}
interface Option {
[key: string]: any
}
/**
* 点击配置
*/
export type ClickConfig<T extends Option> = {
/** Hit-detection 容差(css像素) */
hitTolerance?: number
/** 优先级,数字越大越优先 */
priority?: number
/** 是否处理这个点击 */
visible?: (context: ClickContext) => boolean | undefined | void
/** 点击回调 */
handler: (context: ClickContext) => void
/** 扩展配置 */
} & T
export type ClickConfigList<T extends Option> = ClickConfig<T>[]
export interface UseClickHandlerOptions<T extends Option = Option> {
/** 地图实例 */
mapRef: MaybeRefOrGetter<OLMap | undefined>
/** 提示配置列表 */
items: MaybeRefOrGetter<ClickConfigList<T>>
/** 事件类型 */
type: ClickEventType
}
/**
* 通用的点击处理 Hook
*/
export function useMapClickHandler<T extends Option = Option>(
options: UseClickHandlerOptions<T>,
) {
const { mapRef, items } = options
let currentMap: OLMap | undefined
/** 按优先级和 hitTolerance 分组 */
function groupConfigsByPriority(
configs: ClickConfigList<T>,
): Array<{
tolerance: number
items: ClickConfigList<T>
}> {
// 按优先级排序
const sorted = [...configs].sort(
(a, b) => (b.priority ?? 0) - (a.priority ?? 0),
)
// 按相邻相同 tolerance 分组
const groups: Array<{ tolerance: number, items: ClickConfigList<T> }> = []
let currentGroup: { tolerance: number, items: ClickConfigList<T> } | null = null
sorted.forEach((item) => {
const tolerance = item.hitTolerance ?? 0
if (!currentGroup || currentGroup.tolerance !== tolerance) {
currentGroup = { tolerance, items: [item] }
groups.push(currentGroup)
}
else {
currentGroup.items.push(item)
}
})
return groups
}
/** 创建点击事件处理器 */
function handler(evt: MapBrowserEvent) {
const map = evt.map as OLMap
if (!map)
return
const grouped = groupConfigsByPriority(toValue(items))
let foundFeature: FeatureLike | undefined
let foundLayer: LayerLike | undefined
for (const group of grouped) {
map.forEachFeatureAtPixel(
evt.pixel,
(feature, layer) => {
foundFeature = feature
foundLayer = layer
return true
},
{ hitTolerance: group.tolerance },
)
const context: ClickContext = {
map,
coordinate: evt.coordinate,
pixel: evt.pixel,
feature: foundFeature,
layer: foundLayer!,
}
for (const item of group.items) {
const isVisible = typeof item.visible === 'function' ? item.visible(context) : true
if (isVisible) {
item.handler(context)
return
}
}
}
}
/** 绑定事件 */
function bindMapEvents(map: OLMap) {
map.on(options.type, handler)
}
/** 解绑事件 */
function unbindMapEvents(map: OLMap) {
map.un(options.type, handler)
}
/** 监听 mapRef 变化 */
watch(
() => toValue(mapRef),
(newMap, oldMap) => {
if (oldMap) {
unbindMapEvents(oldMap)
}
if (oldMap !== newMap) {
currentMap = newMap
if (newMap) {
bindMapEvents(newMap)
}
}
},
{ immediate: true },
)
onBeforeUnmount(() => {
if (currentMap) {
unbindMapEvents(currentMap)
}
})
}
export type UseMapClickHandlerReturn = ReturnType<typeof useMapClickHandler>
export function useMapClick<T extends Option = Option>(
mapRef: MaybeRefOrGetter<OLMap | undefined>,
type: ClickEventType,
) {
const itemMap = shallowRef<{
[key: string]: ClickConfigList<T>
}>({
})
useMapClickHandler({
mapRef,
items: computed(() => Object.values(itemMap.value).flat()),
type,
})
const add = (key: string, items: ClickConfigList<T>) => {
itemMap.value[key] = items
}
const remove = (key: string) => {
delete itemMap.value[key]
}
return {
add,
remove,
}
}
export type UseMapClickReturn = ReturnType<typeof useMapClick>