一、概述
我之前在文章 OpenLayers:封装Tooltip-CSDN博客 中封装了一个Overlay
的子类Tooltip
,实现了一些原生的Overlay
所不具备的功能。现在我更进一步封装一个Tooltip
的子类Popup
。所以请先阅读我的上一篇文章。
在我的设计中Popup
完全继承Tooltip
原有的功能。它新增的功能只有一个,那就是Popup
默认隐藏,并会与一个目标相关联,这个目标可以是一个要素或是一个坐标位置,只有鼠标点击或悬停到关联目标上,Popup
才会显示。

二、实现弹出功能
我的基本思路就是,首先增加两个属性target
和trigger
,target
决定popup所绑定的目标是什么,trigger
则表示触发提示框弹出的方式。然后去侦听地图对应的事件(鼠标点击或者鼠标移动事件),在事件处理函数中去判断目标是否被点击到了(或是否悬停在其上方),若被点击到则显示提示框,没被点击到就隐藏提示框。
1.新增target和trigger属性
target
属性表示popup所关联的对象,在我的设想中target
可以支持四种类型:
Featrue
,一个要素,例如new Feature(new Point(...))
Geomtry
,一个几何图形,例如Point
、LineString
、Ploygon
等对象。Coordinate
,一个坐标,例如[112 , 23]
Extent
,一个坐标范围,例如[112.9 , 23.5 , 113.4 , 23.7]
JavaScript
/**
* 所绑定的目标对象。
* @type { Feature | Geometry | import("ol/coordinate").Coordinate | import("ol/extent").Extent}
* @private
*/
this.target = options.target;
trigger
属性表示触发弹出的方式,它有两种值'click' | 'hover'
JavaScript
/**
* 弹窗的触发方式。
* @type {'click' | 'hover'}
* @private
*/
this.trigger = options.trigger;
2.侦听地图事件
我要根据触发方式的不同来侦听不同的地图事件。如果trigger
为 'click'
则侦听地图的click
事件 ,如果trigger
为 'hover'
则侦听地图的pointermove
事件。
JavaScript
class Popup extends Tooltip {
constructor(options) {
......
this.on("change:map", this.handlePopupMapChange);
}
/**
* 处理地图变更时的回调。
* @protected
* @returns {void}
*/
handlePopupMapChange() {
const map = this.getMap();
if (map) {
const eventType = this.trigger === "click" ? "click" : "pointermove";
this.listenKeys.push(
listen(map, eventType, this.handleTargetTrigger, this)
);
} else {
// 移除所有事件监听
this.removeAllListeners();
}
}
/**
* 处理目标触发事件。
* @protected
* @param {import("ol/MapBrowserEvent").default} event 地图浏览事件对象
* @returns {void}
*/
handleTargetTrigger(event) {
const coordinate = event.coordinate;
}
}
3.如何判断是否触发了target?
我的思路是这样,由于target
有多种可能的类型,所以我要先将target
转换成一种固定的类型,我选择全都转换为Extent
。这样问题就被转化为了鼠标当前点击(悬停)位置的坐标是否在一个Extent
(也就是target
)的范围内, 可以利用OpenLayers内置的containsCoordinate
方法来实现这样的判断。
我写了一个工具方法getTargetExtent
来实现target
转Extent
的功能。但是遇到一个问题,当我将一个Point
或Cooridnate
转换为一个Extent
后,其实Extent
还是一个点,例如将[112 , 23]
转换之后的结果就是[112 , 23 , 112 , 23]
,在实际的操作中这样单独的一个坐标根本点击不到。因此当target
是一个单独点位的时候,我将会转换后的Extent
扩大,我会将其扩大为一个以target
为中心边长10像素的Extent
。
JavaScript
function getTargetExtent(map, target) {
if (
Array.isArray(target) &&
target.every(item => typeof item === "number") &&
target.length === 4
)
return target;
let geometry = null;
if (target instanceof Feature) {
geometry = target.getGeometry();
} else if (target instanceof Geometry) {
geometry = target;
} else if (
Array.isArray(target) &&
target.every(item => typeof item === "number") &&
target.length === 2
) {
geometry = new Point(target);
}
if (geometry instanceof Point) {
const coordinate = geometry.getCoordinates();
const resolution = map.getView().getResolution();
const extentSize = 5 * resolution;
const extent = [
coordinate[0] - extentSize,
coordinate[1] - extentSize,
coordinate[0] + extentSize,
coordinate[1] + extentSize,
];
return extent;
} else {
return geometry.getExtent();
}
}
之后在handlePopupMapChange
中调用getTargetExtent
方法,对target
进行处理:
JavaScript
handlePopupMapChange() {
const map = this.getMap();
// 移除所有事件监听
this.removeAllListeners();
if (map) {
this.targetExtent = getTargetExtent(map, this.target);
const eventType = this.trigger === "click" ? "click" : "pointermove";
this.listenKeys.push(
listen(map, eventType, this.handleTargetTrigger, this)
);
}
}
最后在handleTargetTrigger
中调用containsCoordinate
方法进行判断。
JavaScript
import { containsCoordinate } from "ol/extent";
/**
* 处理目标触发事件。
* @protected
* @param {import("ol/MapBrowserEvent").default} event 地图浏览事件对象
* @returns {void}
*/
handleTargetTrigger(event) {
const coordinate = event.coordinate;
// 目标被触发
if (containsCoordinate(this.targetExtent, coordinate)) {
this.open();
}
// 目标未触发
else {
this.isAutoClose && this.close();
}
}
4.控制提示框的显隐
当目标被触发时就要显示提示框,反之则要隐藏提示框。为控制提示框的显隐我专门写了open()
与close()
两个方法,它们本质上还是在利用Tooltip
中的visibility
控制显隐。当然这里也有可能会出现显隐冲突的问题,因此我新增了一个isTrigger
属性来表示target
是否被触发,并重写了setVisibility()
方法,将isTrigger
作为限制条件加入了进去。
JavaScript
/**
* 设置弹窗的可见性。
* @override
* @param {boolean} visibility 是否可见
* @returns {void}
*/
setVisibility(visibility) {
if (!this.isTrigger && this.isOnZoomRange) return;
this.set("visibility", visibility);
}
/**
* 关闭弹窗。
* @returns {void}
*/
close() {
this.isTrigger = false;
this.set("visibility", false);
}
/**
* 打开弹窗。
* @returns {void}
*/
open() {
this.isTrigger = true;
this.set("visibility", true);
}
最终呈现的效果如下:

