RegularShape类
# 概述
在 Openlayers 中,RegularShape
类会将常规形状样式设置为矢量特征。当提供 radius
时,生成的形状将是一个正多边形;当同时提供 radius
和 radius2
时,生成的形状将是一个星形。
RegularShape
类继承于ImageStyle
类,关于ImageStyle
类可以参考这篇文章
# 源码分析
# RegularShape
类的源码实现
RegularShape
类的源码实现如下:
class RegularShape extends ImageStyle {
constructor(options) {
super({
opacity: 1,
rotateWithView:
options.rotateWithView !== undefined ? options.rotateWithView : false,
rotation: options.rotation !== undefined ? options.rotation : 0,
scale: options.scale !== undefined ? options.scale : 1,
displacement:
options.displacement !== undefined ? options.displacement : [0, 0],
declutterMode: options.declutterMode,
});
this.canvases_;
this.hitDetectionCanvas_ = null;
this.fill_ = options.fill !== undefined ? options.fill : null;
this.origin_ = [0, 0];
this.points_ = options.points;
this.radius = options.radius;
this.radius2_ = options.radius2;
this.angle_ = options.angle !== undefined ? options.angle : 0;
this.stroke_ = options.stroke !== undefined ? options.stroke : null;
this.size_;
this.renderOptions_;
this.imageState_ =
this.fill_ && this.fill_.loading()
? ImageState.LOADING
: ImageState.LOADED;
if (this.imageState_ === ImageState.LOADING) {
this.ready().then(() => (this.imageState_ = ImageState.LOADED));
}
this.render();
}
clone() {
const scale = this.getScale();
const style = new RegularShape({
fill: this.getFill() ? this.getFill().clone() : undefined,
points: this.getPoints(),
radius: this.getRadius(),
radius2: this.getRadius2(),
angle: this.getAngle(),
stroke: this.getStroke() ? this.getStroke().clone() : undefined,
rotation: this.getRotation(),
rotateWithView: this.getRotateWithView(),
scale: Array.isArray(scale) ? scale.slice() : scale,
displacement: this.getDisplacement().slice(),
declutterMode: this.getDeclutterMode(),
});
style.setOpacity(this.getOpacity());
return style;
}
getAnchor() {
const size = this.size_;
const displacement = this.getDisplacement();
const scale = this.getScaleArray();
return [
size[0] / 2 - displacement[0] / scale[0],
size[1] / 2 + displacement[1] / scale[1],
];
}
getAngle() {
return this.angle_;
}
getFill() {
return this.fill_;
}
setFill(fill) {
this.fill_ = fill;
this.render();
}
getHitDetectionImage() {
if (!this.hitDetectionCanvas_) {
this.hitDetectionCanvas_ = this.createHitDetectionCanvas_(
this.renderOptions_
);
}
return this.hitDetectionCanvas_;
}
getImage(pixelRatio) {
let image = this.canvases_[pixelRatio];
if (!image) {
const renderOptions = this.renderOptions_;
const context = createCanvasContext2D(
renderOptions.size * pixelRatio,
renderOptions.size * pixelRatio
);
this.draw_(renderOptions, context, pixelRatio);
image = context.canvas;
this.canvases_[pixelRatio] = image;
}
return image;
}
getPixelRatio(pixelRatio) {
return pixelRatio;
}
getImageSize() {
return this.size_;
}
getImageState() {
return this.imageState_;
}
getOrigin() {
return this.origin_;
}
getPoints() {
return this.points_;
}
getRadius() {
return this.radius;
}
getRadius2() {
return this.radius2_;
}
getSize() {
return this.size_;
}
getStroke() {
return this.stroke_;
}
setStroke(stroke) {
this.stroke_ = stroke;
this.render();
}
listenImageChange(listener) {}
load() {}
unlistenImageChange(listener) {}
calculateLineJoinSize_(lineJoin, strokeWidth, miterLimit) {
if (
strokeWidth === 0 ||
this.points_ === Infinity ||
(lineJoin !== "bevel" && lineJoin !== "miter")
) {
return strokeWidth;
}
let r1 = this.radius;
let r2 = this.radius2_ === undefined ? r1 : this.radius2_;
if (r1 < r2) {
const tmp = r1;
r1 = r2;
r2 = tmp;
}
const points =
this.radius2_ === undefined ? this.points_ : this.points_ * 2;
const alpha = (2 * Math.PI) / points;
const a = r2 * Math.sin(alpha);
const b = Math.sqrt(r2 * r2 - a * a);
const d = r1 - b;
const e = Math.sqrt(a * a + d * d);
const miterRatio = e / a;
if (lineJoin === "miter" && miterRatio <= miterLimit) {
return miterRatio * strokeWidth;
}
const k = strokeWidth / 2 / miterRatio;
const l = (strokeWidth / 2) * (d / e);
const maxr = Math.sqrt((r1 + k) * (r1 + k) + l * l);
const bevelAdd = maxr - r1;
if (this.radius2_ === undefined || lineJoin === "bevel") {
return bevelAdd * 2;
}
const aa = r1 * Math.sin(alpha);
const bb = Math.sqrt(r1 * r1 - aa * aa);
const dd = r2 - bb;
const ee = Math.sqrt(aa * aa + dd * dd);
const innerMiterRatio = ee / aa;
if (innerMiterRatio <= miterLimit) {
const innerLength = (innerMiterRatio * strokeWidth) / 2 - r2 - r1;
return 2 * Math.max(bevelAdd, innerLength);
}
return bevelAdd * 2;
}
createRenderOptions() {
let lineCap = defaultLineCap;
let lineJoin = defaultLineJoin;
let miterLimit = 0;
let lineDash = null;
let lineDashOffset = 0;
let strokeStyle;
let strokeWidth = 0;
if (this.stroke_) {
strokeStyle = asColorLike(this.stroke_.getColor() ?? defaultStrokeStyle);
strokeWidth = this.stroke_.getWidth() ?? defaultLineWidth;
lineDash = this.stroke_.getLineDash();
lineDashOffset = this.stroke_.getLineDashOffset() ?? 0;
lineJoin = this.stroke_.getLineJoin() ?? defaultLineJoin;
lineCap = this.stroke_.getLineCap() ?? defaultLineCap;
miterLimit = this.stroke_.getMiterLimit() ?? defaultMiterLimit;
}
const add = this.calculateLineJoinSize_(lineJoin, strokeWidth, miterLimit);
const maxRadius = Math.max(this.radius, this.radius2_ || 0);
const size = Math.ceil(2 * maxRadius + add);
return {
strokeStyle: strokeStyle,
strokeWidth: strokeWidth,
size: size,
lineCap: lineCap,
lineDash: lineDash,
lineDashOffset: lineDashOffset,
lineJoin: lineJoin,
miterLimit: miterLimit,
};
}
render() {
this.renderOptions_ = this.createRenderOptions();
const size = this.renderOptions_.size;
this.canvases_ = {};
this.hitDetectionCanvas_ = null;
this.size_ = [size, size];
}
draw_(renderOptions, context, pixelRatio) {
context.scale(pixelRatio, pixelRatio);
context.translate(renderOptions.size / 2, renderOptions.size / 2);
this.createPath_(context);
if (this.fill_) {
let color = this.fill_.getColor();
if (color === null) {
color = defaultFillStyle;
}
context.fillStyle = asColorLike(color);
context.fill();
}
if (renderOptions.strokeStyle) {
context.strokeStyle = renderOptions.strokeStyle;
context.lineWidth = renderOptions.strokeWidth;
if (renderOptions.lineDash) {
context.setLineDash(renderOptions.lineDash);
context.lineDashOffset = renderOptions.lineDashOffset;
}
context.lineCap = renderOptions.lineCap;
context.lineJoin = renderOptions.lineJoin;
context.miterLimit = renderOptions.miterLimit;
context.stroke();
}
}
createHitDetectionCanvas_(renderOptions) {
let context;
if (this.fill_) {
let color = this.fill_.getColor();
let opacity = 0;
if (typeof color === "string") {
color = asArray(color);
}
if (color === null) {
opacity = 1;
} else if (Array.isArray(color)) {
opacity = color.length === 4 ? color[3] : 1;
}
if (opacity === 0) {
context = createCanvasContext2D(renderOptions.size, renderOptions.size);
this.drawHitDetectionCanvas_(renderOptions, context);
}
}
return context ? context.canvas : this.getImage(1);
}
createPath_(context) {
let points = this.points_;
const radius = this.radius;
if (points === Infinity) {
context.arc(0, 0, radius, 0, 2 * Math.PI);
} else {
const radius2 = this.radius2_ === undefined ? radius : this.radius2_;
if (this.radius2_ !== undefined) {
points *= 2;
}
const startAngle = this.angle_ - Math.PI / 2;
const step = (2 * Math.PI) / points;
for (let i = 0; i < points; i++) {
const angle0 = startAngle + i * step;
const radiusC = i % 2 === 0 ? radius : radius2;
context.lineTo(radiusC * Math.cos(angle0), radiusC * Math.sin(angle0));
}
context.closePath();
}
}
drawHitDetectionCanvas_(renderOptions, context) {
// set origin to canvas center
context.translate(renderOptions.size / 2, renderOptions.size / 2);
this.createPath_(context);
context.fillStyle = defaultFillStyle;
context.fill();
if (renderOptions.strokeStyle) {
context.strokeStyle = renderOptions.strokeStyle;
context.lineWidth = renderOptions.strokeWidth;
if (renderOptions.lineDash) {
context.setLineDash(renderOptions.lineDash);
context.lineDashOffset = renderOptions.lineDashOffset;
}
context.lineJoin = renderOptions.lineJoin;
context.miterLimit = renderOptions.miterLimit;
context.stroke();
}
}
ready() {
return this.fill_ ? this.fill_.ready() : Promise.resolve();
}
}
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
# RegularShape
类的构造函数
RegularShape
类的构造函数内部会先调用父类ImageStyle
的构造函数,传入一些样式的基本属性,包括:
opacity
: 不透明度,默认为1
(完全不透明)。rotateWithView
: 是否随着视图旋转,默认值为false
。rotation
: 旋转角度,默认为0
。scale
: 缩放因子,默认为1
(无缩放)。displacement
: 位移量,默认为[0, 0]
。declutterMode
: 一些去重(去杂乱)选项,用于改善图形显示效果。
然后初始化和定义一些特定属性,如下:
this.canvases_
和this.hitDetectionCanvas_
: 用于存储画布和碰撞检测画布。this.fill_
: 填充样式,默认为null
,可以设置为某种颜色或图案。this.origin_
: 形状的原点,默认是[0, 0]
。this.points_
: 形状的点数,用于计算多边形或星形的顶点数。this.radius
: 半径,决定了形状的大小。this.radius2_
: 第二个半径,用于定义星形的内外半径。this.angle_
: 旋转角度,默认为0
。this.stroke_
: 边框样式,默认为null
,可以设置为某种颜色或样式。this.imageState_
: 图像的加载状态。如果填充样式在加载中,状态为LOADING
,否则为LOADED
继续判断,如果图像的状态为loading
,这调用this.ready()
方法,并在其链式后面修改图像状态。
最后调用this.render()
方法。
# RegularShape
类的主要方法
clone()
方法:复制当前RegularShape
样式对象并返回,内部就是实例化RegularShape
类,参数为当前对象的属性,返回实例对象。getAnchor()
方法:获取锚点,内部会调用this.getDisplacement
和this.getScaleArray
方法并进行计算,得到锚点。getAngle()
方法:获取角度this.angle_
getFill()
方法:获取填充样式this.fill_
setFill(fill)
方法:设置填充样式,设置this.fill_
,并调用this.render
方法getHitDetectionImage()
方法:获取点击的图像,内部会先判断,若this.hitDetectionCanvas_
不存在,则调用this.createHitDetectionCanvas_
方法创建点击图像,最后会返回this.hitDetectionCanvas_
getImage(pixelRatio)
方法:根据传入的pixelRatio
获取对应分辨率的图像。如果该分辨率的图像不存在,则创建一个新的图像,绘制当前形状,并将图像缓存起来以供后续使用getPixelRatio(pixelRatio)
方法:获取分辨率getImageSize()
方法:获取图像大小getImageState()
方法:获取图像状态getOrigin()
方法:获取原点getPoints()
方法:获取顶点getRadius()
方法:获取this.radius
getRadius2()
方法:获取this.radius2
getSize()
方法:获取this.size_
getStroke()
方法:获取边框样式setStroke(stroke)
方法:设置边框样式,并调用this.render
方法listenImageChange(listener)
方法:监听图像改变,未实现load()
方法:图像加载,未实现unlistenImageChange(listener)
方法:解除监听,未实现calculateLineJoinSize_(lineJoin,strokeWidth,miterLimit)
方法:是一个用于计算线条连接处(lineJoin
)大小的私有方法。它涉及到根据特定的参数(如lineJoin
类型、strokeWidth
(线宽)、miterLimit
(斜接限制)等)计算出正确的尺寸,通常用于图形渲染时的线条连接效果。createRenderOptions()
方法:创建渲染参数,主要和this.stroke_
属性有关,内部也会调用this.calculateLineJoinSize_
方法计算出连接处的大小render()
方法:内部主要是调用this.createRenderOptions
方法构建渲染参数,并将其返回值赋值给this.renderOptions_
draw_(renderOptions, context, pixelRatio)
方法:draw_
方法负责在给定的context
上绘制一个图形。它会:(1)根据提供的像素比率进行缩放。(2)将原点设置为画布的中心。(3)创建图形的路径。(4)根据renderOptions
中的设置进行填充和描边渲染。填充颜色会根据样式设置,描边颜色、线宽、虚线样式等也会被应用。createHitDetectionCanvas_(renderOptions)
方法:用于根据对象的填充样式(尤其是透明度)来决定是否需要创建一个额外的命中检测画布:(1)如果填充样式是透明的,方法会创建一个新的画布,并在该画布上使用默认填充样式绘制图形,用于命中检测。(2)如果填充样式不透明,方法返回一个默认的图像createPath_(context)
方法:根据当前对象的属性(如points_
、radius
、radius2_
和angle_
等)生成一个路径,并绘制该路径在context
上。它通过判断points_
和radius2_
来绘制不同的几何形状drawHitDetectionCanvas_(renderOptions, context)
方法:用于在画布上绘制一个图形(例如一个多边形或圆形),并进行击中检测,它设置了画布的原点,并绘制了路径、填充颜色、边框样式。ready()
方法:检查fill_
是否准备好,如果没有fill_
,则立即返回一个解决的Promise
,否则返回fill_
的ready
方法返回的Promise
。
# 总结
RegularShape
类是 Openlayers 用来绘制常规形状(如正多边形或星形)的样式类。它接受一系列选项来定义形状的外观,包括填充、边框、旋转角度、大小等。类内部会计算并渲染所需的图形,同时处理样式的加载和显示状态。
此类的作用是为矢量要素(如地图上的点)指定样式,使得地图能够展示不同类型的几何图形。