源码分析之Openlayers中OverviewMap鹰眼控件

发布于:2024-12-21 ⋅ 阅读:(11) ⋅ 点赞:(0)

概述

本文主要介绍 Openlayers 中提供的最后一个控件,鹰眼控件OverviewMap的源码实现和核心原理.鹰眼控件简单来说就是提供一个小窗口视图,可以实时反应当前地图在整个地图的地理位置,可以理解成整体局部的关系.

源码分析

OverviewMap类继承于Control类.关于Control类,可以参考源码分析之Openlayers中的控件篇Control基类介绍
.

OverviewMap类源码

OverviewMap类实现如下:

class OverviewMap extends Control {
  constructor(options) {
    options = options ? options : {};

    super({
      element: document.createElement("div"),
      render: options.render,
      target: options.target,
    });

    this.boundHandleRotationChanged_ = this.handleRotationChanged_.bind(this);

    this.collapsed_ =
      options.collapsed !== undefined ? options.collapsed : true;

    this.collapsible_ =
      options.collapsible !== undefined ? options.collapsible : true;

    if (!this.collapsible_) {
      this.collapsed_ = false;
    }

    this.rotateWithView_ =
      options.rotateWithView !== undefined ? options.rotateWithView : false;
    this.viewExtent_ = undefined;

    const className =
      options.className !== undefined ? options.className : "ol-overviewmap";

    const tipLabel =
      options.tipLabel !== undefined ? options.tipLabel : "Overview map";

    const collapseLabel =
      options.collapseLabel !== undefined ? options.collapseLabel : "\u2039";

    if (typeof collapseLabel === "string") {
      this.collapseLabel_ = document.createElement("span");
      this.collapseLabel_.textContent = collapseLabel;
    } else {
      this.collapseLabel_ = collapseLabel;
    }

    const label = options.label !== undefined ? options.label : "\u203A";

    if (typeof label === "string") {
      this.label_ = document.createElement("span");
      this.label_.textContent = label;
    } else {
      this.label_ = label;
    }

    const activeLabel =
      this.collapsible_ && !this.collapsed_ ? this.collapseLabel_ : this.label_;
    const button = document.createElement("button");
    button.setAttribute("type", "button");
    button.title = tipLabel;
    button.appendChild(activeLabel);

    button.addEventListener(
      EventType.CLICK,
      this.handleClick_.bind(this),
      false
    );

    this.ovmapDiv_ = document.createElement("div");
    this.ovmapDiv_.className = "ol-overviewmap-map";

    this.view_ = options.view;

    const ovmap = new Map({
      view: options.view,
      controls: new Collection(),
      interactions: new Collection(),
    });

    this.ovmap_ = ovmap;

    if (options.layers) {
      options.layers.forEach(function (layer) {
        ovmap.addLayer(layer);
      });
    }

    const box = document.createElement("div");
    box.className = "ol-overviewmap-box";
    box.style.boxSizing = "border-box";

    /**
     * @type {import("../Overlay.js").default}
     * @private
     */
    this.boxOverlay_ = new Overlay({
      position: [0, 0],
      positioning: "center-center",
      element: box,
    });
    this.ovmap_.addOverlay(this.boxOverlay_);

    const cssClasses =
      className +
      " " +
      CLASS_UNSELECTABLE +
      " " +
      CLASS_CONTROL +
      (this.collapsed_ && this.collapsible_ ? " " + CLASS_COLLAPSED : "") +
      (this.collapsible_ ? "" : " ol-uncollapsible");
    const element = this.element;
    element.className = cssClasses;
    element.appendChild(this.ovmapDiv_);
    element.appendChild(button);

    /* Interactive map */

    const scope = this;

    const overlay = this.boxOverlay_;
    const overlayBox = this.boxOverlay_.getElement();

    /* Functions definition */

    const computeDesiredMousePosition = function (mousePosition) {
      return {
        clientX: mousePosition.clientX,
        clientY: mousePosition.clientY,
      };
    };

    const move = function (event) {
      const position = /** @type {?} */ (computeDesiredMousePosition(event));
      const coordinates = ovmap.getEventCoordinate(
        /** @type {MouseEvent} */ (position)
      );

      overlay.setPosition(coordinates);
    };

    const endMoving = function (event) {
      const coordinates = ovmap.getEventCoordinateInternal(event);

      scope.getMap().getView().setCenterInternal(coordinates);

      window.removeEventListener("pointermove", move);
      window.removeEventListener("pointerup", endMoving);
    };

    /* Binding */

    this.ovmapDiv_.addEventListener("pointerdown", function () {
      if (event.target === overlayBox) {
        window.addEventListener("pointermove", move);
      }
      window.addEventListener("pointerup", endMoving);
    });
  }

