概述
本文主要介绍 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 viewthis.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 cyclereturn;}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 coordinatesoverlay.setPosition(center);// set box size calculated from map extent size and overview map resolutionif (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 expandedconst 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
,ovmap
的view
参数是options.view
传递过来的;判断,若options.layers
存在,则遍历它将图层添加到ovmap
中;然后创建一个Overlay
即this.boxOverlay_
添加到ovmap
中,this.boxOverlay_
是一个矩形框,用来模拟地图主视图区域,然后监听ovmap
的的容器pointerdown
类型的事件,主要是pointermove
方法,即在鹰眼控件视图内移动鼠标可以控制矩形框boxOverlay_
的位置;还有会监听pointerup
事件,当鼠标抬起时调用endMoving
方法,此时会获取鼠标在ovmap
中的坐标,然后调用this.getMap()
即获得地图主视图,设置它的中心点为鼠标抬起的坐标位置,最后移除pointermove
和pointerup
事件监听.
OverviewMap
类主要方法
OverviewMap
类主要有以下方法:
setMap
方法
setMap
方法被调用时,会先获取地图主视图,判断参数map
是否就是地图主视图,若二者一致,则return
;判断,若地图主视图存在,则调用this.unbindView_
进行解绑,并且鹰眼控件的目标元素置空;然后调用父类的setMap
方法;判断,若参数map
存在,则设置鹰眼控件的目标元素,并且注册地图主视图propertychange
类型的监听,事件为this.handleMapPropertyChange_
;判断,若地图主视图存在,则调用this.bindView_
进行绑定;最后判断,若鹰眼地图视图没有渲染,则调用this.updateBoxAfterOvmapIsRendered_
方法,更新鹰眼控件中的矩形overlay
handleMapPropertyChange_
方法
当地图的属性发生改变时,会调用handleMapPropertyChange_
方法;该方法内部会先判断参数event
的key
是否为view
,即是否是视图发生变化,若是,则需要解绑旧视图,绑定新视图;若不是,则判断,若鹰眼地图没有渲染完成并且event.key
是target
或者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
的实现原理和主要方法的讲解。