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篇

    • VectorContext类
    • Build类
    • TextBuilder类
    • BuilderGroup类
    • PolygonBuilder类
    • LineStringBuilder类
    • ImageBuilder类
    • renderFeature介绍
    • CanvasLayerRenderer类
      • 概述
      • 源码剖析
        • createTransform方法
        • CanvasLayerRenderer类中的方法
        • CanvasLayerRenderer的完整实现如下
      • 总结
    • LayerRender类
  • Feature篇

  • style样式篇

  • 《Openlayers源码》笔记
  • Renderer篇
东流
2024-12-09
目录

CanvasLayerRenderer类

# 概述

本文主要介绍CanvasLayerRenderer类的实现,该类是创建矢量、瓦片、矢量瓦片、矢量图和静态图像图层渲染器的基类。

# 源码剖析

CanvasLayerRenderer类是继承于LayerRenderer类,其构造函数接受一个参数layer,然后将其传给父类,定义了如下变量:

  • this.container:地图容器
  • this.renderedResolution:渲染后的分辨率
  • this.tempTransform:临时转换函数,通过createTransform()创建
  • pixelTransform:像素转换方法,渲染像素到视口CSS像素的转换。
  • inversePixelTransform:视口CSS像素到渲染像素的转换方法
  • this.context:canvas的上下文
  • deferredContext_:延迟(渲染)的上下文
  • this.containerReused:表示地图元素是否重复利用,初始值为false
  • this.frameState:帧状态

# createTransform方法

createTransform方法本质上就是返回一个一维数组(矩阵),如下所示;

export function create() {
  return [1, 0, 0, 1, 0, 0];
}
1
2
3

# CanvasLayerRenderer类中的方法

  • getImageData方法:通过行列号col、row获取图像中对应位置的数据,主要是通过canvas的getImageData方法实现,这个可以参考

  • getBackground方法:通过参数frameState帧状态获取背景background,返回颜色字符串或者undefined

  • useContainer方法

其实现如下:

 useContainer(target, transform, backgroundColor) {
    const layerClassName = this.getLayer().getClassName();
    let container, context;
    if (
      target &&
      target.className === layerClassName &&
      (!backgroundColor ||
        (target &&
          target.style.backgroundColor &&
          equals(
            asArray(target.style.backgroundColor),
            asArray(backgroundColor),
          )))
    ) {
      const canvas = target.firstElementChild;
      if (canvas instanceof HTMLCanvasElement) {
        context = canvas.getContext('2d');
      }
    }
    if (context && context.canvas.style.transform === transform) {
      // Container of the previous layer renderer can be used.
      this.container = target;
      this.context = context;
      this.containerReused = true;
    } else if (this.containerReused) {
      // Previously reused container cannot be used any more.
      this.container = null;
      this.context = null;
      this.containerReused = false;
    } else if (this.container) {
      this.container.style.backgroundColor = null;
    }
    if (!this.container) {
      container = document.createElement('div');
      container.className = layerClassName;
      let style = container.style;
      style.position = 'absolute';
      style.width = '100%';
      style.height = '100%';
      context = createCanvasContext2D();
      const canvas = context.canvas;
      container.appendChild(canvas);
      style = canvas.style;
      style.position = 'absolute';
      style.left = '0';
      style.transformOrigin = 'top left';
      this.container = container;
      this.context = context;
    }
    if (
      !this.containerReused &&
      backgroundColor &&
      !this.container.style.backgroundColor
    ) {
      this.container.style.backgroundColor = backgroundColor;
    }
  }
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

