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控件篇

  • Geom几何图形篇

  • Layer图层篇

  • Renderer篇

  • Feature篇

  • style样式篇

    • Style类
    • Fill类
    • Stroke类
    • Text类
    • ImageStyle类
    • Icon类
    • RegularShape类
      • 概述
      • 源码分析
        • RegularShape类的源码实现
        • RegularShape类的构造函数
        • RegularShape类的主要方法
      • 总结
    • Circle类
    • IconImage类
    • IconImageCache类
  • 《Openlayers源码》笔记
  • style样式篇
东流
2025-01-20
目录

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();
  }
}
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
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 用来绘制常规形状(如正多边形或星形)的样式类。它接受一系列选项来定义形状的外观,包括填充、边框、旋转角度、大小等。类内部会计算并渲染所需的图形,同时处理样式的加载和显示状态。

此类的作用是为矢量要素(如地图上的点)指定样式,使得地图能够展示不同类型的几何图形。

编辑 (opens new window)
上次更新: 2025/01/23, 07:47:27
Icon类
Circle类

← Icon类 Circle类→

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