Skip to content

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> 中除 hitTolerancepriorityvisiblehandler 以外的字段都会保留在配置项中,可在 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

参数类型默认值说明
mapRefMaybeRefOrGetter<OLMap | undefined>-地图实例
typeClickEventType-事件类型

UseMapClickReturn

名称类型说明
add(key: string, items: ClickConfigList<T>) => void添加一组点击配置,相同 key 会覆盖
remove(key: string) => void移除指定 key 的点击配置

useMapClickHandler

底层 composable,直接传入配置列表和地图实例。适用于不需要按 key 管理配置项的场景。

UseClickHandlerOptions<T>

名称类型默认值说明
mapRefMaybeRefOrGetter<OLMap | undefined>-地图实例
itemsMaybeRefOrGetter<ClickConfigList<T>>-点击配置列表
typeClickEventType-事件类型

ClickEventType

ts
type ClickEventType = 'click' | 'dblclick' | 'singleclick'

ClickConfig<T>

名称类型默认值说明
handler(context: ClickContext) => void-点击回调
hitTolerancenumber0Hit-detection 容差(css 像素)
prioritynumber0优先级,数字越大越优先
visible(context: ClickContext) => boolean | undefined | void-是否响应此点击
其他字段...args: T-通过泛型扩展的自定义属性

ClickContext

名称类型说明
mapOLMap地图实例
coordinateCoordinate点击位置的地图坐标
pixelPixel点击位置的像素坐标
featureFeatureLike | undefined命中的要素
layerLayerLike要素所在图层

类型别名

名称类型说明
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>

Released under the ISC License.