useContainer方法接受三个参数:target目标容器、transform属性、backgroundColor背景。 useContainer方法就是获取(或者叫设置)渲染容器,先是获取图层的className,如果图层的className和target的className是一致,然后二者的背景色一致,那么就获取target的第一个子元素,如果其元素类型是canvas,则设置变量context的值;然后再根据context判断元素的transform属性值,若它和参数transform相等,则设置this.container、this.context的值,修改this.containerReused为true;若它们的transform不等,则判断this.containerReused,若其值为true,则不渲染该图层,重置this.context等变量;然后判断this.container的值,重置背景. 后面useContainer还进行一个判断,若container不存在,则创建一个DOM元素,设置其样式以及设置this.context等值;最后若参数backgroundColor存在且容器没有渲染过,容器背景样式也不存在,就设置容器的背景样式;

  • clipUnrotated方法:接受三个参数:context上下文,frameState帧状态和extent边界范围;该方法就是对边界范围采用canvas的clip方法进行裁剪范围,在裁剪之前经过两次的坐标转换,两次的transform分别为frameState.coordinateToPixelTransform和this.inversePixelTransform

  • prepareContainer方法:顾名思义就是预设container,接受两个参数frameState、target;获取frameState的范围、分辨率、旋转角度以及像素比,然后通过一些列的矩阵变化计算得到canvasTransform,然后调用useContainer方法,最后设置canvas的宽高

  • dispatchRenderEvent_方法 dispatchRenderEvent方法接受三个参数:type类型、context上下文和frameState帧状态。方法内部先是通过getLayer获取图层,然后判断图层是否注册了type类的监听事件,若注册了,则实例化RenderEvent类,(RenderEvent类就是基于Event类扩展了几个参数)然后调用layer.dispatchEvent方法派发事件,其实现如下:

  dispatchRenderEvent_(type, context, frameState) {
    const layer = this.getLayer();
    if (layer.hasListener(type)) {
      const event = new RenderEvent(
        type,
        this.inversePixelTransform,
        frameState,
        context,
      );
      layer.dispatchEvent(event);
    }
  }
1
2
3
4
5
6
7
8
9
10
11
12
  • preRender和postRender方法:内部都是调用dispatchRenderEvent_进行事件派发
  • renderDeferredInternal方法:未实现,内部渲染延迟方法
  • getRenderContext方法:获取渲染上下文,接受frameState作为参数,若帧状态的declutter存在但是去杂上下文this.deferredContext_不存在,则实例化ZIndexContext类,最后判断帧状态的declutter是否存在,若存在则获取this.deferredContext的上下文,否则返回this.context
  • renderDeferred方法:延迟渲染方法主要用于 Openlayers 的去杂操作
  • getRenderTransform方法:根据中心点、宽高、偏移、像素比等,获取一个transform作用于元素

# CanvasLayerRenderer的完整实现如下

class CanvasLayerRenderer extends LayerRenderer {
  constructor(layer) {
    super(layer);
    this.container = null;
    this.renderedResolution;
    this.tempTransform = createTransform();
    this.inversePixelTransform = createTransform();
    this.context = null;
    this.deferredContext_ = null;
    this.containerReused = false;
    this.frameState = null;
  }

  getImageData(image, col, row) {
    if (!pixelContext) {
      createPixelContext();
    }
    pixelContext.clearRect(0, 0, 1, 1);

    let data;
    try {
      pixelContext.drawImage(image, col, row, 1, 1, 0, 0, 1, 1);
      data = pixelContext.getImageData(0, 0, 1, 1).data;
    } catch (err) {
      pixelContext = null;
      return null;
    }

    return data;
  }

  getBackground(frameState) {
    const layer = this.getLayer();
    let background = layer.getBackground();
    if (typeof background === "function") {
      background = background(frameState.viewState.resolution);
    }
    return background || undefined;
  }

  useContainer(target, transform, backgroundColor) {
    const layerClassName = this.getLayer().getClassName();
    let container, context;
    if (
      target &&
      target.className === layerClassName &&
      (!backgroundColor ||
        (target &&
          target.style.backgroundColor &&
          equals(
            asArray(target.style.backgroundColor),
            asArray(backgroundColor)
          )))
    ) {
      const canvas = target.firstElementChild;
      if (canvas instanceof HTMLCanvasElement) {
        context = canvas.getContext("2d");
      }
    }
    if (context && context.canvas.style.transform === transform) {
      this.container = target;
      this.context = context;
      this.containerReused = true;
    } else if (this.containerReused) {
      this.container = null;
      this.context = null;
      this.containerReused = false;
    } else if (this.container) {
      this.container.style.backgroundColor = null;
    }
    if (!this.container) {
      container = document.createElement("div");
      container.className = layerClassName;
      let style = container.style;
      style.position = "absolute";
      style.width = "100%";
      style.height = "100%";
      context = createCanvasContext2D();
      const canvas = context.canvas;
      container.appendChild(canvas);
      style = canvas.style;
      style.position = "absolute";
      style.left = "0";
      style.transformOrigin = "top left";
      this.container = container;
      this.context = context;
    }
    if (
      !this.containerReused &&
      backgroundColor &&
      !this.container.style.backgroundColor
    ) {
      this.container.style.backgroundColor = backgroundColor;
    }
  }

