vue3 + Openlayer 超详细Overlay地图弹框使用

672 阅读9分钟

文章目录:

1. 编写悬浮框组件

2. 创建悬浮框 Overlay 实例,与组件绑定

3. 监听地图事件,展示悬浮框

4. 完整代码

5. 悬浮框优化01

6.优化01 完整代码

7. 悬浮框优化02

8. 优化02 完整代码

先展示完成效果

1. 编写悬浮框组件

需要配置 ref 使 ol 能获取该组件

<div ref="popupElement" style="width: 50px; height: 50px">弹框</div>

2. 创建悬浮框 Overlay 实例,与组件绑定

使用 new Overlay 创建一个 Overlay 实例并在地图中添加改实例。注意:Overlay 不能在初始化的时候直接创建,这时候 Dom 还没有被加载,会报错。

创建一个基本的 Overlay只需要传入 element 属性即可,elementOverlay 需要绑定的组件 Dom, 所以也可以改成使用 docoument 获取。

const popupElement = ref(null)   // 弹框DOM
let popupOverlay                 // 弹框 Overlay 实例

const initMapOverlay = () => {
  popupOverlay = new Overlay({
    element: popupElement.value,  // 绑定弹框DOM
    positioning: 'bottom-center', // 定位方式
    stopEvent: false,             // 不阻止事件(可以点击地图等)
  })
  map.addOverlay(popupOverlay)    // 添加到地图
}

Overlay 的其他可配置属性:

属性描述类型默认值
element绑定的 DOM 元素,用于显示 Overlay 内容HTMLElement必须设置
positioning定位方式,决定元素相对于坐标的位置,常用值:'top-left''bottom-center'string'top-left'
stopEvent是否阻止事件传播,false表示不阻止事件booleantrue
autoPan启用自动平移,确保 Overlay 显示在视图中booleanfalse
autoPanMargin自动平移时的边距(像素),用于控制弹窗与视图边界的间距number20
offset相对于坐标的偏移量(像素),调整 Overlay 的显示位置[number, number][0, 0]
insertFirst是否将 Overlay 插入到所有图层的最前面booleanfalse

3. 监听地图事件,展示悬浮框

当鼠标移动时设定弹窗坐标,达到弹框跟随鼠标移动的效果

const addMapEvents = () => {
  // 鼠标移动
  map.on('pointermove', function (e) {
    console.log(e.coordinate)
    popupOverlay.setPosition(e.coordinate) // 设置弹框位置
  })
}

addMapEvents()

这样,一个简单的 openlayers 弹框就完成了,以下是效果展示

4. 完整代码

<template>
  <div class="base-map w-[100%] h-[100%] flex justify-center items-center">
    <!-- 地图组件 -->
    <div id="map" style="width: 500px; height: 300px"></div>
    <!-- 地图弹框组件 -->
    <div ref="popupElement" style="width: 50px; height: 50px; background-color: palevioletred">弹框</div>
  </div>
</template>

<script setup>
import { Map, Overlay, View } from 'ol'
import TileLayer from 'ol/layer/Tile'
import { OSM } from 'ol/source'
import { onMounted, ref } from 'vue'

var map = null
onMounted(() => {
  initMap()
})

const initMap = () => {
  map = new Map({
    target: 'map', // 地图容器div的id
    layers: [
      // 图层
      new TileLayer({
        source: new OSM(), // 图层数据源 openlayer自带默认全球瓦片地图
      }),
    ],
    controls: [], // 设为空数组,去除默认控件
    view: new View({
      // 地图视图
      center: [0, 0], // 地图中心点坐标
      projection: 'EPSG:4326', // 地图坐标系,有EPSG:4326和EPSG:3857
      zoom: 2, // 地图默认缩放级别
    }),
  })
  initMapOverlay()
  addMapEvents()
}