  setMap(map) {
    const oldMap = this.getMap();
    if (map === oldMap) {
      return;
    }
    if (oldMap) {
      const oldView = oldMap.getView();
      if (oldView) {
        this.unbindView_(oldView);
      }
      this.ovmap_.setTarget(null);
    }
    super.setMap(map);

    if (map) {
      this.ovmap_.setTarget(this.ovmapDiv_);
      this.listenerKeys.push(
        listen(
          map,
          ObjectEventType.PROPERTYCHANGE,
          this.handleMapPropertyChange_,
          this
        )
      );

      const view = map.getView();
      if (view) {
        this.bindView_(view);
      }

      if (!this.ovmap_.isRendered()) {
        this.updateBoxAfterOvmapIsRendered_();
      }
    }
  }

  handleMapPropertyChange_(event) {
    if (event.key === MapProperty.VIEW) {
      const oldView = /** @type {import("../View.js").default} */ (
        event.oldValue
      );
      if (oldView) {
        this.unbindView_(oldView);
      }
      const newView = this.getMap().getView();
      this.bindView_(newView);
    } else if (
      !this.ovmap_.isRendered() &&
      (event.key === MapProperty.TARGET || event.key === MapProperty.SIZE)
    ) {
      this.ovmap_.updateSize();
    }
  }

  bindView_(view) {
    if (!this.view_) {
      // Unless an explicit view definition was given, derive default from whatever main map uses.
      const newView = new View({
        projection: view.getProjection(),
      });
      this.ovmap_.setView(newView);
    }

    view.addChangeListener(
      ViewProperty.ROTATION,
      this.boundHandleRotationChanged_
    );
    // Sync once with the new view
    this.handleRotationChanged_();

    if (view.isDef()) {
      this.ovmap_.updateSize();
      this.resetExtent_();
    }
  }

  unbindView_(view) {
    view.removeChangeListener(
      ViewProperty.ROTATION,
      this.boundHandleRotationChanged_
    );
  }

  handleRotationChanged_() {
    if (this.rotateWithView_) {
      this.ovmap_.getView().setRotation(this.getMap().getView().getRotation());
    }
  }

  validateExtent_() {
    const map = this.getMap();
    const ovmap = this.ovmap_;

    if (!map.isRendered() || !ovmap.isRendered()) {
      return;
    }

    const mapSize = /** @type {import("../size.js").Size} */ (map.getSize());

    const view = map.getView();
    const extent = view.calculateExtentInternal(mapSize);

    if (this.viewExtent_ && equalsExtent(extent, this.viewExtent_)) {
      // repeats of the same extent may indicate constraint conflicts leading to an endless cycle
      return;
    }
    this.viewExtent_ = extent;

    const ovmapSize = /** @type {import("../size.js").Size} */ (
      ovmap.getSize()
    );

    const ovview = ovmap.getView();
    const ovextent = ovview.calculateExtentInternal(ovmapSize);

    const topLeftPixel = ovmap.getPixelFromCoordinateInternal(
      getTopLeft(extent)
    );
    const bottomRightPixel = ovmap.getPixelFromCoordinateInternal(
      getBottomRight(extent)
    );

    const boxWidth = Math.abs(topLeftPixel[0] - bottomRightPixel[0]);
    const boxHeight = Math.abs(topLeftPixel[1] - bottomRightPixel[1]);

    const ovmapWidth = ovmapSize[0];
    const ovmapHeight = ovmapSize[1];

    if (
      boxWidth < ovmapWidth * MIN_RATIO ||
      boxHeight < ovmapHeight * MIN_RATIO ||
      boxWidth > ovmapWidth * MAX_RATIO ||
      boxHeight > ovmapHeight * MAX_RATIO
    ) {
      this.resetExtent_();
    } else if (!containsExtent(ovextent, extent)) {
      this.recenter_();
    }
  }

  resetExtent_() {
    if (MAX_RATIO === 0 || MIN_RATIO === 0) {
      return;
    }

    const map = this.getMap();
    const ovmap = this.ovmap_;

    const mapSize = /** @type {import("../size.js").Size} */ (map.getSize());

    const view = map.getView();
    const extent = view.calculateExtentInternal(mapSize);

    const ovview = ovmap.getView();

    // get how many times the current map overview could hold different
    // box sizes using the min and max ratio, pick the step in the middle used
    // to calculate the extent from the main map to set it to the overview map,
    const steps = Math.log(MAX_RATIO / MIN_RATIO) / Math.LN2;
    const ratio = 1 / (Math.pow(2, steps / 2) * MIN_RATIO);
    scaleFromCenter(extent, ratio);
    ovview.fitInternal(polygonFromExtent(extent));
  }

