概述
本文主要介绍 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
,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
的实现原理和主要方法的讲解。