GIS前端开发之路——Openlayers为地图添加自定义标注(四)

30 阅读2分钟

地图标注用到的主要是Overlay

这里做了优化,通过手动添加,删除元素来达到提升性能效果。

监听地图视野的改变来动态变化标注数量的加载,让程序仅渲染视野内的标注信息。


<template>
  <div ref="rootRef" style="display: none"></div>
</template>

<script setup>
import { ref, watch, onUnmounted } from 'vue'
import Overlay from 'ol/Overlay'
import { fromLonLat } from 'ol/proj'

const props = defineProps({
  map: { type: Object, required: true },
  markers: { type: Array, default: () => [] },
  titleField: { type: String, default: 'name' },
  debounceTime: { type: Number, default: 100 },
  autoPan: { type: Boolean, default: true }
})

const activeOverlays = new Map()
let moveEndKey = null

// ---------- 工具函数 ----------
function debounce(fn, delay) {
  let timer
  return function (...args) {
    clearTimeout(timer)
    timer = setTimeout(() => fn.apply(this, args), delay)
  }
}

function buildMarkerElement(marker) {
  const el = document.createElement('div')
  el.className = 'marker-layer-item'
  const title = marker[props.titleField] || '●'
  el.innerHTML = `<div class="marker-card"><strong>${title}</strong></div>`
  el.addEventListener('click', () => console.log('点击标注:', marker))
  return el
}

function addMarker(marker) {
  const element = buildMarkerElement(marker)
  const overlay = new Overlay({
    element,
    position: fromLonLat([marker.lon, marker.lat]),
    positioning: 'bottom-center',
    offset: [0, -6],
    autoPan: props.autoPan,
    stopEvent: false
  })
  props.map.addOverlay(overlay)
  activeOverlays.set(marker.id, { overlay, element })
}

function removeMarker(id) {
  const item = activeOverlays.get(id)
  if (!item) return
  props.map.removeOverlay(item.overlay)
  item.element.remove()
  activeOverlays.delete(id)
}

// ---------- 视野裁剪更新 ----------
const updateVisibleMarkers = debounce(() => {
  const map = props.map
  if (!map) return

  map.updateSize()
  const size = map.getSize()
  if (!size || size[0] === 0 || size[1] === 0) return

  const extent = map.getView().calculateExtent(size)
  const visibleIds = new Set()

  props.markers.forEach(m => {
    if (!m || m.lon === undefined || m.lat === undefined) return
    const coord = fromLonLat([m.lon, m.lat])
    if (
      coord[0] >= extent[0] && coord[0] <= extent[2] &&
      coord[1] >= extent[1] && coord[1] <= extent[3]
    ) {
      visibleIds.add(m.id)
    }
  })

  activeOverlays.forEach((_, id) => {
    if (!visibleIds.has(id)) removeMarker(id)
  })
  visibleIds.forEach(id => {
    if (!activeOverlays.has(id)) {
      const marker = props.markers.find(x => x.id === id)
      if (marker) addMarker(marker)
    }
  })
}, props.debounceTime)

// ---------- 初始化(地图就绪时立刻添加标注)----------
function initMarkers() {
  const map = props.map
  if (!map) return

  // 强制确保尺寸
  map.updateSize()
  const size = map.getSize()
  if (!size || size[0] === 0 || size[1] === 0) {
    // 尺寸仍无效,等待 postrender 事件(仅一次)
    const onReady = () => {
      map.un('postrender', onReady)
      initMarkers() // 再次尝试
    }
    map.on('postrender', onReady)
    return
  }

  // 尺寸有效,执行首次标注更新,并绑定移动事件
  updateVisibleMarkers()
  if (!moveEndKey) {
    moveEndKey = map.on('moveend', updateVisibleMarkers)
  }
}

// ---------- 监听 map 变化 ----------
watch(() => props.map, (newMap, oldMap) => {
  if (oldMap) {
    if (moveEndKey) oldMap.un('moveend', moveEndKey)
    moveEndKey = null
    activeOverlays.forEach((_, id) => removeMarker(id))
  }

  if (newMap) {
    initMarkers()
  }
}, { immediate: true })

// ---------- 监听 markers 变化 ----------
watch(() => props.markers, () => updateVisibleMarkers(), { deep: true })

// ---------- 清理 ----------
onUnmounted(() => {
  if (props.map && moveEndKey) {
    props.map.un('moveend', moveEndKey)
  }
  activeOverlays.forEach((_, id) => removeMarker(id))
  activeOverlays.clear()
})
</script>

<style>
.marker-layer-item {
  position: absolute;
  white-space: nowrap;
  pointer-events: auto;
  transform: translate(-50%, -100%);
}
.marker-card {
  background: white;
  border-radius: 6px;
  padding: 4px 10px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.3);
  font-size: 12px;
  font-family: sans-serif;
  cursor: pointer;
  user-select: none;
  position: relative;
}
.marker-card::after {
  content: '';
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  border: 5px solid transparent;
  border-top-color: white;
}
</style>