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比例尺控件源码分析
      • 概述
      • 源码分析
        • ScaleLine控件构造函数
        • ScaleLine控件主要方法
      • 总结
    • Attribution属性控件源码分析
    • FullScreen全屏控件源码分析
    • OverviewMap鹰眼控件源码分析
    • MousePosition鼠标位置控件源码分析
  • Geom几何图形篇

  • Layer图层篇

  • Renderer篇

  • Feature篇

  • style样式篇

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

ScaleLine比例尺控件源码分析

# 概述

ScaleLine比例尺控件也是地图最基本的控件之一, Openlayers 中的比例尺是在ScaleLine类中实现的。比例尺时理解地图、进行空间分析和实际应用的基础工具。在 Openlayers 中不同的投影坐标参考系的比例尺单位不一样,关于 Openlayers 中的比例尺单位可以参考这篇文章Openlayers的比例尺 (opens new window)

本文主要介绍 Openlayers 中ScaleLine比例尺控件的具体实现以及源码分析。

# 源码分析

ScaleLine类是继承于Control类实现的,其实现如下:

class ScaleLine extends Control {
  constructor(options) {
    options = options ? options : {};
    const element = document.createElement("div");
    element.style.pointerEvents = "none";
    super({
      element: element,
      render: options.render,
      target: options.target,
    });
    this.on;
    this.once;
    this.un;
    const className =
      options.className !== undefined
        ? options.className
        : options.bar
        ? "ol-scale-bar"
        : "ol-scale-line";
    this.innerElement_ = document.createElement("div");
    this.innerElement_.className = className + "-inner";
    this.element.className = className + " " + CLASS_UNSELECTABLE;
    this.element.appendChild(this.innerElement_);
    this.viewState_ = null;
    this.minWidth_ = options.minWidth !== undefined ? options.minWidth : 64;
    this.maxWidth_ = options.maxWidth;
    this.renderedVisible_ = false;
    this.renderedWidth_ = undefined;
    this.renderedHTML_ = "";
    this.addChangeListener(UNITS_PROP, this.handleUnitsChanged_);
    this.setUnits(options.units || "metric");
    this.scaleBar_ = options.bar || false;
    this.scaleBarSteps_ = options.steps || 4;
    this.scaleBarText_ = options.text || false;
    this.dpi_ = options.dpi || undefined;
  }
  getUnits() {
    return this.get(UNITS_PROP);
  }
  handleUnitsChanged_() {
    this.updateElement_();
  }
  setUnits() {
    this.set(UNITS_PROP, units);
  }
  setDpi(dpi) {
    this.dpi_ = dpi;
  }
  updateElement_() {
    const viewState = this.viewState_;

    if (!viewState) {
      if (this.renderedVisible_) {
        this.element.style.display = "none";
        this.renderedVisible_ = false;
      }
      return;
    }

    const center = viewState.center;
    const projection = viewState.projection;
    const units = this.getUnits();
    const pointResolutionUnits = units == "degrees" ? "degrees" : "m";
    let pointResolution = getPointResolution(
      projection,
      viewState.resolution,
      center,
      pointResolutionUnits
    );

    const minWidth =
      (this.minWidth_ * (this.dpi_ || DEFAULT_DPI)) / DEFAULT_DPI;

    const maxWidth =
      this.maxWidth_ !== undefined
        ? (this.maxWidth_ * (this.dpi_ || DEFAULT_DPI)) / DEFAULT_DPI
        : undefined;

    let nominalCount = minWidth * pointResolution;
    let suffix = "";
    if (units == "degrees") {
      const metersPerDegree = METERS_PER_UNIT.degrees;
      nominalCount *= metersPerDegree;
      if (nominalCount < metersPerDegree / 60) {
        suffix = "\u2033"; // seconds
        pointResolution *= 3600;
      } else if (nominalCount < metersPerDegree) {
        suffix = "\u2032"; // minutes
        pointResolution *= 60;
      } else {
        suffix = "\u00b0"; // degrees
      }
    } else if (units == "imperial") {
      if (nominalCount < 0.9144) {
        suffix = "in";
        pointResolution /= 0.0254;
      } else if (nominalCount < 1609.344) {
        suffix = "ft";
        pointResolution /= 0.3048;
      } else {
        suffix = "mi";
        pointResolution /= 1609.344;
      }
    } else if (units == "nautical") {
      pointResolution /= 1852;
      suffix = "NM";
    } else if (units == "metric") {
      if (nominalCount < 1e-6) {
        suffix = "nm";
        pointResolution *= 1e9;
      } else if (nominalCount < 0.001) {
        suffix = "μm";
        pointResolution *= 1000000;
      } else if (nominalCount < 1) {
        suffix = "mm";
        pointResolution *= 1000;
      } else if (nominalCount < 1000) {
        suffix = "m";
      } else {
        suffix = "km";
        pointResolution /= 1000;
      }
    } else if (units == "us") {
      if (nominalCount < 0.9144) {
        suffix = "in";
        pointResolution *= 39.37;
      } else if (nominalCount < 1609.344) {
        suffix = "ft";
        pointResolution /= 0.30480061;
      } else {
        suffix = "mi";
        pointResolution /= 1609.3472;
      }
    } else {
      throw new Error("Invalid units");
    }

    let i = 3 * Math.floor(Math.log(minWidth * pointResolution) / Math.log(10));
    let count, width, decimalCount;
    let previousCount, previousWidth, previousDecimalCount;
    while (true) {
      decimalCount = Math.floor(i / 3);
      const decimal = Math.pow(10, decimalCount);
      count = LEADING_DIGITS[((i % 3) + 3) % 3] * decimal;
      width = Math.round(count / pointResolution);
      if (isNaN(width)) {
        this.element.style.display = "none";
        this.renderedVisible_ = false;
        return;
      }
      if (maxWidth !== undefined && width >= maxWidth) {
        count = previousCount;
        width = previousWidth;
        decimalCount = previousDecimalCount;
        break;
      } else if (width >= minWidth) {
        break;
      }
      previousCount = count;
      previousWidth = width;
      previousDecimalCount = decimalCount;
      ++i;
    }
    const html = this.scaleBar_
      ? this.createScaleBar(width, count, suffix)
      : count.toFixed(decimalCount < 0 ? -decimalCount : 0) + " " + suffix;

    if (this.renderedHTML_ != html) {
      this.innerElement_.innerHTML = html;
      this.renderedHTML_ = html;
    }

    if (this.renderedWidth_ != width) {
      this.innerElement_.style.width = width + "px";
      this.renderedWidth_ = width;
    }

    if (!this.renderedVisible_) {
      this.element.style.display = "";
      this.renderedVisible_ = true;
    }
  }
  createScaleBar(width, scale, suffix) {
    const resolutionScale = this.getScaleForResolution();
    const mapScale =
      resolutionScale < 1
        ? Math.round(1 / resolutionScale).toLocaleString() + " : 1"
        : "1 : " + Math.round(resolutionScale).toLocaleString();
    const steps = this.scaleBarSteps_;
    const stepWidth = width / steps;
    const scaleSteps = [this.createMarker("absolute")];
    for (let i = 0; i < steps; ++i) {
      const cls =
        i % 2 === 0 ? "ol-scale-singlebar-odd" : "ol-scale-singlebar-even";
      scaleSteps.push(
        "<div>" +
          "<div " +
          `class="ol-scale-singlebar ${cls}" ` +
          `style="width: ${stepWidth}px;"` +
          ">" +
          "</div>" +
          this.createMarker("relative") +
          (i % 2 === 0 || steps === 2
            ? this.createStepText(i, width, false, scale, suffix)
            : "") +
          "</div>"
      );
    }
    scaleSteps.push(this.createStepText(steps, width, true, scale, suffix));

    const scaleBarText = this.scaleBarText_
      ? `<div class="ol-scale-text" style="width: ${width}px;">` +
        mapScale +
        "</div>"
      : "";
    return scaleBarText + scaleSteps.join("");
  }
  createMarker(position) {
    const top = position === "absolute" ? 3 : -10;
    return (
      "<div " +
      'class="ol-scale-step-marker" ' +
      `style="position: ${position}; top: ${top}px;"` +
      "></div>"
    );
  }
  createStepText(i, width, isLast, scale, suffix) {
    const length =
      i === 0 ? 0 : Math.round((scale / this.scaleBarSteps_) * i * 100) / 100;
    const lengthString = length + (i === 0 ? "" : " " + suffix);
    const margin = i === 0 ? -3 : (width / this.scaleBarSteps_) * -1;
    const minWidth = i === 0 ? 0 : (width / this.scaleBarSteps_) * 2;
    return (
      "<div " +
      'class="ol-scale-step-text" ' +
      'style="' +
      `margin-left: ${margin}px;` +
      `text-align: ${i === 0 ? "left" : "center"};` +
      `min-width: ${minWidth}px;` +
      `left: ${isLast ? width + "px" : "unset"};` +
      '">' +
      lengthString +
      "</div>"
    );
  }
  getScaleForResolution() {
    const resolution = getPointResolution(
      this.viewState_.projection,
      this.viewState_.resolution,
      this.viewState_.center,
      "m"
    );
    const dpi = this.dpi_ || DEFAULT_DPI;
    const inchesPerMeter = 1000 / 25.4;
    return resolution * inchesPerMeter * dpi;
  }
  render(mapEvent) {
    const frameState = mapEvent.frameState;
    if (!frameState) {
      this.viewState_ = null;
    } else {
      this.viewState_ = frameState.viewState;
    }
    this.updateElement_();
  }
}
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

