OverviewMap鹰眼控件源码分析
# 概述
本文主要介绍 Openlayers 中提供的最后一个控件,鹰眼控件OverviewMap
的源码实现和核心原理.鹰眼控件简单来说就是提供一个小窗口视图,可以实时反应当前地图在整个地图的地理位置,可以理解成整体与局部的关系.
# 源码分析
OverviewMap
类继承于Control
类.关于Control
类,可以参考Control 基类介绍 (opens new window).
# 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_();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
# 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
的实现原理和主要方法的讲解。