Build类
# 概述
在 Openlayers 中,矢量数据绘制的基础是canvas
,其核心逻辑主要分为两步:(一)根据需要绘制的几何对象geometry
生成绘制任务;(二)任务处理,执行绘制操作。第一步主要是基于CanvasBuilder
类,而第二步则是基于Executor
类进行绘制。
CanvasBuilder
类继承于VectorContext
类(关于VectorContext
类,可以参考这篇文章()[]),主要用于构建矢量图形(如点、线、面等)的绘制任务,提供多个属性来支持具体的渲染操作。本文主要介绍CanvasBuilder
类,它也是一个基类。
# 源码分析
# CanvasBuilder
类的源码实现
CanvasBuilder
类的源码实现如下:
class CanvasBuilder extends VectorContext {
constructor(tolerance, maxExtent, resolution, pixelRatio) {
super();
this.tolerance = tolerance;
this.maxExtent = maxExtent;
this.pixelRatio = pixelRatio;
this.maxLineWidth = 0;
this.resolution = resolution;
this.beginGeometryInstruction1_ = null;
this.beginGeometryInstruction2_ = null;
this.bufferedMaxExtent_ = null;
this.instructions = [];
this.coordinates = [];
this.tmpCoordinate_ = [];
this.hitDetectionInstructions = [];
this.state = {};
}
applyPixelRatio(dashArray) {
const pixelRatio = this.pixelRatio;
return pixelRatio == 1
? dashArray
: dashArray.map(function (dash) {
return dash * pixelRatio;
});
}
appendFlatPointCoordinates(flatCoordinates, stride) {
const extent = this.getBufferedMaxExtent();
const tmpCoord = this.tmpCoordinate_;
const coordinates = this.coordinates;
let myEnd = coordinates.length;
for (let i = 0, ii = flatCoordinates.length; i < ii; i += stride) {
tmpCoord[0] = flatCoordinates[i];
tmpCoord[1] = flatCoordinates[i + 1];
if (containsCoordinate(extent, tmpCoord)) {
coordinates[myEnd++] = tmpCoord[0];
coordinates[myEnd++] = tmpCoord[1];
}
}
return myEnd;
}
appendFlatLineCoordinates(
flatCoordinates,
offset,
end,
stride,
closed,
skipFirst
) {
const coordinates = this.coordinates;
let myEnd = coordinates.length;
const extent = this.getBufferedMaxExtent();
if (skipFirst) {
offset += stride;
}
let lastXCoord = flatCoordinates[offset];
let lastYCoord = flatCoordinates[offset + 1];
const nextCoord = this.tmpCoordinate_;
let skipped = true;
let i, lastRel, nextRel;
for (i = offset + stride; i < end; i += stride) {
nextCoord[0] = flatCoordinates[i];
nextCoord[1] = flatCoordinates[i + 1];
nextRel = coordinateRelationship(extent, nextCoord);
if (nextRel !== lastRel) {
if (skipped) {
coordinates[myEnd++] = lastXCoord;
coordinates[myEnd++] = lastYCoord;
skipped = false;
}
coordinates[myEnd++] = nextCoord[0];
coordinates[myEnd++] = nextCoord[1];
} else if (nextRel === Relationship.INTERSECTING) {
coordinates[myEnd++] = nextCoord[0];
coordinates[myEnd++] = nextCoord[1];
skipped = false;
} else {
skipped = true;
}
lastXCoord = nextCoord[0];
lastYCoord = nextCoord[1];
lastRel = nextRel;
}
// Last coordinate equals first or only one point to append:
if ((closed && skipped) || i === offset + stride) {
coordinates[myEnd++] = lastXCoord;
coordinates[myEnd++] = lastYCoord;
}
return myEnd;
}
drawCustomCoordinates_(flatCoordinates, offset, ends, stride, builderEnds) {
for (let i = 0, ii = ends.length; i < ii; ++i) {
const end = ends[i];
const builderEnd = this.appendFlatLineCoordinates(
flatCoordinates,
offset,
end,
stride,
false,
false
);
builderEnds.push(builderEnd);
offset = end;
}
return offset;
}
drawCustom(geometry, feature, renderer, hitDetectionRenderer, index) {
this.beginGeometry(geometry, feature, index);
const type = geometry.getType();
const stride = geometry.getStride();
const builderBegin = this.coordinates.length;
let flatCoordinates, builderEnd, builderEnds, builderEndss;
let offset;
switch (type) {
case "MultiPolygon":
flatCoordinates = geometry.getOrientedFlatCoordinates();
builderEndss = [];
const endss = geometry.getEndss();
offset = 0;
for (let i = 0, ii = endss.length; i < ii; ++i) {
const myEnds = [];
offset = this.drawCustomCoordinates_(
flatCoordinates,
offset,
endss[i],
stride,
myEnds
);
builderEndss.push(myEnds);
}
this.instructions.push([
CanvasInstruction.CUSTOM,
builderBegin,
builderEndss,
geometry,
renderer,
inflateMultiCoordinatesArray,
index,
]);
this.hitDetectionInstructions.push([
CanvasInstruction.CUSTOM,
builderBegin,
builderEndss,
geometry,
hitDetectionRenderer || renderer,
inflateMultiCoordinatesArray,
index,
]);
break;
case "Polygon":
case "MultiLineString":
builderEnds = [];
flatCoordinates =
type == "Polygon"
? geometry.getOrientedFlatCoordinates()
: geometry.getFlatCoordinates();
offset = this.drawCustomCoordinates_(
flatCoordinates,
0,
geometry.getEnds(),
stride,
builderEnds
);
this.instructions.push([
CanvasInstruction.CUSTOM,
builderBegin,
builderEnds,
geometry,
renderer,
inflateCoordinatesArray,
index,
]);
this.hitDetectionInstructions.push([
CanvasInstruction.CUSTOM,
builderBegin,
builderEnds,
geometry,
hitDetectionRenderer || renderer,
inflateCoordinatesArray,
index,
]);
break;
case "LineString":
case "Circle":
flatCoordinates = geometry.getFlatCoordinates();
builderEnd = this.appendFlatLineCoordinates(
flatCoordinates,
0,
flatCoordinates.length,
stride,
false,
false
);
this.instructions.push([
CanvasInstruction.CUSTOM,
builderBegin,
builderEnd,
geometry,
renderer,
inflateCoordinates,
index,
]);
this.hitDetectionInstructions.push([
CanvasInstruction.CUSTOM,
builderBegin,
builderEnd,
geometry,
hitDetectionRenderer || renderer,
inflateCoordinates,
index,
]);
break;
case "MultiPoint":
flatCoordinates = geometry.getFlatCoordinates();
builderEnd = this.appendFlatPointCoordinates(flatCoordinates, stride);
if (builderEnd > builderBegin) {
this.instructions.push([
CanvasInstruction.CUSTOM,
builderBegin,
builderEnd,
geometry,
renderer,
inflateCoordinates,
index,
]);
this.hitDetectionInstructions.push([
CanvasInstruction.CUSTOM,
builderBegin,
builderEnd,
geometry,
hitDetectionRenderer || renderer,
inflateCoordinates,
index,
]);
}
break;
case "Point":
flatCoordinates = geometry.getFlatCoordinates();
this.coordinates.push(flatCoordinates[0], flatCoordinates[1]);
builderEnd = this.coordinates.length;
this.instructions.push([
CanvasInstruction.CUSTOM,
builderBegin,
builderEnd,
geometry,
renderer,
undefined,
index,
]);
this.hitDetectionInstructions.push([
CanvasInstruction.CUSTOM,
builderBegin,
builderEnd,
geometry,
hitDetectionRenderer || renderer,
undefined,
index,
]);
break;
default:
}
this.endGeometry(feature);
}
beginGeometry(geometry, feature, index) {
this.beginGeometryInstruction1_ = [
CanvasInstruction.BEGIN_GEOMETRY,
feature,
0,
geometry,
index,
];
this.instructions.push(this.beginGeometryInstruction1_);
this.beginGeometryInstruction2_ = [
CanvasInstruction.BEGIN_GEOMETRY,
feature,
0,
geometry,
index,
];
this.hitDetectionInstructions.push(this.beginGeometryInstruction2_);
}
finish() {
return {
instructions: this.instructions,
hitDetectionInstructions: this.hitDetectionInstructions,
coordinates: this.coordinates,
};
}
reverseHitDetectionInstructions() {
const hitDetectionInstructions = this.hitDetectionInstructions;
hitDetectionInstructions.reverse();
let i;
const n = hitDetectionInstructions.length;
let instruction;
let type;
let begin = -1;
for (i = 0; i < n; ++i) {
instruction = hitDetectionInstructions[i];
type = instruction[0];
if (type == CanvasInstruction.END_GEOMETRY) {
begin = i;
} else if (type == CanvasInstruction.BEGIN_GEOMETRY) {
instruction[2] = i;
reverseSubArray(this.hitDetectionInstructions, begin, i);
begin = -1;
}
}
}
setFillStrokeStyle(fillStyle, strokeStyle) {
const state = this.state;
if (fillStyle) {
const fillStyleColor = fillStyle.getColor();
state.fillPatternScale =
fillStyleColor &&
typeof fillStyleColor === "object" &&
"src" in fillStyleColor
? this.pixelRatio
: 1;
state.fillStyle = asColorLike(
fillStyleColor ? fillStyleColor : defaultFillStyle
);
} else {
state.fillStyle = undefined;
}
if (strokeStyle) {
const strokeStyleColor = strokeStyle.getColor();
state.strokeStyle = asColorLike(
strokeStyleColor ? strokeStyleColor : defaultStrokeStyle
);
const strokeStyleLineCap = strokeStyle.getLineCap();
state.lineCap =
strokeStyleLineCap !== undefined ? strokeStyleLineCap : defaultLineCap;
const strokeStyleLineDash = strokeStyle.getLineDash();
state.lineDash = strokeStyleLineDash
? strokeStyleLineDash.slice()
: defaultLineDash;
const strokeStyleLineDashOffset = strokeStyle.getLineDashOffset();
state.lineDashOffset = strokeStyleLineDashOffset
? strokeStyleLineDashOffset
: defaultLineDashOffset;
const strokeStyleLineJoin = strokeStyle.getLineJoin();
state.lineJoin =
strokeStyleLineJoin !== undefined
? strokeStyleLineJoin
: defaultLineJoin;
const strokeStyleWidth = strokeStyle.getWidth();
state.lineWidth =
strokeStyleWidth !== undefined ? strokeStyleWidth : defaultLineWidth;
const strokeStyleMiterLimit = strokeStyle.getMiterLimit();
state.miterLimit =
strokeStyleMiterLimit !== undefined
? strokeStyleMiterLimit
: defaultMiterLimit;
if (state.lineWidth > this.maxLineWidth) {
this.maxLineWidth = state.lineWidth;
// invalidate the buffered max extent cache
this.bufferedMaxExtent_ = null;
}
} else {
state.strokeStyle = undefined;
state.lineCap = undefined;
state.lineDash = null;
state.lineDashOffset = undefined;
state.lineJoin = undefined;
state.lineWidth = undefined;
state.miterLimit = undefined;
}
}
createFill(state) {
const fillStyle = state.fillStyle;
const fillInstruction = [CanvasInstruction.SET_FILL_STYLE, fillStyle];
if (typeof fillStyle !== "string") {
fillInstruction.push(state.fillPatternScale);
}
return fillInstruction;
}
applyStroke(state) {
this.instructions.push(this.createStroke(state));
}
createStroke(state) {
return [
CanvasInstruction.SET_STROKE_STYLE,
state.strokeStyle,
state.lineWidth * this.pixelRatio,
state.lineCap,
state.lineJoin,
state.miterLimit,
this.applyPixelRatio(state.lineDash),
state.lineDashOffset * this.pixelRatio,
];
}
updateFillStyle(state, createFill) {
const fillStyle = state.fillStyle;
if (typeof fillStyle !== "string" || state.currentFillStyle != fillStyle) {
if (fillStyle !== undefined) {
this.instructions.push(createFill.call(this, state));
}
state.currentFillStyle = fillStyle;
}
}
updateStrokeStyle(state, applyStroke) {
const strokeStyle = state.strokeStyle;
const lineCap = state.lineCap;
const lineDash = state.lineDash;
const lineDashOffset = state.lineDashOffset;
const lineJoin = state.lineJoin;
const lineWidth = state.lineWidth;
const miterLimit = state.miterLimit;
if (
state.currentStrokeStyle != strokeStyle ||
state.currentLineCap != lineCap ||
(lineDash != state.currentLineDash &&
!equals(state.currentLineDash, lineDash)) ||
state.currentLineDashOffset != lineDashOffset ||
state.currentLineJoin != lineJoin ||
state.currentLineWidth != lineWidth ||
state.currentMiterLimit != miterLimit
) {
if (strokeStyle !== undefined) {
applyStroke.call(this, state);
}
state.currentStrokeStyle = strokeStyle;
state.currentLineCap = lineCap;
state.currentLineDash = lineDash;
state.currentLineDashOffset = lineDashOffset;
state.currentLineJoin = lineJoin;
state.currentLineWidth = lineWidth;
state.currentMiterLimit = miterLimit;
}
}
endGeometry(feature) {
this.beginGeometryInstruction1_[2] = this.instructions.length;
this.beginGeometryInstruction1_ = null;
this.beginGeometryInstruction2_[2] = this.hitDetectionInstructions.length;
this.beginGeometryInstruction2_ = null;
const endGeometryInstruction = [CanvasInstruction.END_GEOMETRY, feature];
this.instructions.push(endGeometryInstruction);
this.hitDetectionInstructions.push(endGeometryInstruction);
}
getBufferedMaxExtent() {
if (!this.bufferedMaxExtent_) {
this.bufferedMaxExtent_ = clone(this.maxExtent);
if (this.maxLineWidth > 0) {
const width = (this.resolution * (this.maxLineWidth + 1)) / 2;
buffer(this.bufferedMaxExtent_, width, this.bufferedMaxExtent_);
}
}
return this.bufferedMaxExtent_;
}
}
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
466
467
468
469
470
471
472
473
474
475
# CanvasBuilder
类的构造函数
CanvasBuilder
类的构造函数接受四个参数:tolerance
(绘制精度的容忍度)、maxExtent
(地图的最大范围)、resolution
(地图的分辨率)和pixelRatio
(像素比,表示当前设备的像素密度)。构造函数会将这四个参数挂到对象的全局变量上,并且初始化了如下变量:
this.maxLineWidth
:默认为0
,最大线条宽度,用于设置矢量图形的最大线条宽度,确保绘制线条的宽度不会超出这个值this.beginGeometryInstruction1_
:默认为null
,内部变量,用于存储绘制几何图形的指令信息this.beginGeometryInstruction2_
:默认为null
,内部变量,用于存储绘制几何图形的指令信息this.bufferedMaxExtent
:默认为null
,用于存储一个带有缓冲区的最大绘制范围this.instructions
:默认为[]
,用于存储绘制过程中生成的指令,每个绘制的元素都会先生成一个指令,后续交由Executor
类处理this.coordinate_
:默认为[]
,用于存储绘制过程中的坐标数据this.tmpCoordinate_
:默认为[]
,用于存储中间计算得到的坐标,临时数组this.hitDetectionInstructions
:默认为[]
,用于存储绘制时的点击检测指令this.state
:默认为{}
,用于存储绘制状态的相关信息,可能包括当前的样式、绘制模式等。它用于在绘制过程中保持状态,确保每次绘制时都能正确应用样式和设置。
# CanvasBuilder
类的主要方法
CanvasBuilder
类的主要方法如下:
applyPixelRatio(dashArray)
:根据设备的像素比率来调整虚线的样式数组,参数dashArray
表示虚线的模式,如[3,2]
表示实线长3px
,虚线长2px
appendFlatPointCoordinates(flatCoordinates,stride)
:该方法会在绘制多个点时被调用,接受两个参数flatCoordinates
扁平一维数组和stride
步幅,用于从flatCoordinates
中根据stride
取出每个点坐标,然后调用this.getBufferedMaxExtent
判断点是否在地图最大范围内,若不在则舍去,最后返回坐标数组的长度。appendFlatLineCoordinates(flatCoordinates,offset,end,stride,closed,skipFirst,)
:该方法会在绘制多边形和线段中的一条线段时调用,根据给定的坐标数组,处理并将其坐标按照一定规则(如是否跳过第一个点、是否闭合和点是否在地图最大范围内)添加到当前对象的坐标数组中。接受六个参数:flatCoordinates
坐标数组、offset
开始绘制坐标的位置、end
结束绘制坐标的位置、stride
步幅、closed
是否闭合和skipFirst
是否跳过第一个点;该方法在遍历坐标数组flatCoordinates
时,也会调用this.getBufferedMaxExtent
方法来确保点在地图范围内,返回的是坐标长度并且修改了this.coordinates
。drawCustomCoordinates_(flatCoordinates, offset, ends, stride, builderEnds)
:在绘制多条线段,无论是多边形、多线段还是多个多边形时会调用,本质上就是从this.flatCoordinates
中取出每条线段的坐标,然后调用this.appendFlatLineCoordinates
方法。drawCustom(geometry, feature, renderer, hitDetectionRenderer, index)
:核心方法,用于自定义绘制几何图形,接受五个参数:geometry
(要绘制的几何对象)、feature
(要素数据可能包括附加属性等)、renderer
(用于实际绘制的渲染器,负责将坐标数据转换为屏幕上的可视内容)、hitDetectionRenderer
(用于碰撞检测的渲染器,用于在地图上检测用户点击的区域)和index
(几何图形在数据集中的索引);方法内部会先调用this.beginGeometry
方法初始化并开始处理当前的几何对象、特征和索引;然后通过geometry.getType()
获取几何对象的类型,再根据类型获取几何对象的坐标数据,生成不同的绘制指令和检测指令,分别存放在this.instructions
和this.hitDetectionInstructions
中,最后调用this.endGeometry
方法结束当前几何对象的处理。beginGeometry(geometry, feature, index)
:该方法表示开始处理几何对象,会在this.instructions
和this.hitDetectionInstructions
中添加绘制开始的指令,finish()
:该方法会返回生成的绘制指令集和碰撞检测指令集以及几何图形的坐标数据reverseHitDetectionInstructions()
:反转碰撞检测指令集setFillStrokeStyle(fillStyle, strokeStyle)
:设置填充和边框样式,接受两个参数:fillStyle
和strokeStyle
,这两个参数分别是Fill
类和Stroke
类的实例对象,setFillStrokeStyle
方法会根据参数上的样式,将其设置到this.state
上,进而作为在canvas
上绘制的样式,若参数不存在,则置为undefined
。createFill(state)
:该方法用于创建填充样式指令集,会先从参数state
上取出fillStyle
,然后构造填充样式指令,并返回applyStroke(state)
:接受一个参数state
,然后调用this.createStroke
方法构建一个边框样式指令,将其添加到绘制指令集this.instructions
中。createStroke(state)
:接受一个参数state
,然后构建一个边框样式指令并返回updateFillStyle(state,createFill)
:用于更新填充样式,接受两个参数state
和createFill
方法,updateFillStyle
方法首先会判断,若state
中的fillStyle
和currentFillStyle
不相等或者fillStyle
不为undefined
也不是一个字符串,就调用参数createFill
方法构建一个填充样式指令,并将其添加到绘制指令集中this.instructions
中,最后会修改state
的currentFillStyle
为fillStyle
。updateStrokeStyle(state,applyStroke)
:用于更新边框样式,endGeometry(feature)
:结束绘制时的处理,会将this.beginGeometryInstruction1_
和this.beginGeometryInstruction2_
重置,并且构建结束绘制指令,并将其添加到绘制指令集和碰撞检测指令集中。getBufferedMaxExtent()
:获取缓冲区中的地图范围,方法内部会先判断缓冲变量是否存在,若不存在,则通过this.maxExtent
构造;否则直接返回缓冲区地图范围
# 指令
指令的分类如下:
const Instruction = {
//开始一个几何图形的绘制。例如,当开始绘制一个新的多边形或线时,使用此指令。它标记了一个几何体的起始。
BEGIN_GEOMETRY: 0,
//开始绘制一个路径。在绘制一个线段或多边形的边界时使用该指令,它是路径的开始。
BEGIN_PATH: 1,
//绘制圆形。此指令用于绘制一个圆,通常是基于中心点和半径来定义的几何图形。
CIRCLE: 2,
//结束当前路径,回到路径的起点。例如,在绘制多边形时,使用此指令来连接最后一个点到起点,形成封闭的多边形
CLOSE_PATH: 3,
//自定义指令,用于处理用户自定义的绘制操作。这个指令可能是为了支持某些不常见或不标准的图形绘制方式。
CUSTOM: 4,
//绘制字符。这个指令通常用于文本绘制,将字符渲染到画布上。
DRAW_CHARS: 5,
//绘制图像。此指令用于绘制图像到画布上,通常在标记或图形上放置图像标记时使用。
DRAW_IMAGE: 6,
//结束一个几何体的绘制。这标志着某个几何体绘制的完成,通常是在 BEGIN_GEOMETRY 之后使用。
END_GEOMETRY: 7,
//填充操作。这个指令通常用于填充多边形、路径或其他封闭形状的区域,应用指定的填充样式(如颜色或渐变)。
FILL: 8,
//同时执行 moveTo 和 lineTo 操作。它用于在绘图过程中同时移动到指定位置并画线,通常用于路径的绘制。
MOVE_TO_LINE_TO: 9,
//设置填充样式。使用此指令来定义图形的填充样式,例如颜色、渐变等
SET_FILL_STYLE: 10,
//设置描边样式。这个指令用于定义路径或几何图形的边框样式,例如线条颜色、宽度等。
SET_STROKE_STYLE: 11,
//描边操作。使用此指令绘制图形的轮廓,通常用于多边形、线条等形状的边缘。
STROKE: 12,
};
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
# 总结
本文主要介绍了CanvasBuilder
类的实现,核心逻辑,就是获取几何图形(几何对象)的类型,然后通过类型算出坐标以及构建指令集;最后列举了指令集的类别。