完整代码
JavaScript
import Feature from "ol/Feature";
import { Geometry, Point, Circle } from "ol/geom";
import { containsCoordinate } from "ol/extent";
import Tooltip from "./Tooltip";
import { listen } from "ol/events";
import "./Popup.css";
function getTargetExtent(map, target) {
if (
Array.isArray(target) &&
target.every(item => typeof item === "number") &&
target.length === 4
)
return target;
let geometry = null;
if (target instanceof Feature) {
geometry = target.getGeometry();
} else if (target instanceof Geometry) {
geometry = target;
} else if (
Array.isArray(target) &&
target.every(item => typeof item === "number") &&
target.length === 2
) {
geometry = new Point(target);
}
if (geometry instanceof Point) {
const coordinate = geometry.getCoordinates();
const resolution = map.getView().getResolution();
const extentSize = 5 * resolution;
const extent = [
coordinate[0] - extentSize,
coordinate[1] - extentSize,
coordinate[0] + extentSize,
coordinate[1] + extentSize,
];
return extent;
} else {
return geometry.getExtent();
}
}
/**
* Popup 弹出框覆盖物类,继承自 Tooltip。
* @class
* @augments Tooltip
* @classdesc 自定义的 OpenLayers 弹出框覆盖物类,继承自 ol/Overlay。
*
* @param {Object} options 配置选项
* @param {Feature | Geometry | import("ol/coordinate").Coordinate | import("ol/extent").Extent} options.target 所绑定的目标
* @param {'click' | 'hover'} options.trigger 触发方式
*/
class Popup extends Tooltip {
/**
* 创建 Popup 实例。
* @param {Object} options 配置选项
* @param {Feature | Geometry | import("ol/coordinate").Coordinate | import("ol/extent").Extent} options.target 所绑定的目标
* @param {'click' | 'hover'} options.trigger 触发方式
*/
constructor(options) {
super(options);
/**
* 是否已触发弹窗显示。
* @type {boolean}
* @private
*/
this.isTrigger = false;
/**
* 弹窗的触发方式。
* @type {'click' | 'hover'}
* @private
*/
this.trigger = options.trigger;
/**
* 所绑定的目标对象。
* @type { Feature | Geometry | import("ol/coordinate").Coordinate | import("ol/extent").Extent}
* @private
*/
this.target = options.target;
/**
* 目标的范围。
* @type { import("ol/extent").Extent}
* @private
*/
this.targetExtent = null;
/**
* 是否自动关闭弹窗
* @type {boolean}
* @private
*/
this.isAutoClose =
options.isAutoClose !== undefined ? options.isAutoClose : true;
/**
* 是否显示关闭按钮
* @type {boolean}
* @private
*/
this.isShowCloseBtn =
options.isShowCloseBtn !== undefined ? options.isShowCloseBtn : false;
this.set("visibility", false);
this.on("change:map", this.handlePopupMapChange);
}
/**
* 处理地图变更时的回调。
* @protected
* @returns {void}
*/
handlePopupMapChange() {
const map = this.getMap();
if (map) {
this.targetExtent = getTargetExtent(map, this.target);
const eventType = this.trigger === "click" ? "click" : "pointermove";
this.listenKeys.push(
listen(map, eventType, this.handleTargetTrigger, this)
);
} else {
// 移除所有事件监听
this.removeAllListeners();
}
}
/**
* 处理目标触发事件。
* @protected
* @param {import("ol/MapBrowserEvent").default} event 地图浏览事件对象
* @returns {void}
*/
handleTargetTrigger(event) {
const coordinate = event.coordinate;
if (containsCoordinate(this.targetExtent, coordinate)) {
this.open();
} else {
this.isAutoClose && this.close();
}
}
/**
* 设置弹窗的可见性。
* @override
* @param {boolean} visibility 是否可见
* @returns {void}
*/
setVisibility(visibility) {
if (!this.isTrigger && this.isOnZoomRange) return;
this.set("visibility", visibility);
}
/**
* 格式化弹窗元素。
* @override
* @param {HTMLElement} element 弹窗元素
* @returns {HTMLElement} 格式化后的弹窗元素
*/
formatElement(element) {
const _element = super.formatElement(element);
if (this.isUseContainer) {
const container = _element;
container.className = `ol-custom-popup ${this.getPlacement()}`;
if (this.isShowCloseBtn) {
const closeButton = document.createElement("div");
closeButton.className = "ol-custom-popup-close";
closeButton.innerHTML = "×";
container.appendChild(closeButton);
closeButton.addEventListener(
"click",
function () {
this.close();
}.bind(this)
);
}
}
return _element;
}
/**
* 关闭弹窗。
* @returns {void}
*/
close() {
this.isTrigger = false;
this.set("visibility", false);
}
/**
* 打开弹窗。
* @returns {void}
*/
open() {
this.isTrigger = true;
this.set("visibility", true);
}
}
export default Popup;