const popupElement = ref(null) // 弹框DOM
let popupOverlay // 弹框 Overlay 实例
// 初始化地图弹框
const initMapOverlay = () => {
  popupOverlay = new Overlay({
    element: popupElement.value, // 绑定弹框DOM
    positioning: 'bottom-center', // 定位方式
    stopEvent: false, // 不阻止事件(可以点击地图等)
  })
  map.addOverlay(popupOverlay) // 添加到地图
}

const addMapEvents = () => {
  // 鼠标移动
  map.on('pointermove', function (e) {
    popupOverlay.setPosition(e.coordinate) // 设置弹框位置
  })
}
</script>

5. 悬浮框优化01

上面一个弹框会有一个问题:当鼠标靠近地图边界时,弹窗会超出地图。像下面这样:

可以使用 DOM.getBoundingClientRect() 获取弹窗和地图组件的最顶部/底部/左/右位置,进行判断弹窗是否超出地图,然后通过修改弹窗的定位方式来解决这个问题

在鼠标移动的监听事件中添加该判断

map.on('pointermove', function (e) {
    popupOverlay.setPosition(e.coordinate) // 设置弹框位置
    const popRect = popupElement.value.getBoundingClientRect()
    const popTop = popRect.top
    const popLeft = popRect.left
    const popRight = popRect.right
    const popBottom = popRect.bottom
    const mapRect = mapElement.value.getBoundingClientRect()
    const mapTop = mapRect.top
    const mapLeft = mapRect.left
    const mapRight = mapRect.right
    const mapBottom = mapRect.bottom

    // 弹框超出地图边界处理
    let overTop = mapTop - popTop
    let overLeft = mapLeft - popLeft
    let overRight = popRight - mapRight
    let overBottom = popBottom - mapBottom

    if (overTop > 0) {
      if (overLeft > 0) {
        console.log('超出左上角')
        changeOverlayPosition('top-left')
      } else if (overRight > 0) {
        console.log('超出右上角')
        changeOverlayPosition('top-right')
      } else {
        console.log('超出上边界')
        changeOverlayPosition('top-center')
      }
    } else if (overBottom > 0) {
      if (overLeft > 0) {
        console.log('超出左下角')
        changeOverlayPosition('bottom-left')
      } else if (overRight > 0) {
        console.log('超出右下角')
        changeOverlayPosition('bottom-right')
      } else {
        console.log('超出下边界')
        changeOverlayPosition('bottom-center')
      }
    } else if (overLeft > 0) {
      console.log('超出左边界')
      changeOverlayPosition('center-left')
    } else if (overRight > 0) {
      console.log('超出右边界')
      changeOverlayPosition('center-right')
    }
  })

这里是修改弹窗定位方式的方法,overlaypositioning 是无法直接修改的,所以要销毁再重建。

const changeOverlayPosition = (position) => {
  // 销毁目前的 overlay
  map.removeOverlay(popupOverlay)
  // 重新创建 overlay
  popupOverlay = new Overlay({
    element: popupElement.value, // 绑定弹框DOM
    positioning: position, // 定位方式
    stopEvent: false, // 不阻止事件(可以点击地图等)
  })
  map.addOverlay(popupOverlay) // 添加到地图
}

效果演示:这里看不到鼠标所以看起来会有点莫名其妙,但其实是已经不会再触发弹窗超出地图了的。

6.优化01 完整代码

<template>
  <div class="base-map w-[100%] h-[100%] flex justify-center items-center">
    <!-- 地图组件 -->
    <div id="map" ref="mapElement" style="width: 500px; height: 300px"></div>
    <!-- 地图弹框组件 -->
    <div ref="popupElement" style="width: 50px; height: 50px; background-color: palevioletred">弹框</div>
  </div>
</template>

<script setup>
import { Map, Overlay, View } from 'ol'
import TileLayer from 'ol/layer/Tile'
import { OSM } from 'ol/source'
import { onMounted, ref } from 'vue'

var map = null
const mapElement = ref(null) // 地图DOM

onMounted(() => {
  initMap()
})

