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

uber / deck.gl / 13779

18 Sep 2019 - 0:00 coverage decreased (-2.9%) to 79.902%
13779

Pull #3623

travis-ci-com

9181eb84f9c35729a3bad740fb7f9d93?size=18&default=identiconweb-flow
beta.2
Pull Request #3623: Bump dependency versions

3405 of 4619 branches covered (73.72%)

Branch coverage included in aggregate %.

7031 of 8442 relevant lines covered (83.29%)

5687.45 hits per line

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

67.53
/modules/layers/src/icon-layer/icon-manager.js
1
/* global document */
8×
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;
1×
9

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

12
const DEFAULT_TEXTURE_PARAMETERS = {
1×
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) {
UNCOV
24
  const {naturalWidth, naturalHeight} = imageData;
!
UNCOV
25
  if (width === naturalWidth && height === naturalHeight) {
Branches [[0, 0], [0, 1], [1, 0], [1, 1]] missed. !
UNCOV
26
    return imageData;
!
27
  }
28

UNCOV
29
  ctx.canvas.height = height;
!
UNCOV
30
  ctx.canvas.width = width;
!
31

UNCOV
32
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
!
33

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

UNCOV
37
  return ctx.canvas;
!
38
}
39

40
function getIconId(icon) {
41
  return icon && (icon.id || icon.url);
14×
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++) {
3×
48
    const {icon, xOffset} = columns[i];
5×
49
    const id = getIconId(icon);
5×
50
    mapping[id] = Object.assign({}, icon, {
5×
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();
!
UNCOV
77
  return texture;
!
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;
1×
93

94
  let columns = [];
1×
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++) {
1×
103
    const icon = icons[i];
5×
104
    const id = getIconId(icon);
5×
105

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

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

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

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

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

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

133
  return {
1×
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. 2×
UNCOV
146
    return null;
!
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++;
4×
154
    const icon = getIcon(object, objectInfo);
4×
155
    const id = getIconId(icon);
4×
156

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

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

165
    if (!icons[id] && (!cachedIcons[id] || icon.url !== cachedIcons[id].url)) {
4×
166
      icons[id] = icon;
2×
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;
3×
180
    this.onUpdate = onUpdate;
3×
181

182
    this._getIcon = null;
3×
183

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

188
    this._autoPacking = false;
3×
189

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

UNCOV
292
      this.onUpdate();
!
293

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

299
  _loadIcons(icons) {
UNCOV
300
    const ctx = this._canvas.getContext('2d');
!
UNCOV
301
    const canvasHeight = this._texture.height;
!
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