文章目录:
先展示完成效果
1. 编写悬浮框组件
需要配置 ref 使 ol 能获取该组件
<div ref="popupElement" style="width: 50px; height: 50px">弹框</div>
2. 创建悬浮框 Overlay 实例,与组件绑定
使用 new Overlay 创建一个 Overlay 实例并在地图中添加改实例。注意:Overlay 不能在初始化的时候直接创建,这时候 Dom 还没有被加载,会报错。
创建一个基本的 Overlay只需要传入 element 属性即可,element 为 Overlay 需要绑定的组件 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表示不阻止事件 | boolean | true |
autoPan | 启用自动平移,确保 Overlay 显示在视图中 | boolean | false |
autoPanMargin | 自动平移时的边距(像素),用于控制弹窗与视图边界的间距 | number | 20 |
offset | 相对于坐标的偏移量(像素),调整 Overlay 的显示位置 | [number, number] | [0, 0] |
insertFirst | 是否将 Overlay 插入到所有图层的最前面 | boolean | false |
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')
}
})
这里是修改弹窗定位方式的方法,overlay 的 positioning 是无法直接修改的,所以要销毁再重建。
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>