function initMap() {
  map = new Map({
    target: 'map', // 地图容器div的id
    layers: [
      // 图层
      new TileLayer({
        source: new OSM(), // 图层数据源 openlayer自带默认全球瓦片地图
      }),
    ],
    controls: [], // 设为空数组,去除默认控件
    view: new View({
      // 地图视图
      center: [0, 0], // 地图中心点坐标
      projection: 'EPSG:4326', // 地图坐标系,有EPSG:4326和EPSG:3857
      zoom: 2, // 地图默认缩放级别
    }),
  })
  initMapOverlay()
  addMapEvents()
}

const popupElement = ref(null) // 弹框DOM
let popupOverlay // 弹框 Overlay 实例
// 初始化地图弹框
const initMapOverlay = () => {
  popupOverlay = new Overlay({
    element: popupElement.value, // 绑定弹框DOM
    positioning: 'bottom-center', // 定位方式
    stopEvent: false, // 不阻止事件(可以点击地图等)
  })
  map.addOverlay(popupOverlay) // 添加到地图
}

const addMapEvents = () => {
  // 鼠标移动
  map.on('pointermove', function (e) {
    popupOverlay.setPosition(e.coordinate) // 设置弹框位置
    const popRect = popupElement.value.getBoundingClientRect()
    const popTop = popRect.top
    const popLeft = popRect.left
    const popRight = popRect.right
    const popBottom = popRect.bottom
    const mapRect = mapElement.value.getBoundingClientRect()
    const mapTop = mapRect.top
    const mapLeft = mapRect.left
    const mapRight = mapRect.right
    const mapBottom = mapRect.bottom

    // 弹框超出地图边界处理
    let overTop = mapTop - popTop
    let overLeft = mapLeft - popLeft
    let overRight = popRight - mapRight
    let overBottom = popBottom - mapBottom

    if (overTop > 0) {
      if (overLeft > 0) {
        console.log('超出左上角')
        changeOverlayPosition('top-left')
      } else if (overRight > 0) {
        console.log('超出右上角')
        changeOverlayPosition('top-right')
      } else {
        console.log('超出上边界')
        changeOverlayPosition('top-center')
      }
    } else if (overBottom > 0) {
      if (overLeft > 0) {
        console.log('超出左下角')
        changeOverlayPosition('bottom-left')
      } else if (overRight > 0) {
        console.log('超出右下角')
        changeOverlayPosition('bottom-right')
      } else {
        console.log('超出下边界')
        changeOverlayPosition('bottom-center')
      }
    } else if (overLeft > 0) {
      console.log('超出左边界')
      changeOverlayPosition('center-left')
    } else if (overRight > 0) {
      console.log('超出右边界')
      changeOverlayPosition('center-right')
    }
  })
}

const changeOverlayPosition = (position) => {
  // 销毁目前的 overlay
  map.removeOverlay(popupOverlay)
  // 重新创建 overlay
  popupOverlay = new Overlay({
    element: popupElement.value, // 绑定弹框DOM
    positioning: position, // 定位方式
    stopEvent: false, // 不阻止事件(可以点击地图等)
  })
  map.addOverlay(popupOverlay) // 添加到地图
}
</script>

7. 悬浮框优化02

在某些需求中弹框将会使用特定的组件,并且拥有会有一个指向的箭头组件,向如下这样。优化1 将不会在满足。

这个悬浮框我的优化思路是使用两个单独的组件来展示

使用了组件1旋转45度来充当箭头,组件2为弹框主体,用来放置需要展示的信息。注意:组件2需要比组件1多出 width/2

<!-- 组件1 -->
    <div ref="popupPointElement" style="width: 10px; height: 10px; transform: rotate(45deg); background-color: palevioletred; margin: 5px"></div>
    <!-- 组件2 -->
    <div ref="popupElement" style="width: 50px; height: 80px; background-color: palevioletred; margin: 10px">弹框</div>

两个组件都绑定上弹框