  recenter_() {
    const map = this.getMap();
    const ovmap = this.ovmap_;

    const view = map.getView();

    const ovview = ovmap.getView();

    ovview.setCenterInternal(view.getCenterInternal());
  }

  updateBox_() {
    const map = this.getMap();
    const ovmap = this.ovmap_;

    if (!map.isRendered() || !ovmap.isRendered()) {
      return;
    }

    const mapSize = /** @type {import("../size.js").Size} */ (map.getSize());

    const view = map.getView();

    const ovview = ovmap.getView();

    const rotation = this.rotateWithView_ ? 0 : -view.getRotation();

    const overlay = this.boxOverlay_;
    const box = this.boxOverlay_.getElement();
    const center = view.getCenter();
    const resolution = view.getResolution();
    const ovresolution = ovview.getResolution();
    const width = (mapSize[0] * resolution) / ovresolution;
    const height = (mapSize[1] * resolution) / ovresolution;

    // set position using center coordinates
    overlay.setPosition(center);

    // set box size calculated from map extent size and overview map resolution
    if (box) {
      box.style.width = width + "px";
      box.style.height = height + "px";
      const transform = "rotate(" + rotation + "rad)";
      box.style.transform = transform;
    }
  }

  updateBoxAfterOvmapIsRendered_() {
    if (this.ovmapPostrenderKey_) {
      return;
    }
    this.ovmapPostrenderKey_ = listenOnce(
      this.ovmap_,
      MapEventType.POSTRENDER,
      (event) => {
        delete this.ovmapPostrenderKey_;
        this.updateBox_();
      }
    );
  }

  handleClick_(event) {
    event.preventDefault();
    this.handleToggle_();
  }

  handleToggle_() {
    this.element.classList.toggle(CLASS_COLLAPSED);
    if (this.collapsed_) {
      replaceNode(this.collapseLabel_, this.label_);
    } else {
      replaceNode(this.label_, this.collapseLabel_);
    }
    this.collapsed_ = !this.collapsed_;

    // manage overview map if it had not been rendered before and control
    // is expanded
    const ovmap = this.ovmap_;
    if (!this.collapsed_) {
      if (ovmap.isRendered()) {
        this.viewExtent_ = undefined;
        ovmap.render();
        return;
      }
      ovmap.updateSize();
      this.resetExtent_();
      this.updateBoxAfterOvmapIsRendered_();
    }
  }

  getCollapsible() {
    return this.collapsible_;
  }

  setCollapsible(collapsible) {
    if (this.collapsible_ === collapsible) {
      return;
    }
    this.collapsible_ = collapsible;
    this.element.classList.toggle("ol-uncollapsible");
    if (!collapsible && this.collapsed_) {
      this.handleToggle_();
    }
  }

  setCollapsed(collapsed) {
    if (!this.collapsible_ || this.collapsed_ === collapsed) {
      return;
    }
    this.handleToggle_();
  }

  getCollapsed() {
    return this.collapsed_;
  }

  getRotateWithView() {
    return this.rotateWithView_;
  }

  setRotateWithView(rotateWithView) {
    if (this.rotateWithView_ === rotateWithView) {
      return;
    }
    this.rotateWithView_ = rotateWithView;
    if (this.getMap().getView().getRotation() !== 0) {
      if (this.rotateWithView_) {
        this.handleRotationChanged_();
      } else {
        this.ovmap_.getView().setRotation(0);
      }
      this.viewExtent_ = undefined;
      this.validateExtent_();
      this.updateBox_();
    }
  }

  getOverviewMap() {
    return this.ovmap_;
  }

  render(mapEvent) {
    this.validateExtent_();
    this.updateBox_();
  }
}

OverviewMap类构造函数

OverviewMap类构造函数接受一个参数对象options,默认为空对象{};调用super方法,创建鹰眼控件容器;绑定方法handleRotationChanged_this指向,初始化折叠属性,和Attributions属性控件类似,默认情况下,鹰眼控件是折叠状态,点击可以展开.初始化this.rotateWithView_变量,默认为false,表示鹰眼地图视图的旋转是否与地图主视图同步;初始化鹰眼控件的类名和标签,创建控件元素等;然后是监听控件按钮的点击事件handleClick_;创建鹰眼地图的容器ol-overviewmap-map,调用Map类实例化ovmap,ovmapview参数是options.view传递过来的;判断,若options.layers存在,则遍历它将图层添加到ovmap中;然后创建一个Overlaythis.boxOverlay_添加到ovmap中,this.boxOverlay_是一个矩形框,用来模拟地图主视图区域,然后监听ovmap的的容器pointerdown类型的事件,主要是pointermove方法,即在鹰眼控件视图内移动鼠标可以控制矩形框boxOverlay_的位置;还有会监听pointerup事件,当鼠标抬起时调用endMoving方法,此时会获取鼠标在ovmap中的坐标,然后调用this.getMap()即获得地图主视图,设置它的中心点为鼠标抬起的坐标位置,最后移除pointermovepointerup事件监听.

