在我之前写的一篇文章 OpenLayers:封装Overlay的方法_openlayer overlay-CSDN博客 中我封装一个工具方法addOverlay
,当时我通过这个方法给Overlay
增加一些额外的功能,如:控制显隐、分组、支持传入DOMString作为element
等。但是之前的封装方式还是太过简陋有种种缺陷,使用起来也不甚方便。
最近我封装了一个Overlay
的子类Tooltip
类 ,在其中集成了之前的那些功能,并且还新增了一些功能。接下来我将在这篇文章中阐述各个功能的实现思路以及实现过程中我的一些收获。
一、Tooltip功能概述
首先我要简单的说明一下我将要封装的Tooltip
类,首先我会让它作为Overlay
的子类,这样就可以继承Overlay
本身的各项功能,并且Tooltip
的实例也可以直接被Map.addOverlay
添加到地图中。
JavaScript
import Overlay from "ol/Overlay";
class Tooltip extends Overlay {
......
}
---------------------------------------
const tooltip = new Tooltip()
map.addOverlay(tooltip) // 直接可以将Tooltip的实例也可以被 `addOverlay` 等地图方法使用
我想要在Tooltip
类中实现的功能主要有以下这些:
1.默认模版样式
我希望我的提示框可以默认有如下的容器样式(有一个蓝色的背景,有一个指向地图位置的 '小箭头' 等)。这样以后再不同项目中复用Tooltip
的时候就不用总是自己写样式了。

2.支持 "上、下、左、右" 四个方位
虽然Overlay
中有positioning
属性可以自由设置Overlay
的相对位置但是我认为这个属性并不好用。因为positioning
属性是用来表示位置点相对与Overlay
的位置,例如将positioning
设置为'bottom-center'
表示位置点位于Overlay
的正下方(即Overlay
位于位置点的正上方)。
我个人还是更习惯以Overlay
为"主体"来描述其与位置点的位置关系,例如'top'
表示Overlay
位于位置点的正上方。所以我希望可以创建一个新的属性来替代positioning
属性的功能。
另外由于我的提示框有一个指向位置点的小箭头,这个小箭头的位置也要随着提示框位置的变化而变化,因此就要做对应的处理。

3.支持以DOMString作为element属性的值
Overlay
的element
属性只支持接收一个 HTMLElement。