  clipUnrotated(context, frameState, extent) {
    const topLeft = getTopLeft(extent);
    const topRight = getTopRight(extent);
    const bottomRight = getBottomRight(extent);
    const bottomLeft = getBottomLeft(extent);

    applyTransform(frameState.coordinateToPixelTransform, topLeft);
    applyTransform(frameState.coordinateToPixelTransform, topRight);
    applyTransform(frameState.coordinateToPixelTransform, bottomRight);
    applyTransform(frameState.coordinateToPixelTransform, bottomLeft);

    const inverted = this.inversePixelTransform;
    applyTransform(inverted, topLeft);
    applyTransform(inverted, topRight);
    applyTransform(inverted, bottomRight);
    applyTransform(inverted, bottomLeft);

    context.save();
    context.beginPath();
    context.moveTo(Math.round(topLeft[0]), Math.round(topLeft[1]));
    context.lineTo(Math.round(topRight[0]), Math.round(topRight[1]));
    context.lineTo(Math.round(bottomRight[0]), Math.round(bottomRight[1]));
    context.lineTo(Math.round(bottomLeft[0]), Math.round(bottomLeft[1]));
    context.clip();
  }

  prepareContainer(frameState, target) {
    const extent = frameState.extent;
    const resolution = frameState.viewState.resolution;
    const rotation = frameState.viewState.rotation;
    const pixelRatio = frameState.pixelRatio;
    const width = Math.round((getWidth(extent) / resolution) * pixelRatio);
    const height = Math.round((getHeight(extent) / resolution) * pixelRatio);
    composeTransform(
      this.pixelTransform,
      frameState.size[0] / 2,
      frameState.size[1] / 2,
      1 / pixelRatio,
      1 / pixelRatio,
      rotation,
      -width / 2,
      -height / 2
    );
    makeInverse(this.inversePixelTransform, this.pixelTransform);

    const canvasTransform = toTransformString(this.pixelTransform);
    this.useContainer(target, canvasTransform, this.getBackground(frameState));

    if (!this.containerReused) {
      const canvas = this.context.canvas;
      if (canvas.width != width || canvas.height != height) {
        canvas.width = width;
        canvas.height = height;
      } else {
        this.context.clearRect(0, 0, width, height);
      }
      if (canvasTransform !== canvas.style.transform) {
        canvas.style.transform = canvasTransform;
      }
    }
  }

  dispatchRenderEvent_(type, context, frameState) {
    const layer = this.getLayer();
    if (layer.hasListener(type)) {
      const event = new RenderEvent(
        type,
        this.inversePixelTransform,
        frameState,
        context
      );
      layer.dispatchEvent(event);
    }
  }

  preRender(context, frameState) {
    this.frameState = frameState;
    if (frameState.declutter) {
      return;
    }

    this.dispatchRenderEvent_(RenderEventType.PRERENDER, context, frameState);
  }

  postRender(context, frameState) {
    if (frameState.declutter) {
      return;
    }
    this.dispatchRenderEvent_(RenderEventType.POSTRENDER, context, frameState);
  }

  renderDeferredInternal(frameState) {}
  getRenderContext(frameState) {
    if (frameState.declutter && !this.deferredContext_) {
      this.deferredContext_ = new ZIndexContext();
    }
    return frameState.declutter
      ? this.deferredContext_.getContext()
      : this.context;
  }

  renderDeferred(frameState) {
    if (!frameState.declutter) {
      return;
    }
    this.dispatchRenderEvent_(
      RenderEventType.PRERENDER,
      this.context,
      frameState
    );

    if (frameState.declutter && this.deferredContext_) {
      this.deferredContext_.draw(this.context);
      this.deferredContext_.clear();
    }
    this.renderDeferredInternal(frameState);
    this.dispatchRenderEvent_(
      RenderEventType.POSTRENDER,
      this.context,
      frameState
    );
  }

  getRenderTransform(
    center,
    resolution,
    rotation,
    pixelRatio,
    width,
    height,
    offsetX
  ) {
    const dx1 = width / 2;
    const dy1 = height / 2;
    const sx = pixelRatio / resolution;
    const sy = -sx;
    const dx2 = -center[0] + offsetX;
    const dy2 = -center[1];

    return composeTransform(
      this.tempTransform,
      dx1,
      dy1,
      sx,
      sy,
      -rotation,
      dx2,
      dy2
    );
  }

  disposeInternal() {
    delete this.frameState;
    super.disposeInternal();
  }
}
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

# 总结

本文介绍了CanvasLayerRenderer的一些方法,矩阵的转换计算较为繁琐,暂不深入。

编辑 (opens new window)
上次更新: 2025/01/11, 05:41:02
renderFeature介绍
LayerRender类

← renderFeature介绍 LayerRender类→

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