Coveralls logob
Coveralls logo
  • Home
  • Features
  • Pricing
  • Docs
  • Sign In

uber / deck.gl / 14011

20 Sep 2019 - 23:54 coverage increased (+2.7%) to 82.999%
14011

Pull #3672

travis-ci-com

9181eb84f9c35729a3bad740fb7f9d93?size=18&default=identiconweb-flow
bump loaders.gl version
Pull Request #3672: Fix bugs in pre-bundled version

3393 of 4577 branches covered (74.13%)

Branch coverage included in aggregate %.

7157 of 8134 relevant lines covered (87.99%)

4305.9 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

74.09
/modules/layers/src/icon-layer/icon-manager.js
1
/* global document */
2
import GL from '@luma.gl/constants';
3
import {Texture2D, readPixelsToBuffer} from '@luma.gl/core';
4
import {loadImage} from '@loaders.gl/images';
5
import {createIterable} from '@deck.gl/core';
6

7
const DEFAULT_CANVAS_WIDTH = 1024;
1×
8
const DEFAULT_BUFFER = 4;
8×
9

10
const noop = () => {};
8×
11

12
const DEFAULT_TEXTURE_PARAMETERS = {
7×
13
  [GL.TEXTURE_MIN_FILTER]: GL.LINEAR_MIPMAP_LINEAR,
14
  // GL.LINEAR is the default value but explicitly set it here
15
  [GL.TEXTURE_MAG_FILTER]: GL.LINEAR
16
};
17

18
function nextPowOfTwo(number) {
19
  return Math.pow(2, Math.ceil(Math.log2(number)));
1×
20
}
21

22
// resize image to given width and height
23
function resizeImage(ctx, imageData, width, height) {
24
  const {naturalWidth, naturalHeight} = imageData;
1×
25
  if (width === naturalWidth && height === naturalHeight) {
Branches [[0, 0], [0, 1], [1, 0], [1, 1]] missed. 1×
26
    return imageData;
1×
27
  }
28

29
  ctx.canvas.height = height;
1×
30
  ctx.canvas.width = width;
1×
31

32
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
1×
33

34
  // image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight
35
  ctx.drawImage(imageData, 0, 0, naturalWidth, naturalHeight, 0, 0, width, height);
1×
36

37
  return ctx.canvas;
1×
38
}
39

40
function getIconId(icon) {
41
  return icon && (icon.id || icon.url);
1×
42
}
43

44
// traverse icons in a row of icon atlas
45
// extend each icon with left-top coordinates
46
function buildRowMapping(mapping, columns, yOffset) {
47
  for (let i = 0; i < columns.length; i++) {
1×
48
    const {icon, xOffset} = columns[i];
1×
49
    const id = getIconId(icon);
1×
UNCOV
50
    mapping[id] = Object.assign({}, icon, {
!
51
      x: xOffset,
52
      y: yOffset
53
    });
54
  }
55
}
56

57
// resize texture without losing original data
58
function resizeTexture(texture, width, height) {
59
  const oldWidth = texture.width;
!
60
  const oldHeight = texture.height;
!
61
  const oldPixels = readPixelsToBuffer(texture, {});
!
62

63
  texture.resize({width, height});
!
64

65
  texture.setSubImageData({
!
66
    data: oldPixels,
67
    x: 0,
68
    y: height - oldHeight,
69
    width: oldWidth,
70
    height: oldHeight,
71
    parameters: DEFAULT_TEXTURE_PARAMETERS
72
  });
73

74
  texture.generateMipmap();
!
75

76
  oldPixels.delete();
!
77
  return texture;
14×
78
}
79

80
/**
81
 * Generate coordinate mapping to retrieve icon left-top position from an icon atlas
82
 * @param icons {Array<Object>} list of icons, each icon requires url, width, height
83
 * @param buffer {Number} add buffer to the right and bottom side of the image
84
 * @param xOffset {Number} right position of last icon in old mapping
85
 * @param yOffset {Number} top position in last icon in old mapping
86
 * @param canvasWidth {Number} max width of canvas
87
 * @param mapping {object} old mapping
88
 * @returns {{mapping: {'/icon/1': {url, width, height, ...}},, canvasHeight: {Number}}}
89
 */
