Opnelayers:封装Popup

一、概述

我之前在文章 OpenLayers:封装Tooltip-CSDN博客 中封装了一个Overlay的子类Tooltip,实现了一些原生的Overlay所不具备的功能。现在我更进一步封装一个Tooltip的子类Popup。所以请先阅读我的上一篇文章。

在我的设计中Popup完全继承Tooltip原有的功能。它新增的功能只有一个,那就是Popup默认隐藏,并会与一个目标相关联,这个目标可以是一个要素或是一个坐标位置,只有鼠标点击或悬停到关联目标上,Popup才会显示。

二、实现弹出功能

我的基本思路就是,首先增加两个属性targettriggertarget决定popup所绑定的目标是什么,trigger则表示触发提示框弹出的方式。然后去侦听地图对应的事件(鼠标点击或者鼠标移动事件),在事件处理函数中去判断目标是否被点击到了(或是否悬停在其上方),若被点击到则显示提示框,没被点击到就隐藏提示框。

1.新增target和trigger属性

target属性表示popup所关联的对象,在我的设想中target可以支持四种类型:

  • Featrue,一个要素,例如new Feature(new Point(...))
  • Geomtry,一个几何图形,例如 PointLineStringPloygon等对象。
  • 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来实现targetExtent的功能。但是遇到一个问题,当我将一个PointCooridnate转换为一个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;
相关推荐
coding随想2 小时前
JavaScript ES6 解构:优雅提取数据的艺术
前端·javascript·es6
年老体衰按不动键盘2 小时前
快速部署和启动Vue3项目
java·javascript·vue
小小小小宇2 小时前
一个小小的柯里化函数
前端
灵感__idea2 小时前
JavaScript高级程序设计(第5版):无处不在的集合
前端·javascript·程序员
小小小小宇2 小时前
前端双Token机制无感刷新
前端
小小小小宇2 小时前
重提React闭包陷阱
前端
小小小小宇2 小时前
前端XSS和CSRF以及CSP
前端
UFIT2 小时前
NoSQL之redis哨兵
java·前端·算法
超级土豆粉2 小时前
CSS3 的特性
前端·css·css3
星辰引路-Lefan2 小时前
深入理解React Hooks的原理与实践
前端·javascript·react.js