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_();
}
}
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
方法,则可以在地图视图变化时实时调用,借此更新比例尺的信息。