90
export function buildMapping({icons, buffer, mapping = {}, xOffset = 0, yOffset = 0, canvasWidth}) {
91
  // height of current row
92
  let rowHeight = 0;
3×
93

94
  let columns = [];
3×
95
  // Strategy to layout all the icons into a texture:
96
  // traverse the icons sequentially, layout the icons from left to right, top to bottom
97
  // when the sum of the icons width is equal or larger than canvasWidth,
98
  // move to next row starting from total height so far plus max height of the icons in previous row
99
  // row width is equal to canvasWidth
100
  // row height is decided by the max height of the icons in that row
101
  // mapping coordinates of each icon is its left-top position in the texture
102
  for (let i = 0; i < icons.length; i++) {
5×
103
    const icon = icons[i];
5×
UNCOV
104
    const id = getIconId(icon);
!
105

UNCOV
106
    if (!mapping[id]) {
Branches [[6, 1]] missed. !
UNCOV
107
      const {height, width} = icon;
!
108

109
      // fill one row
UNCOV
110
      if (xOffset + width + buffer > canvasWidth) {
!
UNCOV
111
        buildRowMapping(mapping, columns, yOffset);
!
112

UNCOV
113
        xOffset = 0;
!
UNCOV
114
        yOffset = rowHeight + yOffset + buffer;
!
UNCOV
115
        rowHeight = 0;
!
116
        columns = [];
1×
117
      }
118

119
      columns.push({
1×
120
        icon,
121
        xOffset
122
      });
123

124
      xOffset = xOffset + width + buffer;
1×
125
      rowHeight = Math.max(rowHeight, height);
1×
126
    }
127
  }
128

129
  if (columns.length > 0) {
Branches [[8, 1]] missed. 5×
130
    buildRowMapping(mapping, columns, yOffset);
5×
131
  }
132

133
  return {
5×
134
    mapping,
135
    xOffset,
136
    yOffset,
137
    canvasWidth,
138
    canvasHeight: nextPowOfTwo(rowHeight + yOffset + buffer)
139
  };
140
}
141

142
// extract icons from data
143
// return icons should be unique, and not cached or cached but url changed
144
export function getDiffIcons(data, getIcon, cachedIcons) {
145
  if (!data || !getIcon) {
Branches [[9, 0]] missed. 5×
146
    return null;
5×
147
  }
148

149
  cachedIcons = cachedIcons || {};
Branches [[11, 1]] missed. 2×
150
  const icons = {};
2×
151
  const {iterable, objectInfo} = createIterable(data);
2×
152
  for (const object of iterable) {
2×
153
    objectInfo.index++;
2×
154
    const icon = getIcon(object, objectInfo);
5×
155
    const id = getIconId(icon);
5×
156

157
    if (!icon) {
Branches [[12, 0]] missed. 5×
158
      throw new Error('Icon is missing.');
1×
159
    }
160

161
    if (!icon.url) {
Branches [[13, 0]] missed. 1×
162
      throw new Error('Icon url is missing.');
1×
163
    }
164

165
    if (!icons[id] && (!cachedIcons[id] || icon.url !== cachedIcons[id].url)) {
2×
UNCOV
166
      icons[id] = icon;
!
167
    }
168
  }
169
  return icons;
2×
170
}
171