OverviewMap类主要方法

OverviewMap类主要有以下方法:

  • setMap方法

setMap方法被调用时,会先获取地图主视图,判断参数map是否就是地图主视图,若二者一致,则return;判断,若地图主视图存在,则调用this.unbindView_进行解绑,并且鹰眼控件的目标元素置空;然后调用父类的setMap方法;判断,若参数map存在,则设置鹰眼控件的目标元素,并且注册地图主视图propertychange类型的监听,事件为this.handleMapPropertyChange_;判断,若地图主视图存在,则调用this.bindView_进行绑定;最后判断,若鹰眼地图视图没有渲染,则调用this.updateBoxAfterOvmapIsRendered_方法,更新鹰眼控件中的矩形overlay

  • handleMapPropertyChange_方法

当地图的属性发生改变时,会调用handleMapPropertyChange_方法;该方法内部会先判断参数eventkey是否为view,即是否是视图发生变化,若是,则需要解绑旧视图,绑定新视图;若不是,则判断,若鹰眼地图没有渲染完成并且event.keytarget或者size,则更新鹰眼控件的地图大小。

  • bindView_方法

bindView_方法首先会判断,若this.view_不存在,则实例化一个View类,投影为参数view的投影,然后设置鹰眼地图的视图;然后注册视图的rotation类型的监听事件this.boundHandleRotationChanged_,然后调用this.boundHandleRotationChanged_执行一次;最后判断,若参数view没有中心点也没有分辨率,则重置鹰眼地图的大小以及调用resetExtent_重置鹰眼地图视图为地图主视图最大值和最小值比例的一半。

  • unbindView_方法

unbindView_方法就是移除参数view上的rotation的监听事件this.boundHandleRotationChanged_

  • handleRotationChanged_方法

handleRotationChanged_方法内部会判断,若this.rotateWithView_true,则根据地图主视图的旋转角度同步设置鹰眼地图的旋转角度,意思就是同步两个地图的旋转角度,但是this.rotateWithView_默认为false,若需要同步地图,则需要传参数。

  • validateExtent_方法

validateExtent_方法内部就是用来计算两个地图的范围,然后决定是调用this.resetExtent_还是this.recenter_方法

  • resetExtent_方法

resetExtent_方法用于重置鹰眼地图范围

  • recenter_方法

recenter_方法首先会获取地图主视图的中心点,然后将其设置为鹰眼地图的中心点

  • updateBox_方法

updateBox_用于设置鹰眼控件中的overlay,前面提过该overlay矩形表示当前地图主视图在地图最大范围中的相对大小;该方法内部会先获取地图主视图的中心点、分辨率,然后进行一系列的换算,然后更新鹰眼控件中的矩形大小和位置。

  • updateBoxAfterOvmapIsRendered_方法

updateBoxAfterOvmapIsRendered_方法就是在鹰眼地图渲染完成后,调用this.updateBox_更新overlay;这里设计得很巧妙,只监听一次;

  • handleClick_方法

handleClick_方法内部就是调用handleToggle_

  • handleToggle_方法

handleToggle_方法就是用来显示或隐藏鹰眼地图

  • getCollapsible方法

getCollapsible方法获取鹰眼控件地图是否有可以进行折叠的能力

  • setCollapsible方法

setCollapsible方法就是设置鹰眼控件的折叠属性

  • setCollapsed方法

setCollapsed方法就是设置显示或隐藏鹰眼控件的地图,内部是调用this.handleToggle_方法

  • getCollapsed方法

getCollapsed方法用于获取鹰眼控件地图的折叠状态

  • getRotateWithView方法

getRotateWithView方法获取变量this.rotateWithView_

  • setRotateWithView方法

setRotateWithView方法用于设置this.rotateWithView_变量,调用该方法后,若地图主视图的旋转角度不为0,则判断,若this.rotateWithView_(等同于参数rotateWithView)为true,则调用this.handleRotationChanged_进行旋转鹰眼地图;否则将鹰眼地图旋转角度设置为0;最后调用validateExtent_updateBox_方法。

  • getOverviewMap方法

getOverviewMap方法用于获取鹰眼地图的实例。

  • render方法
    在鹰眼控件进行setMap后会调用,内部就是执行this.validateExtent_updateBox_方法。

总结

本文主要介绍了鹰眼控件OverviewMap的实现原理和主要方法的讲解。