const popupElement = ref(null) // 弹框DOM
const popupPointElement = ref(null) // 弹框定位DOM
let popupOverlay // 弹框 Overlay 实例
let popupPointOverlay // 弹框定位 Overlay 实例
// 初始化地图弹框
const initMapOverlay = () => {
  popupOverlay = new Overlay({
    element: popupElement.value, // 绑定弹框DOM
    positioning: 'center-left', // 定位方式
    stopEvent: false, // 不阻止事件(可以点击地图等)
  })
  popupPointOverlay = new Overlay({
    element: popupPointElement.value,
    positioning: 'center-left',
    stopEvent: false,
  })
  map.addOverlay(popupOverlay) // 添加到地图
  map.addOverlay(popupPointOverlay)
}

这是目前的效果:

接下来是鼠标移动时弹框的显示逻辑(最重要部分)

当弹框将要超出地图的边界时,通过固定弹框的 y 轴坐标来阻止。map.getCoordinateFromPixel()用于将像素值转化为地图坐标, map 为创建的地图实例。并且,弹窗左右边界使用优化1的处理方式,使这个弹框变得完美无缺。

const addMapEvents = () => {
  // 鼠标移动
  map.on('pointermove', function (e) {
    popupOverlay.setPosition(e.coordinate) // 设置弹框位置
    popupPointOverlay.setPosition(e.coordinate) // 设置弹框定位位置

    // 通过弹窗dom判断弹窗是否溢出屏幕,如果溢出屏幕,弹窗y轴位置不变
    const boxRect = popupElement.value.getBoundingClientRect()
    const mapRect = mapElement.value.getBoundingClientRect()
    // 弹窗高度
    const boxHeight = boxRect.height
    // 鼠标悬停 Y轴位置
    const pixelY = e.pixel[1]
    // 如果鼠标悬停高度-弹框高度/2 < 0,说明弹框顶部溢出屏幕
    // 如果鼠标悬停高度+弹框高度/2 > 地图底部-地图顶部,说明弹框底部溢出屏幕
    // 由于 pixelY 是根据地图的左上角为原点,所以需要加上地图的顶部位置
    if (pixelY - boxHeight / 2 < 0) {
      // 弹框触顶
      popupOverlay.setPosition(map.getCoordinateFromPixel([e.pixel[0], boxHeight / 2]))
    } else if (pixelY + boxHeight / 2 > mapRect.bottom - mapRect.top) {
      // 弹框触底
      popupOverlay.setPosition(map.getCoordinateFromPixel([e.pixel[0], mapRect.bottom - mapRect.top - boxHeight / 2]))
    } else {
      popupOverlay.setPosition(e.coordinate)
    }

    // 判断弹窗是否超出地图左/右边界
    if (boxRect.right > mapRect.right) {
      changeOverlayPosition('center-right')
    } else if (boxRect.left < mapRect.left) {
      changeOverlayPosition('center-left')
    }
  })
}

const changeOverlayPosition = (position) => {
  // 销毁目前的 overlay
  map.removeOverlay(popupOverlay)
  map.removeOverlay(popupPointOverlay)
  // 重新创建 overlay
  popupOverlay = new Overlay({
    element: popupElement.value, // 绑定弹框DOM
    positioning: position, // 定位方式
    stopEvent: false, // 不阻止事件(可以点击地图等)
  })
  popupPointOverlay = new Overlay({
    element: popupPointElement.value,
    positioning: position,
    stopEvent: false,
  })
  map.addOverlay(popupOverlay) // 添加到地图
  map.addOverlay(popupPointOverlay)
}

效果展示:

8. 优化02 完整代码

<template>
  <div class="base-map w-[100%] h-[100%] flex justify-center items-center">
    <!-- 地图组件 -->
    <div id="map" ref="mapElement" style="width: 500px; height: 300px"></div>

    <!-- 组件1 -->
    <div ref="popupPointElement" style="width: 10px; height: 10px; transform: rotate(45deg); background-color: palevioletred; margin: 5px"></div>
    <!-- 组件2 -->
    <div ref="popupElement" style="width: 50px; height: 80px; background-color: palevioletred; margin: 10px">弹框</div>
  </div>
</template>