如果像下面这样传入一个DOMString作为element`,则会报错。
JavaScript
new Overlay({
element: `<div>我是一个Overlay</div>`
})
在实际使用过程中我感觉这十分不便,很多时候我在创建一些简单的Overlay
时,我希望可以支持直接传入一个DOMString。
4.分组
在实际的开发中常常会向地图中添加几种不同的Overlay
,并需要对同一种类的Overlay
进行统一的操作(例如,全部移除、全部隐藏等)。而由于Overlay
中只有一个id
属性来标识不同的个体,所以导致批量的操作非常不方便,我很难将某一种类的Overlay
筛选出来。因此我希望Overlay
可以有一个groupId
属性来标识它们的种类。
希望可以达到类似于下面这样的效果:
JavaScript
// 添加某一类的Tooltip,类标识为tooltip-test
const positions = [];
positions.forEach((position, index) => {
const tooltip = new Tooltip({
id: `tooltip-${index}`,
groupId: "tooltip-test",
element: `tooltip-${index}`,
position: position,
});
map.addOverlay(tooltip);
});
// 获取某一类的Tooltip,类标识为tooltip-test
const tooltips = map.getOverlays().getArray().filter(overlay => {
let groupId = overlay.getGroupId ? overlay.getGroupId() : overlay.groupId;
return groupId ? groupId === "tooltip-test" : false;
});
5.控制显隐
我在实际的开发常遇到需要暂时隐藏(关闭)Overlay
的需求,但是苦于Overlay
不像Layer
那样有visible
属性来控制显隐。
我只能通过移除Overlay
或者是将position
置空的方式来实现隐藏的效果。但是使用这些方法隐藏容易,重新显示就难了。需要将Overlay
缓存起来以便重新添加到地图中,或者将position
的值缓存起来以便重新设置Overlay
的位置。
scss
// 移除Overlay
map.removeOverlay(tooltip)
// 将Overlay的position置空
tooltip.setPosition(null)
希望可以达到类似于下面这样的效果:

6.限制显示的缩放层级
另一个常见的需求是希望Tooltip只在某些缩放层级中被显示。就像Layer
的minZoom
、maxZoom
那样

希望可以达到类似于下面这样的效果:

7.设置层级
Overlay
本身不具有像zIndex
这样设置层级的属性,因此当多个Overlay
重叠在一起时我无法设置哪个在上哪个在下。
二、实现默认模版样式与支持DOMString
我的基本实现思路是创建一个容器元素来包裹传入的element
属性值,然后给这个默认容器元素设置模版样式。Overlay
的源码中也是遵循这样的思路,会给传入的element
包裹一层.ol-overlay-container
元素。

1.编写一个formatElement方法
这个方法可以格式element
,它接受一个参数element
(可以是 HTMLElement 或 DOMString),然后对其进行处理。会将element
由 DOMString 转换为 HTMLElement 并为其包裹一个.ol-custom-tooltip
元素以应用默认模版样式。
JavaScript
/**
* 格式化元素
* @param {HTMLElement | string} element - 元素
* @returns {HTMLElement} 格式化后的元素
*
*/
formatElement(element) {
let _element = element;
// DOMString 转换为 HTMLElement
if (typeof element === "string") {
const div = document.createElement("div");
div.innerHTML = element;
_element = div.firstElementChild;
}
// 使用自定义的模版
if (this.isUseContainer) {
const container = document.createElement("div");
container.className = `ol-custom-tooltip ${this.getPlacement()}`;
container.appendChild(_element);
_element = container;
}
return _element;
}
2.编写CSS样式
我在一个 Tooltip.css 文件中编写相应的默认样式,这个文件会被导入 Tooltip
类所在的js文件。
JavaScript
/* 基础样式 */
.ol-custom-tooltip {
--bg: rgba(25, 82, 78, 0.8);
--arrowSize: 10px;
position: relative;
z-index: 100;
min-width: 120px;
color: #fff;
padding: 5px;
background-color: var(--bg);
border: none;
border-radius: 4px;
line-height: 1;
font-size: 12px;
text-align: start;
box-sizing: border-box;
user-select: none;
}
.ol-custom-tooltip::after {
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
border-width: var(--arrowSize);
}
.ol-custom-tooltip.top::after {
top: 100%;
left: 50%;
transform: translateX(-50%);
border-top-color: var(--bg) !important;
}
.ol-custom-tooltip.bottom::after {
bottom: 100%;
left: 50%;
transform: translateX(-50%);
border-bottom-color: var(--bg) !important;
}
.ol-custom-tooltip.left::after {
left: 100%;
top: 50%;
transform: translateY(-50%);
border-left-color: var(--bg) !important;
}
.ol-custom-tooltip.right::after {
right: 100%;
top: 50%;
transform: translateY(-50%);
border-right-color: var(--bg) !important;
}
3.重写setElement方法
由于Overlay
是一个可写属性,可以通过overlay.setElement
方法来修改它。因此我的计划是提前拦截传入的element
值,在它被设置之前对其进行 ' 格式化 ' 。
因此我在Tooltip
类中重写setElement
方法,以覆盖它的父类Overlay
类中setElement
方法。在新的setElement
方法中提前对element
进行格式化。
JavaScript
/**
*
* @override
*/
setElement(element) {
let _element = this.formatElement(element);
super.setElement(_element);
}
小技巧:钩子函数
这里我就要讲一个我自己总结的技巧了:
在OpenLayers中开发一个新类的时候,如果需要在父类的某个属性被设置前执行一个钩子函数 beforeHook
,可以通过重写属性的set
方法来实现,在新的set
中先执行钩子函数,再执行父类的set
方法(即super.set
)。
JavaScript
// 子类上的新 set 方法
setProperty(property){
// 要执行的 `设置前钩子函数`
beforeHook()
// 父类上的 set 方法
super.setProperty(property)
}
如果需要在父类的某个属性被设置后执行一个钩子函数afterHook
,可以将钩子函数作为该属性的change
事件(即change:property
事件)的事件处理函数。
JavaScript
overlay.on('change:property' , afterHook)
三、实现对 ' 四方位 ' 的支持
基本思路是我新增一个placement
属性,它部分替代positioning
功能,会设置placement
属性与positioning
属性进行 "双向绑定"(其中一个属性发生改变后另一个也会跟着变化)。另外,在设置placement
属性值的时候也还会同步更改提示框小箭头的方向。
小技巧:普通属性和Property属性
在OpenLayers的类中的属性分为两种:普通属性 和Property属性。
可以看下图中是Overlay
类中的两个属性的get
方法,其中element
属性是使用this.get('element')
的方式读取,而id
属性则是通过this.id
的方式读取。element
就是Property属性,而id
则是普通属性。

普通属性和Property属性区别主要有两点:
- 普通属性直接添加到class中通过
this.xxx
进行读写。Property属性会被添加到一个特殊的存储对象value_
中,只能通过get
、set
、setProperties
等方法读写。 - Property属性会自动注册对应的事件
'change:xxx'
,Property属性的改变会触发对应的事件。而普通属性则不具备这样的功能。
因此在添加一个新属性时如果该属性是一个 ' 常量 ' 则应该设置为普通属性,如果该属性是一个' 变量 ' 则应该设置为Property属性。
1.创建placement属性
placement
属性表示Tooltip
相对于position
的位置,它有'top' | 'bottom' | 'left' | 'right'
四个可选的值。placement
属性与positioning
属性有对应关系,它的四个值分别对应了positioning
的'bottom-center' | 'top-center' | 'center-right' | 'center-left'
。
JavaScript
const PLACEMENTS = {
TOP: "top",
BOTTOM: "bottom",
LEFT: "left",
RIGHT: "right",
};
const POSITIONINGS = {
TOP: "bottom-center",
BOTTOM: "top-center",
LEFT: "center-right",
RIGHT: "center-left",
};
我还编写了placement
与positioning
相互转换的方法。
JavaScript
/**
* 根据positioning获取placement
* @param {string} positioning - positioning值
* @returns {string} placement值
*/
const toPlacement = positioning => {
switch (positioning) {
case POSITIONINGS.TOP:
return PLACEMENTS.TOP;
case POSITIONINGS.RIGHT:
return PLACEMENTS.RIGHT;
case POSITIONINGS.BOTTOM:
return PLACEMENTS.BOTTOM;
case POSITIONINGS.LEFT:
return PLACEMENTS.LEFT;
default:
return "";
}
};
/**
* 根据placement获取positioning
* @param {string} placement - placement值
* @returns {string} positioning值
*/
const toPositioning = placement => {
switch (placement) {
case PLACEMENTS.TOP:
return POSITIONINGS.TOP;
case PLACEMENTS.RIGHT:
return POSITIONINGS.RIGHT;
case PLACEMENTS.BOTTOM:
return POSITIONINGS.BOTTOM;
case PLACEMENTS.LEFT:
return POSITIONINGS.LEFT;
default:
return "";
}
};
之后为placement
属性编写了读写的方法。
JavaScript
/**
* 设置Tooltip的位置方位
* @param {string} placement - 位置方位,可选值:'top' | 'bottom' | 'left' | 'right'
* @description 设置Tooltip的位置方位
*/
setPlacement(placement = "top") {
this.set("placement", placement);
}
/**
* 获取Tooltip的位置方位
* @returns {string} 当前位置方位
* @description 返回Tooltip的当前位置方位
*/
getPlacement() {
return this.get("placement");
}
2.实现placement与positioning的双向绑定
实现双向绑定的思路就是使用变量设置后的钩子函数,当placement
的值发生变化的时候调用钩子同步修改positioning
的值,反之亦然。
首先在构造函数中添加两个属性的变化事件。
JavaScript
this.on("change:placement", this.handleTooltipPlacementChanged);
this.on("change:positioning", this.handleTooltipPositioningChanged);
在事件处理函数中修改另一个属性的值。
JavaScript
/**
* @protected
*
*/
handleTooltipPlacementChanged() {
const placement = this.getPlacement();
const positioning = toPositioning(placement);
if (positioning) {
this.setPositioning(positioning);
}
}
/**
* @protected
*/
handleTooltipPositioningChanged() {
const positioning = this.getPositioning();
const placement = toPlacement(positioning);
if (placement) {
this.setPlacement(placement);
}
}
3.修改Tooltip的样式(小箭头的位置)
当placement
变化时我还要同步修改小箭头的位置,方法是给element
添加一个样式类来给小箭头设置不同的样式。另外由于我设置的小箭头本质上是一个伪元素,它不会被计算在tooltip的尺寸内,所以必需根据小箭头的尺寸(我这里是10像素)设置一个偏移,才能让小箭头正好指向position
位置。

JavaScript
/**
* @protected
*
*/
handleTooltipPlacementChanged() {
const placement = this.getPlacement();
const positioning = toPositioning(placement);
if (positioning) {
this.setPositioning(positioning);
}
if (this.isUseContainer) {
// 添加样式类
this.getElement().className = `ol-custom-tooltip ${placement}`;
// 设置偏移
switch (placement) {
case PLACEMENTS.TOP:
this.setOffset([0, -10]);
break;
case PLACEMENTS.BOTTOM:
this.setOffset([0, 10]);
break;
case PLACEMENTS.LEFT:
this.setOffset([-10, 0]);
break;
case PLACEMENTS.RIGHT:
this.setOffset([10, 0]);
break;
default:
this.setOffset([0, 0]);
}
}
}
四、实现分组功能
思路还是给Tooltip
增加一个GroupId
属性。
JavaScript
class Tooltip extends Overlay {
constructor(options){
......
/**
* 提示框分组ID
* @type {string}
* @protected
*/
this.groupId = options.groupId !== undefined ? options.groupId : "default";
}
/**
* 获取Tooltip的组ID
* @returns {string} 当前组ID
* @description 返回Tooltip所属的组ID
*/
getGroupId() {
return this.groupId;
}
}
之后可以采用如下的方式获取某一个组别的Overlay
。
JavaScript
const overlays = map
.getOverlays()
.getArray()
.filter(overlay => {
let groupId = overlay.getGroupId ? overlay.getGroupId() : overlay.groupId;
return groupId ? groupId === "tooltip-test" : false;
});
console.log(overlays);
五、实现控制显隐功能
控制显隐的功能也很简单,我的思路是新增一个Property属性visiblity
,当visiblity
的值变化时就去修改element
的 样式属性display
。
JavaScript
class Tooltip extends Overlay {
constructor(options){
......
this.on("change:visibility", this.handleTooltipVisibilityChanged);
// 设置可见性
this.set(
"visibility",
options.visibility !== undefined ? options.visibility : true
);
}
/**
* @protected
*/
handleTooltipVisibilityChanged() {
const element = this.getElement();
if (element) {
element.style.display = this.getVisibility() ? "flex" : "none";
} else {
this.once("change:element", this.handleTooltipVisibilityChanged);
}
}
/**
* 设置Tooltip的可见性
* @param {boolean} visibility - 是否可见
* @description 设置Tooltip的可见性
*/
setVisibility(visibility) {
this.set("visibility", visibility);
}
/**
* 获取Tooltip的可见性
* @returns {boolean} 当前可见性状态
* @description 返回Tooltip的当前可见性状态
*/
getVisibility() {
return this.get("visibility");
}
}
六、实现显示缩放级别限制功能
这个功能在我实现的过程中难度是最大的。当然基本的思路很简单,首先新增两个属性minZoom
和maxZoom
用于设置tooltip的显示范围,然后侦听map
的moveend
事件,若zoom
在设置的范围内则显示tooltip,反之则隐藏tooltip。
但是有两个问题需要解决:
第一,绑定moveend
事件容易,但是想解绑就比较麻烦了。因为我需要在tooltip被添加到地图中时绑定事件,tooltip被从地图中移除时解绑事件,但是既然tooltip都已经被从地图中移除了,又怎么获取到地图对象呢?我最初使用的方法的用一个oldMap
属性将地图对象缓存起来,但是总感觉这种方法不太好,代码质量太低。
第二,与显隐功能的冲突,原本用户只能通过调用tooltip的setVisibility()
方法才能控制显隐。现在根据地图缩放级别的变化也能够自动调整显隐了,因此有的时候就会有冲突。例如,地图的当前的zoom
已经超出了tooltip的显示范围,tooltip被隐藏了,但是用户又执行了tooltip.setVisibility(true)
,结果tooltip又显示了。这种情况就是错误的,正确的应该是只要地图的缩放级别处于规定的范围之外,无论怎样都无法让tooltip显示出来。

1.新增minZoom与maxZoom属性
minZoom
与maxZoom
分别表示Tooltip的最小缩放级别和最大缩放级别,只要在这个范围内Tooltip就可以正常显示(也可以通过setVisibility()
方法控制显隐),超出这个范围Tooltip就会被隐藏(此时setVisibility()
方法应当失效)。
minZoom
的默认值为0
,maxZoom
的默认值为Infinity
。
JavaScript
class Tooltip extends Overlay {
constructor(options){
......
/**
* 最小缩放级别
* @type {number}
* @protected
*/
this.minZoom = options.minZoom !== undefined ? options.minZoom : 0;
/**
* 最大缩放级别
* @type {number}
* @protected
*/
this.maxZoom = options.maxZoom !== undefined ? options.maxZoom : Infinity;
}
/**
* 获取Tooltip的最大缩放级别
* @returns {number} 当前最大缩放级别
* @description 返回Tooltip的最大缩放级别
*/
getMaxZoom() {
return this.maxZoom;
}
/**
* 获取Tooltip的最小缩放级别
* @returns {number} 当前最小缩放级别
* @description 返回Tooltip的最小缩放级别
*/
getMinZoom() {
return this.minZoom;
}
}
2.实现地图的zoom变化时自动调整tooltip显隐
我的实现方式是侦听tooltip的change:map
事件,当为tooltip设置了一个map
时,给map
绑定moveend
事件。
JavaScript
this.on("change:map", this.handleTooltipMapChanged);
为了方便进行事件管理,我使用OpenLayers中两个事件方法listen
和unlistenByKey
。listen
方法用于绑定事件,它会返回一个EventsKey
对象,其中存储了事件的信息(例如事件对象、事件处理函数)。unlistenByKey
方法则会接收一个EventsKey
对象然后移除该事件的绑定。
JavaScript
/**
* Key to use with {@link module:ol/Observable.unByKey}.
* @typedef {Object} EventsKey
* @property {ListenerFunction} listener Listener.
* @property {import("./events/Target.js").EventTargetLike} target Target.
* @property {string} type Type.
* @api
*/
JavaScript
/**
* @protected
*/
handleTooltipMapChanged() {
const map = this.getMap();
// 移除所有事件监听
this.removeAllListeners();
if (map) {
// 监听地图缩放事件(实现对Tooltip的缩放范围控制)
if (this.minZoom > 0 && this.maxZoom < Infinity) {
this.handleMapZoomChange();
this.listenKeys.push(
listen(map, "moveend", this.handleMapZoomChange, this)
);
}
}
}
/**
* @protected
*/
handleMapZoomChange() {
const zoom = this.getMap().getView().getZoom();
const visibility = zoom >= this.minZoom && zoom <= this.maxZoom;
this.isOnZoomRange = visibility;
this.set("visibility", visibility);
}
/**
* 移除所有事件监听
* @description 移除所有事件监听
*/
removeAllListeners() {
this.listenKeys.forEach(key => {
unlistenByKey(key);
});
this.listenKeys = [];
}
3.解决显隐冲突的问题
为了解决"显隐冲突"的问题,我专门设置了一个isOnZoomRange
属性,它用来表示当前地图的zoom
是否在Tooltip的minZoom
与maxZoom
的范围内。
JavaScript
/**
* 是否在缩放范围内
* @type {boolean}
* @private
*/
this.isOnZoomRange = true;
若isZoomOnRange = false
,则setVisibility()
方法将会被禁用。
JavaScript
/**
* 设置Tooltip的可见性
* @param {boolean} visibility - 是否可见
* @description 设置Tooltip的可见性
*/
setVisibility(visibility) {
if (!this.isOnZoomRange) return;
this.set("visibility", visibility);
}
七、实现层级控制功能
基本思路是新增一个zIndex
属性,将.ol-overlay-container
元素的z-index
样式属性设置为zIndex
的值。具体的原理和实现方式可以参考我之前写的这篇文章:
OpenLayers:如何控制Overlay的层级?_openlayer设置overlay在最上面-CSDN博客
1.新增zIndex属性
zIndex
属性用来设置tooltip在z方向上的层级,我设置zIndex
的取值范围在0
到9999
之间。
JavaScript
const MIN_Z_INDEX = 0;
const MAX_Z_INDEX = 9999;
JavaScript
/**
* 设置Tooltip的z-index
* @param {number} zIndex - z-index值
* @description 设置Tooltip的z-index,同时更新DOM元素的z-index
*/
setZIndex(zIndex) {
let _zIndex = zIndex;
if (zIndex < MIN_Z_INDEX) {
_zIndex = MIN_Z_INDEX;
} else if (zIndex > MAX_Z_INDEX) {
_zIndex = MAX_Z_INDEX;
}
this.set("zIndex", _zIndex);
}
/**
* 获取Tooltip的z-index
* @returns {number} 当前z-index值
* @description 返回Tooltip的当前z-index值
*/
getZIndex() {
return this.get("zIndex");
}
2.为.ol-overlay-container元素设置层级
JavaScript
class Tooltip extends Overlay {
constructor(options){
......
}
/**
* 获取Tooltip的最大缩放级别
* @returns {number} 当前最大缩放级别
* @description 返回Tooltip的最大缩放级别
*/
getMaxZoom() {
return this.maxZoom;
}
/**
* 获取Tooltip的最小缩放级别
* @returns {number} 当前最小缩放级别
* @description 返回Tooltip的最小缩放级别
*/
getMinZoom() {
return this.minZoom;
}
}
完整代码
JavaScript
import Overlay from "ol/Overlay";
import { listen, unlistenByKey } from "ol/events";
import "./Tooltip.css";
const MIN_Z_INDEX = 0;
const MAX_Z_INDEX = 9999;
const PLACEMENTS = {
TOP: "top",
BOTTOM: "bottom",
LEFT: "left",
RIGHT: "right",
};
const POSITIONINGS = {
TOP: "bottom-center",
BOTTOM: "top-center",
LEFT: "center-right",
RIGHT: "center-left",
};
const PLACEMENT_MAP_POSITIONING = [
[PLACEMENTS.TOP, POSITIONINGS.TOP],
[PLACEMENTS.RIGHT, POSITIONINGS.RIGHT],
[PLACEMENTS.BOTTOM, POSITIONINGS.BOTTOM],
[PLACEMENTS.LEFT, POSITIONINGS.LEFT],
];
/**
* 根据positioning获取placement
* @param {string} positioning - positioning值
* @returns {string} placement值
*/
const toPlacement = positioning => {
switch (positioning) {
case POSITIONINGS.TOP:
return PLACEMENTS.TOP;
case POSITIONINGS.RIGHT:
return PLACEMENTS.RIGHT;
case POSITIONINGS.BOTTOM:
return PLACEMENTS.BOTTOM;
case POSITIONINGS.LEFT:
return PLACEMENTS.LEFT;
default:
return "";
}
};
/**
* 根据placement获取positioning
* @param {string} placement - placement值
* @returns {string} positioning值
*/
const toPositioning = placement => {
switch (placement) {
case PLACEMENTS.TOP:
return POSITIONINGS.TOP;
case PLACEMENTS.RIGHT:
return POSITIONINGS.RIGHT;
case PLACEMENTS.BOTTOM:
return POSITIONINGS.BOTTOM;
case PLACEMENTS.LEFT:
return POSITIONINGS.LEFT;
default:
return "";
}
};
/**
* @class Tooltip
* @extends {Overlay}
* @classdesc 自定义的 OpenLayers 提示框覆盖物类,继承自 ol/Overlay
*
* @param {Object} options - 配置选项
* @param {string} [options.groupId='default'] - 提示框分组ID
* @param {boolean} [options.visibility=true] - 是否默认可见
* @param {string} [options.placement='top'] - 提示框位置,可选值:'top' | 'bottom' | 'left' | 'right'
* @param {HTMLElement|string} options.element - 提示框内容元素或HTML字符串
* @param {number} [options.maxZoom=Infinity] - 最大显示缩放级别
* @param {number} [options.minZoom=0] - 最小显示缩放级别
* @param {boolean} [options.isUseContainer=true] - 是否使用自定义的提示框容器
* @param {number} [options.zIndex=0] - 提示框的z-index
*
* @example
* // 创建一个顶部显示的提示框
* const tooltip = new Tooltip({
* element: '<div>提示内容</div>',
* placement: 'top'
* });
*
* @property {boolean} isUseContainer - 是否使用自定义的Tooltip容器
* @property {number} minZoom - 最小缩放级别
* @property {number} maxZoom - 最大缩放级别
* @property {Array<number>} listenKeys - 事件监听器数组
* @property {boolean} isOnZoomRange - 是否在缩放范围内
*/
class Tooltip extends Overlay {
constructor(options) {
const { element, ...rest } = options;
super(rest);
/**
* 提示框分组ID
* @type {string}
* @protected
*/
this.groupId = options.groupId !== undefined ? options.groupId : "default";
/**
* 是否使用自定义的Tooltip容器
* @type {boolean}
* @protected
*/
this.isUseContainer =
options.isUseContainer !== undefined ? options.isUseContainer : true;
/**
* 最小缩放级别
* @type {number}
* @protected
*/
this.minZoom = options.minZoom !== undefined ? options.minZoom : 0;
/**
* 最大缩放级别
* @type {number}
* @protected
*/
this.maxZoom = options.maxZoom !== undefined ? options.maxZoom : Infinity;
/**
* 事件监听器
* @type {Array}
* @private
*/
this.listenKeys = [];
/**
* 是否在缩放范围内
* @type {boolean}
* @private
*/
this.isOnZoomRange = true;
// 注册 `change:visibility` 事件侦听器
this.on("change:visibility", this.handleTooltipVisibilityChanged);
this.on("change:placement", this.handleTooltipPlacementChanged);
this.on("change:positioning", this.handleTooltipPositioningChanged);
this.on("change:map", this.handleTooltipMapChanged);
this.on("change:zIndex", this.handleTooltipZIndexChanged);
// 设置元素
this.setElement(options.element);
// 设置可见性
this.set(
"visibility",
options.visibility !== undefined ? options.visibility : true
);
// 设置位置方位
this.set(
"placement",
options.placement !== undefined ? options.placement : "top"
);
// 设置z-index
this.setZIndex(options.zIndex !== undefined ? options.zIndex : MIN_Z_INDEX);
}
/**
* @protected
*/
handleTooltipVisibilityChanged() {
const element = this.getElement();
if (element) {
element.style.display = this.getVisibility() ? "flex" : "none";
} else {
this.once("change:element", this.handleTooltipVisibilityChanged);
}
}
/**
* @protected
*
*/
handleTooltipPlacementChanged() {
const placement = this.getPlacement();
const positioning = toPositioning(placement);
if (positioning) {
this.setPositioning(positioning);
}
if (this.isUseContainer) {
this.getElement().className = `ol-custom-tooltip ${placement}`;
// 设置偏移
switch (placement) {
case PLACEMENTS.TOP:
this.setOffset([0, -10]);
break;
case PLACEMENTS.BOTTOM:
this.setOffset([0, 10]);
break;
case PLACEMENTS.LEFT:
this.setOffset([-10, 0]);
break;
case PLACEMENTS.RIGHT:
this.setOffset([10, 0]);
break;
default:
this.setOffset([0, 0]);
}
}
}
/**
* @protected
*/
handleTooltipPositioningChanged() {
const positioning = this.getPositioning();
const placement = toPlacement(positioning);
if (placement) {
this.setPlacement(placement);
}
}
/**
* @protected
*/
handleTooltipMapChanged() {
const map = this.getMap();
// 移除所有事件监听
this.removeAllListeners();
if (map) {
// 监听地图缩放事件(实现对Tooltip的缩放范围控制)
if (this.minZoom > 0 && this.maxZoom < Infinity) {
this.handleMapZoomChange();
this.listenKeys.push(
listen(map, "moveend", this.handleMapZoomChange, this)
);
}
}
}
/**
* @protected
*/
handleMapZoomChange() {
const zoom = this.getMap().getView().getZoom();
const visibility = zoom >= this.minZoom && zoom <= this.maxZoom;
this.isOnZoomRange = visibility;
this.set("visibility", visibility);
}
/**
* @protected
*/
handleTooltipZIndexChanged() {
const element = this.getElement();
const zIndex = this.getZIndex();
if (element) {
const parentElement = element.parentElement;
parentElement.style.zIndex = zIndex;
parentElement.addEventListener("mouseenter", function () {
this.style.zIndex = MAX_Z_INDEX;
});
parentElement.addEventListener("mouseleave", function () {
this.style.zIndex = zIndex;
});
} else {
this.once("change:element", this.handleTooltipZIndexChanged);
}
}
/**
*
* @override
*/
setElement(element) {
let _element = this.formatElement(element);
super.setElement(_element);
}
/**
* 格式化元素
* @param {HTMLElement | string} element - 元素
* @returns {HTMLElement} 格式化后的元素
*
*/
formatElement(element) {
let _element = element;
// DOMString 转换为 HTMLElement
if (typeof element === "string") {
const div = document.createElement("div");
div.innerHTML = element;
_element = div.firstElementChild;
}
// 使用自定义的模版
if (this.isUseContainer) {
const container = document.createElement("div");
container.className = `ol-custom-tooltip ${this.getPlacement()}`;
container.appendChild(_element);
_element = container;
}
return _element;
}
/**
* 获取Tooltip的组ID
* @returns {string} 当前组ID
* @description 返回Tooltip所属的组ID
*/
getGroupId() {
return this.groupId;
}
/**
* 获取Tooltip的最大缩放级别
* @returns {number} 当前最大缩放级别
* @description 返回Tooltip的最大缩放级别
*/
getMaxZoom() {
return this.maxZoom;
}
/**
* 获取Tooltip的最小缩放级别
* @returns {number} 当前最小缩放级别
* @description 返回Tooltip的最小缩放级别
*/
getMinZoom() {
return this.minZoom;
}
/**
* 设置Tooltip的可见性
* @param {boolean} visibility - 是否可见
* @description 设置Tooltip的可见性
*/
setVisibility(visibility) {
if (!this.isOnZoomRange) return;
this.set("visibility", visibility);
}
/**
* 获取Tooltip的可见性
* @returns {boolean} 当前可见性状态
* @description 返回Tooltip的当前可见性状态
*/
getVisibility() {
return this.get("visibility");
}
/**
* 设置Tooltip的位置方位
* @param {string} placement - 位置方位,可选值:'top' | 'bottom' | 'left' | 'right'
* @description 设置Tooltip的位置方位
*/
setPlacement(placement = "top") {
this.set("placement", placement);
}
/**
* 获取Tooltip的位置方位
* @returns {string} 当前位置方位
* @description 返回Tooltip的当前位置方位
*/
getPlacement() {
return this.get("placement");
}
/**
* 设置Tooltip的z-index
* @param {number} zIndex - z-index值
* @description 设置Tooltip的z-index,同时更新DOM元素的z-index
*/
setZIndex(zIndex) {
let _zIndex = zIndex;
if (zIndex < MIN_Z_INDEX) {
_zIndex = MIN_Z_INDEX;
} else if (zIndex > MAX_Z_INDEX) {
_zIndex = MAX_Z_INDEX;
}
this.set("zIndex", _zIndex);
}
/**
* 获取Tooltip的z-index
* @returns {number} 当前z-index值
* @description 返回Tooltip的当前z-index值
*/
getZIndex() {
return this.get("zIndex");
}
/**
* 移除所有事件监听
* @description 移除所有事件监听
*/
removeAllListeners() {
this.listenKeys.forEach(key => {
unlistenByKey(key);
});
this.listenKeys = [];
}
/**
* 销毁Tooltip实例
* @description 清理所有资源,包括事件监听器、DOM元素和属性
*/
dispose() {
const map = this.getMap();
map && map.removeOverlay(this);
// 移除所有事件监听
this.removeAllListeners();
// 移除Tooltip自身的事件监听
// 清理DOM元素
const element = this.getElement();
if (element && element.parentNode) {
element.parentNode.removeChild(element);
}
// 清理属性
this.set("groupId", null);
this.set("placement", null);
this.set("visibility", null);
// 检查父类是否有dispose方法
if (typeof super.dispose === "function") {
super.dispose();
}
}
}
export {
PLACEMENTS,
POSITIONINGS,
PLACEMENT_MAP_POSITIONING,
toPlacement,
toPositioning,
};
export default Tooltip;