# ScaleLine控件构造函数

ScaleLine控件构造函数接受一个参数对象options,该对象可以包含如下属性

  • className:控件类名,默认为ol-scale-line
  • bar:控件bar,布尔值,若options.bar存在,则渲染一个滑块;若options.className不存在,但options.bar为true,则控件类名为ol-scale-bar
  • minWidth:控件最小宽度,默认为64
  • maxWidth:控件最大宽度
  • units:控件单位,默认为metric,即米
  • steps:步长,默认为4
  • text:名称,默认为false
  • dpi:像素比,默认为undefined

ScaleLine控件除了创建控件DOM,还会调用this.addChangeListener方法监听比例尺单位,回调函数是handleUnitsChanged_,这点和其它控件不一样,其它控件,比如Zoom控件是监听控件元素本身的AddEventListener,而ScaleLine比例尺控件则是调用Observable类中实现的addChangeListener方法,关于Control类的介绍可以参考这篇文章https://jinuss.github.io/blog/pages/644bd8

另外在构造函数内,还会调用setUnits方法设置UNITS_PROP,该方法内部就是调用this.set去设置UNITS_PROP的值,这个初始化操作就会调用handleUnitsChanged_方法,它的内部就是调用this.updateElement_方法; 在updateElement_方法中会先判断this.viewState_的取值,初始状态时,this.viewState_为null,则继续判断this.renderedVisible_,该值初始化时为false,最后return