172
export default class IconManager {
173
  constructor(
174
    gl,
175
    {
176
      onUpdate = noop // notify IconLayer when icon texture update
Branches [[16, 0]] missed.
177
    }
178
  ) {
179
    this.gl = gl;
2×
180
    this.onUpdate = onUpdate;
2×
181

182
    this._getIcon = null;
2×
183

184
    this._texture = null;
4×
185
    this._externalTexture = null;
4×
186
    this._mapping = {};
4×
187

188
    this._autoPacking = false;
4×
189

190
    // internal props used when autoPacking applied
191
    // right position of last icon
UNCOV
192
    this._xOffset = 0;
!
193
    // top position of last icon
194
    this._yOffset = 0;
4×
UNCOV
195
    this._buffer = DEFAULT_BUFFER;
!
196
    this._canvasWidth = DEFAULT_CANVAS_WIDTH;
4×
197
    this._canvasHeight = 0;
2×
198
    this._canvas = null;
2×
199
  }
200

201
  finalize() {
202
    if (this._texture) {
3×
203
      this._texture.delete();
3×
204
    }
205
  }
206

207
  getTexture() {
208
    return this._texture || this._externalTexture;
3×
209
  }
210

211
  getIconMapping(icon) {
212
    const id = this._autoPacking ? getIconId(icon) : icon;
Branches [[19, 0]] missed. 3×
213
    return this._mapping[id] || {};
Branches [[20, 1]] missed. 3×
214
  }
215

216
  setProps({autoPacking, iconAtlas, iconMapping, data, getIcon}) {
217
    if (autoPacking !== undefined) {
3×
218
      this._autoPacking = autoPacking;
3×
219
    }
220

221
    if (getIcon) {
3×
222
      this._getIcon = getIcon;
3×
223
    }
224

225
    if (iconMapping) {
3×
226
      this._mapping = iconMapping;
3×
227
    }
228

229
    if (iconAtlas) {
3×
230
      this._updateIconAtlas(iconAtlas);
3×
231
    }
232

233
    if (this._autoPacking && (data || getIcon) && typeof document !== 'undefined') {
3×
234
      this._canvas = this._canvas || document.createElement('canvas');
1×
235

236
      this._updateAutoPacking(data);
42×
237
    }
238
  }
239

240
  _updateIconAtlas(iconAtlas) {
241
    if (this._texture) {
Branches [[28, 0]] missed. 524,818×
242
      this._texture.delete();
524,818×
243
      this._texture = null;
22×
244
    }
245
    if (iconAtlas instanceof Texture2D) {
4×
246
      iconAtlas.setParameters(DEFAULT_TEXTURE_PARAMETERS);
22×
247

248
      this._externalTexture = iconAtlas;
15×
249
      this.onUpdate();
22×
250
    } else if (iconAtlas) {
Branches [[30, 1]] missed. 3×
251
      // Browser object: Image, ImageData, HTMLCanvasElement, ImageBitmap
252
      this._texture = new Texture2D(this.gl, {
22×
253
        data: iconAtlas,
254
        parameters: DEFAULT_TEXTURE_PARAMETERS
255
      });
256
      this.onUpdate();
3×
257
    }
258
  }
259

260
  _updateAutoPacking(data) {
261
    const icons = Object.values(getDiffIcons(data, this._getIcon, this._mapping) || {});
Branches [[31, 1]] missed. 22×
262

263
    if (icons.length > 0) {
Branches [[32, 0]] missed. 1×
264
      // generate icon mapping
265
      const {mapping, xOffset, yOffset, canvasHeight} = buildMapping({
1×
266
        icons,
267
        buffer: this._buffer,
268
        canvasWidth: this._canvasWidth,
269
        mapping: this._mapping,
270
        xOffset: this._xOffset,
271
        yOffset: this._yOffset
272
      });
273

274
      this._mapping = mapping;
3×
275
      this._xOffset = xOffset;
!
276
      this._yOffset = yOffset;
!
277
      this._canvasHeight = canvasHeight;
3×
278

279
      // create new texture
280
      if (!this._texture) {
Branches [[33, 0], [33, 1]] missed. 2×
281
        this._texture = new Texture2D(this.gl, {
2×
282
          width: this._canvasWidth,
283
          height: this._canvasHeight,
284
          parameters: DEFAULT_TEXTURE_PARAMETERS
285
        });
286
      }
287

288
      if (this._texture.height !== this._canvasHeight) {
Branches [[34, 0], [34, 1]] missed. 2×
289
        resizeTexture(this._texture, this._canvasWidth, this._canvasHeight);
1×
290
      }
291

292
      this.onUpdate();
1×
293

294
      // load images
295
      this._loadIcons(icons);
1×
296
    }
297
  }
298

299
  _loadIcons(icons) {
300
    const ctx = this._canvas.getContext('2d');
1×
301
    const canvasHeight = this._texture.height;
1×
302

303
    for (const icon of icons) {
!
304
      loadImage(icon.url).then(imageData => {
!
305
        const id = getIconId(icon);
!
306
        const {x, y, width, height} = this._mapping[id];
!
307

308
        const data = resizeImage(ctx, imageData, width, height);
!
309

310
        this._texture.setSubImageData({
!
311
          data,
312
          x,
313
          y: canvasHeight - y - height, // flip Y as texture stored as reversed Y
314
          width,
315
          height,
316
          parameters: Object.assign({}, DEFAULT_TEXTURE_PARAMETERS, {
317
            [GL.UNPACK_FLIP_Y_WEBGL]: true
318
          })
319
        });
320

321
        // Call to regenerate mipmaps after modifying texture(s)
322
        this._texture.generateMipmap();
!
323

324
        this.onUpdate();
!
325
      });
326
    }
327
  }
328
}
Troubleshooting · Open an Issue · Sales · Support · ENTERPRISE · CAREERS · STATUS
BLOG · TWITTER · Legal & Privacy · Supported CI Services · What's a CI service? · Automated Testing

© 2019 Coveralls, LLC