<script setup>
import { Map, Overlay, View } from 'ol'
import TileLayer from 'ol/layer/Tile'
import { OSM } from 'ol/source'
import { onMounted, ref } from 'vue'

var map = null
const mapElement = ref(null) // 地图DOM

onMounted(() => {
  initMap()
})

function initMap() {
  map = new Map({
    target: 'map', // 地图容器div的id
    layers: [
      // 图层
      new TileLayer({
        source: new OSM(), // 图层数据源 openlayer自带默认全球瓦片地图
      }),
    ],
    controls: [], // 设为空数组,去除默认控件
    view: new View({
      // 地图视图
      center: [0, 0], // 地图中心点坐标
      projection: 'EPSG:4326', // 地图坐标系,有EPSG:4326和EPSG:3857
      zoom: 2, // 地图默认缩放级别
    }),
  })
  initMapOverlay()
  addMapEvents()
}

const popupElement = ref(null) // 弹框DOM
const popupPointElement = ref(null) // 弹框定位DOM
let popupOverlay // 弹框 Overlay 实例
let popupPointOverlay // 弹框定位 Overlay 实例
// 初始化地图弹框
const initMapOverlay = () => {
  popupOverlay = new Overlay({
    element: popupElement.value, // 绑定弹框DOM
    positioning: 'center-left', // 定位方式
    stopEvent: false, // 不阻止事件(可以点击地图等)
  })
  popupPointOverlay = new Overlay({
    element: popupPointElement.value,
    positioning: 'center-left',
    stopEvent: false,
  })
  map.addOverlay(popupOverlay) // 添加到地图
  map.addOverlay(popupPointOverlay)
}

const addMapEvents = () => {
  // 鼠标移动
  map.on('pointermove', function (e) {
    popupOverlay.setPosition(e.coordinate) // 设置弹框位置
    popupPointOverlay.setPosition(e.coordinate) // 设置弹框定位位置

    // 通过弹窗dom判断弹窗是否溢出屏幕,如果溢出屏幕,弹窗y轴位置不变
    const boxRect = popupElement.value.getBoundingClientRect()
    const mapRect = mapElement.value.getBoundingClientRect()
    // 弹窗高度
    const boxHeight = boxRect.height
    // 鼠标悬停 Y轴位置
    const pixelY = e.pixel[1]
    // 如果鼠标悬停高度-弹框高度/2 < 0,说明弹框顶部溢出屏幕
    // 如果鼠标悬停高度+弹框高度/2 > 地图底部-地图顶部,说明弹框底部溢出屏幕
    // 由于 pixelY 是根据地图的左上角为原点,所以需要加上地图的顶部位置
    if (pixelY - boxHeight / 2 < 0) {
      // 弹框触顶
      popupOverlay.setPosition(map.getCoordinateFromPixel([e.pixel[0], boxHeight / 2]))
    } else if (pixelY + boxHeight / 2 > mapRect.bottom - mapRect.top) {
      // 弹框触底
      popupOverlay.setPosition(map.getCoordinateFromPixel([e.pixel[0], mapRect.bottom - mapRect.top - boxHeight / 2]))
    } else {
      popupOverlay.setPosition(e.coordinate)
    }

    // 判断弹窗是否超出地图左/右边界
    if (boxRect.right > mapRect.right) {
      changeOverlayPosition('center-right')
    } else if (boxRect.left < mapRect.left) {
      changeOverlayPosition('center-left')
    }
  })
}

const changeOverlayPosition = (position) => {
  // 销毁目前的 overlay
  map.removeOverlay(popupOverlay)
  map.removeOverlay(popupPointOverlay)
  // 重新创建 overlay
  popupOverlay = new Overlay({
    element: popupElement.value, // 绑定弹框DOM
    positioning: position, // 定位方式
    stopEvent: false, // 不阻止事件(可以点击地图等)
  })
  popupPointOverlay = new Overlay({
    element: popupPointElement.value,
    positioning: position,
    stopEvent: false,
  })
  map.addOverlay(popupOverlay) // 添加到地图
  map.addOverlay(popupPointOverlay)
}
</script>