# ScaleLine控件主要方法

在构造函数中提到updateElement_方法,初始化时其实相当于啥也没干,在Map类中,但ScaleLine组件执行setMap(该方法是父类Control类)方法时,会调用ScaleLine中的render方法,然后才是真正开始执行ScaleLine类中的核心逻辑

  • render方法:render方法会获取参数mapEvent的frameState,然后将其赋值给this.viewState_,最后执行updateElement_方法

  • updateElement_方法

updateElement_方法的作用就是实时更新当前地图视图的比例尺信息;首先会检查当前地图视图状态,然后通过视图状态viewState获取中心点、投影,通过this.getUnits()获取单位,然后调用getPointResolution方法获取该分辨率的实际物理距离,再就是计算比例尺的最小和最大宽度;接下来就是根据不同的单位来调整比例尺的显示以及对应的单位表示;下一步就是计算比例尺的实际宽度,最后生成比例尺文本信息并更新。

getPointResolution函数就是用于获取某个地图视图分辨率下的"点分辨率"(点的空间分辨率),空间分辨率通常是指在地图上每个像素代表的实际物理距离或角度的大小,单位一般是米或者度等表示;getPointResolution方法内部会先判断投影参数projection是否存在getPointResolutionFunc()方法,若存在则通过它获取点空间分辨率,然后判断投影的坐标的那位是否与参数units一致,若不一致,则调用投影的getMetersPerUnit()方法获取metersPerUnit,若它存在则计算获取PointResolution;若getPointResolutionFunc()方法不存在,则判断获取投影单位,若是度,则直接返回参数resolution分辨率;否则将投影转为EPSG:4326,计算该坐标系下的点空间分辨率,并返回;

  • getUnits和setUnits方法分别是用于获取和设置单位

  • setDpi方法:指定打印机等外设的dpi

  • createScaleBar方法:创建一个块状的比例尺,内部会调用getScaleForResolution

  • createMarker方法:创建一个容器

  • getScaleForResolution方法:内部还是会调用getPointResolution方法,最后返回给定分辨率和单位的适当比例。

# 总结

本文主要介绍了 Openlayers 中ScaleLine比例尺控件的实现原理,核心方法是getPointResolution的过程,只需要重新定义render方法,则可以在地图视图变化时实时调用,借此更新比例尺的信息。

编辑 (opens new window)
上次更新: 2024/12/17, 02:39:15
ZoomSlider滑块缩放控件源码分析
Attribution属性控件源码分析

← ZoomSlider滑块缩放控件源码分析 Attribution属性控件源码分析→

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