TextBuilder类
# 概述
在 Openlayers 中,TextBuilder
类是一个辅助类,用于创建基于文本的样式,通常是用来在地图上的特定要素(如矢量图形或标记)上显示文本。该类继承于CanvasBuilder
类,关于CanvasBuilder
类,可以参考这篇文章
TextBuilder
主要通过将文本样式和相关的图形(如位置、大小、颜色等)结合起来,从而提供灵活的方式来渲染和定制文本显示。它被用作在地图上标注或显示文本内容的一种工具,通常在设置 样式style
时使用。
本文主要介绍TextBuilder
类,即CanvasTextBuilder
类的实现原理。
# 源码分析
# CanvasTextBuilder
类的源码实现
CanvasTextBuilder
类的源码实现如下:
class CanvasTextBuilder extends CanvasBuilder {
constructor(tolerance, maxExtent, resolution, pixelRatio) {
super(tolerance, maxExtent, resolution, pixelRatio);
//用于存储文本标签的数组或集合
this.labels_ = null;
//用于存储要渲染的文本内容
this.text_ = "";
//水平方向上的偏移量
this.textOffsetX_ = 0;
//垂直方向上的偏移量
this.textOffsetY_ = 0;
//这个属性决定文本是否应随视图旋转。如果设置为 true,文本将随着地图视角的旋转而旋转。undefined 表示没有明确设置
this.textRotateWithView_ = undefined;
//用于指定文本的旋转角度,单位是弧度。可以通过这个属性来控制文本的方向。
this.textRotation_ = 0;
//存储文本的填充样式(比如颜色或渐变)。用于指定文本的颜色或填充效果。
this.textFillState_ = null;
//是一个对象,管理不同的填充样式(fillStyle)。defaultFillStyle 是默认的填充样式键,它初始化为 defaultFillStyle 的样式。此对象可以扩展以包含不同的样式
this.fillStates = {};
this.fillStates[defaultFillStyle] = { fillStyle: defaultFillStyle };
//存储文本的描边样式(比如线条宽度、颜色等),用于控制文本的轮廓效果。
this.textStrokeState_ = null;
//管理不同的描边样式。它的值是一个对象,包含具体的描边样式。
this.strokeStates = {};
//这是一个存储文本状态的对象,可能包含文本的其他设置(如字体、大小等)
this.textState_ = {};
//这些属性用于存储文本、填充和描边的“键”或“标识符”。通常用于缓存文本的样式,以避免重复计算
this.textKey_ = "";
this.fillKey_ = "";
this.strokeKey_ = "";
//该属性控制是否启用文本去重模式(decluttering)。在地图上,如果文本标签彼此重叠,去重模式可以帮助避免多个标签重叠在一起
this.declutterMode_ = undefined;
//存储图像与文本一起显示的标志或状态。在某些情况下,地图上不仅显示文本,还有其他图像元素与文本一起渲染
this.declutterImageWithText_ = undefined;
}
finish() {
const instructions = super.finish();
instructions.textStates = this.textStates;
instructions.fillStates = this.fillStates;
instructions.strokeStates = this.strokeStates;
return instructions;
}
drawText(geometry, feature, index) {
const fillState = this.textFillState_;
const strokeState = this.textStrokeState_;
const textState = this.textState_;
if (this.text_ === "" || !textState || (!fillState && !strokeState)) {
return;
}
const coordinates = this.coordinates;
let begin = coordinates.length;
const geometryType = geometry.getType();
let flatCoordinates = null;
let stride = geometry.getStride();
if (
textState.placement === "line" &&
(geometryType == "LineString" ||
geometryType == "MultiLineString" ||
geometryType == "Polygon" ||
geometryType == "MultiPolygon")
) {
if (!intersects(this.maxExtent, geometry.getExtent())) {
return;
}
let ends;
flatCoordinates = geometry.getFlatCoordinates();
if (geometryType == "LineString") {
ends = [flatCoordinates.length];
} else if (geometryType == "MultiLineString") {
ends = geometry.getEnds();
} else if (geometryType == "Polygon") {
ends = geometry.getEnds().slice(0, 1);
} else if (geometryType == "MultiPolygon") {
const endss = geometry.getEndss();
ends = [];
for (let i = 0, ii = endss.length; i < ii; ++i) {
ends.push(endss[i][0]);
}
}
this.beginGeometry(geometry, feature, index);
const repeat = textState.repeat;
const textAlign = repeat ? undefined : textState.textAlign;
// No `justify` support for line placement.
let flatOffset = 0;
for (let o = 0, oo = ends.length; o < oo; ++o) {
let chunks;
if (repeat) {
chunks = lineChunk(
repeat * this.resolution,
flatCoordinates,
flatOffset,
ends[o],
stride
);
} else {
chunks = [flatCoordinates.slice(flatOffset, ends[o])];
}
for (let c = 0, cc = chunks.length; c < cc; ++c) {
const chunk = chunks[c];
let chunkBegin = 0;
let chunkEnd = chunk.length;
if (textAlign == undefined) {
const range = matchingChunk(
textState.maxAngle,
chunk,
0,
chunk.length,
2
);
chunkBegin = range[0];
chunkEnd = range[1];
}
for (let i = chunkBegin; i < chunkEnd; i += stride) {
coordinates.push(chunk[i], chunk[i + 1]);
}
const end = coordinates.length;
flatOffset = ends[o];
this.drawChars_(begin, end);
begin = end;
}
}
this.endGeometry(feature);
} else {
let geometryWidths = textState.overflow ? null : [];
switch (geometryType) {
case "Point":
case "MultiPoint":
flatCoordinates = geometry.getFlatCoordinates();
break;
case "LineString":
flatCoordinates = geometry.getFlatMidpoint();
break;
case "Circle":
flatCoordinates = geometry.getCenter();
break;
case "MultiLineString":
flatCoordinates = geometry.getFlatMidpoints();
stride = 2;
break;
case "Polygon":
flatCoordinates = geometry.getFlatInteriorPoint();
if (!textState.overflow) {
geometryWidths.push(flatCoordinates[2] / this.resolution);
}
stride = 3;
break;
case "MultiPolygon":
const interiorPoints = geometry.getFlatInteriorPoints();
flatCoordinates = [];
for (let i = 0, ii = interiorPoints.length; i < ii; i += 3) {
if (!textState.overflow) {
geometryWidths.push(interiorPoints[i + 2] / this.resolution);
}
flatCoordinates.push(interiorPoints[i], interiorPoints[i + 1]);
}
if (flatCoordinates.length === 0) {
return;
}
stride = 2;
break;
default:
}
const end = this.appendFlatPointCoordinates(flatCoordinates, stride);
if (end === begin) {
return;
}
if (
geometryWidths &&
(end - begin) / 2 !== flatCoordinates.length / stride
) {
let beg = begin / 2;
geometryWidths = geometryWidths.filter((w, i) => {
const keep =
coordinates[(beg + i) * 2] === flatCoordinates[i * stride] &&
coordinates[(beg + i) * 2 + 1] === flatCoordinates[i * stride + 1];
if (!keep) {
--beg;
}
return keep;
});
}
this.saveTextStates_();
if (textState.backgroundFill || textState.backgroundStroke) {
this.setFillStrokeStyle(
textState.backgroundFill,
textState.backgroundStroke
);
if (textState.backgroundFill) {
this.updateFillStyle(this.state, this.createFill);
}
if (textState.backgroundStroke) {
this.updateStrokeStyle(this.state, this.applyStroke);
this.hitDetectionInstructions.push(this.createStroke(this.state));
}
}
this.beginGeometry(geometry, feature, index);
let padding = textState.padding;
if (
padding != defaultPadding &&
(textState.scale[0] < 0 || textState.scale[1] < 0)
) {
let p0 = textState.padding[0];
let p1 = textState.padding[1];
let p2 = textState.padding[2];
let p3 = textState.padding[3];
if (textState.scale[0] < 0) {
p1 = -p1;
p3 = -p3;
}
if (textState.scale[1] < 0) {
p0 = -p0;
p2 = -p2;
}
padding = [p0, p1, p2, p3];
}
const pixelRatio = this.pixelRatio;
this.instructions.push([
CanvasInstruction.DRAW_IMAGE,
begin,
end,
null,
NaN,
NaN,
NaN,
1,
0,
0,
this.textRotateWithView_,
this.textRotation_,
[1, 1],
NaN,
this.declutterMode_,
this.declutterImageWithText_,
padding == defaultPadding
? defaultPadding
: padding.map(function (p) {
return p * pixelRatio;
}),
!!textState.backgroundFill,
!!textState.backgroundStroke,
this.text_,
this.textKey_,
this.strokeKey_,
this.fillKey_,
this.textOffsetX_,
this.textOffsetY_,
geometryWidths,
]);
const scale = 1 / pixelRatio;
const currentFillStyle = this.state.fillStyle;
if (textState.backgroundFill) {
this.state.fillStyle = defaultFillStyle;
this.hitDetectionInstructions.push(this.createFill(this.state));
}
this.hitDetectionInstructions.push([
CanvasInstruction.DRAW_IMAGE,
begin,
end,
null,
NaN,
NaN,
NaN,
1,
0,
0,
this.textRotateWithView_,
this.textRotation_,
[scale, scale],
NaN,
this.declutterMode_,
this.declutterImageWithText_,
padding,
!!textState.backgroundFill,
!!textState.backgroundStroke,
this.text_,
this.textKey_,
this.strokeKey_,
this.fillKey_ ? defaultFillStyle : this.fillKey_,
this.textOffsetX_,
this.textOffsetY_,
geometryWidths,
]);
if (textState.backgroundFill) {
this.state.fillStyle = currentFillStyle;
this.hitDetectionInstructions.push(this.createFill(this.state));
}
this.endGeometry(feature);
}
}
saveTextStates_() {
const strokeState = this.textStrokeState_;
const textState = this.textState_;
const fillState = this.textFillState_;
const strokeKey = this.strokeKey_;
if (strokeState) {
if (!(strokeKey in this.strokeStates)) {
this.strokeStates[strokeKey] = {
strokeStyle: strokeState.strokeStyle,
lineCap: strokeState.lineCap,
lineDashOffset: strokeState.lineDashOffset,
lineWidth: strokeState.lineWidth,
lineJoin: strokeState.lineJoin,
miterLimit: strokeState.miterLimit,
lineDash: strokeState.lineDash,
};
}
}
const textKey = this.textKey_;
if (!(textKey in this.textStates)) {
this.textStates[textKey] = {
font: textState.font,
textAlign: textState.textAlign || defaultTextAlign,
justify: textState.justify,
textBaseline: textState.textBaseline || defaultTextBaseline,
scale: textState.scale,
};
}
const fillKey = this.fillKey_;
if (fillState) {
if (!(fillKey in this.fillStates)) {
this.fillStates[fillKey] = {
fillStyle: fillState.fillStyle,
};
}
}
}
drawChars_(begin, end) {
const strokeState = this.textStrokeState_;
const textState = this.textState_;
const strokeKey = this.strokeKey_;
const textKey = this.textKey_;
const fillKey = this.fillKey_;
this.saveTextStates_();
const pixelRatio = this.pixelRatio;
const baseline = TEXT_ALIGN[textState.textBaseline];
const offsetY = this.textOffsetY_ * pixelRatio;
const text = this.text_;
const strokeWidth = strokeState
? (strokeState.lineWidth * Math.abs(textState.scale[0])) / 2
: 0;
this.instructions.push([
CanvasInstruction.DRAW_CHARS,
begin,
end,
baseline,
textState.overflow,
fillKey,
textState.maxAngle,
pixelRatio,
offsetY,
strokeKey,
strokeWidth * pixelRatio,
text,
textKey,
1,
this.declutterMode_,
]);
this.hitDetectionInstructions.push([
CanvasInstruction.DRAW_CHARS,
begin,
end,
baseline,
textState.overflow,
fillKey ? defaultFillStyle : fillKey,
textState.maxAngle,
pixelRatio,
offsetY,
strokeKey,
strokeWidth * pixelRatio,
text,
textKey,
1 / pixelRatio,
this.declutterMode_,
]);
}
}
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
# CanvasTextBuilder
类的构造函数
CanvasTextBuilder
类的构造函数比较通用,接受以下参数:
tolerance
:容差,通常用于指定绘制精度或容许的误差范围。maxExtent
:最大范围,用来限制图形的显示范围。resolution
:地图的分辨率。pixelRatio
:像素比率,用来处理不同屏幕或设备的显示效果。
除此之外,构造函数还定义初始化了一些专门用于处理文本渲染的变量或属性,如上述源码中的注释,它们提供了对文本渲染的精细控制,使得文本的显示效果可以根据不同需求进行调整和优化。
# CanvasTextBuilder
类的主要方法
CanvasTextBuilder
类的主要方法如下:
setTextStyle(textStyle,sharedData)
方法
setTextStyle
方法用于设置文本样式,接受两个参数textStyle
文本样式和sharedData
数据;参数textStyle
是Text
类的实例(ol/style/Text.js
),参数sharedData
是一个对象;方法内部会先判断,若参数textStyle
没传值,则将this.text_
赋值空字符,即表示不渲染;否则根据textStyle
的实例获取一些样式属性,赋值到构造函数中定义的属性或变量上,最后设置this.declutterMode_
和this.declutterImageWithText_
的值。
drawText(geometry,feature,index)
方法
drawText
方法一般会在调用setTextStyle
方法后再被调用,它用于绘制文本,接受三个参数geometry
几何对象、feature
要素和index
索引;首先方法内部会先判断this.text_
、textState
和fillState
以及strokeState
等,若其中一个不存在就返回;然后判断textSate.placement
是否为line
,该属性表明文本会沿着几何图形的路径绘制;并且只有几何图形存在线段时,后面的逻辑才生效,后面会调用drawChars
方法沿着路径生成绘制文本字符的指令,指令的类型是CanvasInstruction.DRAW_CHARS
;否则生成的指令类型是CanvasInstruction.DRAW_IMAGE
。
saveTextStates_()
方法:保存当前的文本状态,以便后续恢复或使用drawChars_(begin,end)
方法
该方法负责绘制文本字符,并将文本的样式、位置信息、偏移、旋转等各种参数传递给渲染指令。这些指令被用于最终的地图渲染。该方法不仅会执行正常的绘制操作,还会生成碰撞检测指令,使得文本可以参与到用户与地图的交互中(如点击、触摸检测等
finish()
方法:该方法会调用父类CanvasBuilder
类的finish
方法获取指令集instructions
,然后给该指令集添加三个属性textStates
、fillStates
和strokeStates
,最后返回指令集instructions
。
# 总结
CanvasTextBuilder
类提供了多个属性来控制文本的显示样式、位置、旋转和去重处理等。它依赖于继承自 CanvasBuilder
的通用功能,并通过这些属性为文本的渲染提供灵活的定制能力。