Jinuss's blog Jinuss's blog
首页
  • 前端文章

    • JavaScript
  • 学习笔记

    • 《JavaScript高级程序设计》
    • 《Vue》
    • 《React》
    • 《Git》
    • JS设计模式总结
  • HTML
  • CSS
  • 技术文档
  • GitHub技巧
  • 学习
  • 实用技巧
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

东流

前端可视化
首页
  • 前端文章

    • JavaScript
  • 学习笔记

    • 《JavaScript高级程序设计》
    • 《Vue》
    • 《React》
    • 《Git》
    • JS设计模式总结
  • HTML
  • CSS
  • 技术文档
  • GitHub技巧
  • 学习
  • 实用技巧
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 核心基类

  • Control控件篇

    • Control基类介绍
    • 默认Controls控件渲染过程
    • Zoom缩放控件源码分析
    • Rotate旋转控件源码分析
    • ZoomToExtent控件源码分析
    • ZoomSlider滑块缩放控件源码分析
    • ScaleLine比例尺控件源码分析
    • Attribution属性控件源码分析
    • FullScreen全屏控件源码分析
    • OverviewMap鹰眼控件源码分析
      • 概述
      • 源码分析
        • OverviewMap类源码
        • OverviewMap类构造函数
        • OverviewMap类主要方法
      • 总结
    • MousePosition鼠标位置控件源码分析
  • Geom几何图形篇

  • Layer图层篇

  • Renderer篇

  • Feature篇

  • style样式篇

  • 《Openlayers源码》笔记
  • Control控件篇
东流
2024-12-11
目录

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_();
  }
}
1
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的实现原理和主要方法的讲解。

编辑 (opens new window)
上次更新: 2024/12/20, 13:38:48
FullScreen全屏控件源码分析
MousePosition鼠标位置控件源码分析

← FullScreen全屏控件源码分析 MousePosition鼠标位置控件源码分析→

最近更新
01
GeoJSON
05-08
02
Circle
04-15
03
CircleMarker
04-15
更多文章>
Theme by Vdoing | Copyright © 2024-2025 东流 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式