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

uber / deck.gl / 13030

26 Aug 2019 - 19:27 coverage decreased (-2.6%) to 80.38%
13030

Pull #3490

travis-ci-com

9181eb84f9c35729a3bad740fb7f9d93?size=18&default=identiconweb-flow
integrate mapbox's near plane fix
Pull Request #3490: [MapboxLayer] integrate mapbox-gl's near plane fix

3369 of 4577 branches covered (73.61%)

Branch coverage included in aggregate %.

6877 of 8170 relevant lines covered (84.17%)

4644.76 hits per line

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

3.54
/modules/aggregation-layers/src/heatmap-layer/heatmap-layer.js
1
// Copyright (c) 2015 - 2019 Uber Technologies, Inc.
18×
2
//
3
// Permission is hereby granted, free of charge, to any person obtaining a copy
4
// of this software and associated documentation files (the "Software"), to deal
5
// in the Software without restriction, including without limitation the rights
6
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
// copies of the Software, and to permit persons to whom the Software is
8
// furnished to do so, subject to the following conditions:
9
//
10
// The above copyright notice and this permission notice shall be included in
11
// all copies or substantial portions of the Software.
12
//
13
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
// THE SOFTWARE.
20

21
/* global setTimeout */
22
import GL from '@luma.gl/constants';
23
import {
24
  getBounds,
25
  boundsContain,
26
  packVertices,
27
  scaleToAspectRatio,
28
  getTextureCoordinates
29
} from './heatmap-layer-utils';
30
import {Buffer, Transform, getParameter, isWebGL2} from '@luma.gl/core';
31
import {CompositeLayer, AttributeManager, COORDINATE_SYSTEM, log} from '@deck.gl/core';
32
import TriangleLayer from './triangle-layer';
33
import {getFloatTexture} from '../utils/resource-utils';
34
import {defaultColorRange, colorRangeToFlatArray} from '../utils/color-utils';
35
import weights_vs from './weights-vs.glsl';
36
import weights_fs from './weights-fs.glsl';
37
import vs_max from './max-vs.glsl';
38

39
const RESOLUTION = 2; // (number of common space pixels) / (number texels)
1×
40
const SIZE_2K = 2048;
1×
41
const ZOOM_DEBOUNCE = 500; // milliseconds
1×
42
const TEXTURE_PARAMETERS = {
1×
43
  [GL.TEXTURE_MAG_FILTER]: GL.LINEAR,
44
  [GL.TEXTURE_MIN_FILTER]: GL.LINEAR,
45
  [GL.TEXTURE_WRAP_S]: GL.CLAMP_TO_EDGE,
46
  [GL.TEXTURE_WRAP_T]: GL.CLAMP_TO_EDGE
47
};
48

49
const defaultProps = {
1×
UNCOV
50
  getPosition: {type: 'accessor', value: x => x.position},
!
51
  getWeight: {type: 'accessor', value: 1},
52
  intensity: {type: 'number', min: 0, value: 1},
53
  radiusPixels: {type: 'number', min: 1, max: 100, value: 30},
54
  colorRange: defaultColorRange,
55
  threshold: {type: 'number', min: 0, max: 1, value: 0.05}
56
};
57

58
export default class HeatmapLayer extends CompositeLayer {
59
  initializeState() {
UNCOV
60
    const {gl} = this.context;
!
UNCOV
61
    const textureSize = Math.min(SIZE_2K, getParameter(gl, gl.MAX_TEXTURE_SIZE));
!
UNCOV
62
    this.state = {textureSize, supported: true};
!
UNCOV
63
    if (!isWebGL2(gl)) {
Branches [[0, 0], [0, 1]] missed. !
UNCOV
64
      log.error(`HeatmapLayer ${this.id} is not supported on this browser, requires WebGL2`)();
!
UNCOV
65
      this.setState({supported: false});
!
UNCOV
66
      return;
!
67
    }
UNCOV
68
    this._setupAttributes();
!
UNCOV
69
    this._setupResources();
!
70
  }
71

72
  shouldUpdateState({changeFlags}) {
73
    // Need to be updated when viewport changes
UNCOV
74
    return changeFlags.somethingChanged;
!
75
  }
76

77
  updateState(opts) {
UNCOV
78
    if (!this.state.supported) {
Branches [[1, 0], [1, 1]] missed. !
UNCOV
79
      return;
!
80
    }
UNCOV
81
    super.updateState(opts);
!
UNCOV
82
    const {props, oldProps} = opts;
!
UNCOV
83
    const changeFlags = this._getChangeFlags(opts);
!
84

UNCOV
85
    if (changeFlags.viewportChanged) {
Branches [[2, 0], [2, 1]] missed. !
UNCOV
86
      changeFlags.boundsChanged = this._updateBounds();
!
87
    }
88

UNCOV
89
    if (changeFlags.dataChanged || changeFlags.boundsChanged || changeFlags.uniformsChanged) {
Branches [[3, 0], [3, 1], [4, 0], [4, 1], [4, 2]] missed. !
UNCOV
90
      this._updateWeightmap();
!
UNCOV
91
    } else if (changeFlags.viewportZoomChanged) {
Branches [[5, 0], [5, 1]] missed. !
UNCOV
92
      this._debouncedUpdateWeightmap();
!
93
    }
94

UNCOV
95
    if (props.colorRange !== oldProps.colorRange) {
Branches [[6, 0], [6, 1]] missed. !
UNCOV
96
      this._updateColorTexture(opts);
!
97
    }
98

UNCOV
99
    if (changeFlags.viewportChanged) {
Branches [[7, 0], [7, 1]] missed. !
UNCOV
100
      this._updateTextureRenderingBounds();
!
101
    }
102

UNCOV
103
    this.setState({zoom: opts.context.viewport.zoom});
!
104
  }
105

106
  renderLayers() {
UNCOV
107
    if (!this.state.supported) {
Branches [[8, 0], [8, 1]] missed. !
UNCOV
108
      return [];
!
109
    }
110
    const {
111
      weightsTexture,
112
      triPositionBuffer,
113
      triTexCoordBuffer,
114
      maxWeightsTexture,
115
      colorTexture
UNCOV
116
    } = this.state;
!
UNCOV
117
    const {updateTriggers, intensity, threshold} = this.props;
!
118

UNCOV
119
    return new TriangleLayer(
!
120
      this.getSubLayerProps({
121
        id: `${this.id}-triangle-layer`,
122
        updateTriggers
123
      }),
124
      {
125
        id: 'heatmap-triangle-layer',
126
        data: {
127
          attributes: {
128
            positions: triPositionBuffer,
129
            texCoords: triTexCoordBuffer
130
          }
131
        },
132
        vertexCount: 4,
133
        maxTexture: maxWeightsTexture,
134
        colorTexture,
135
        texture: weightsTexture,
136
        intensity,
137
        threshold
138
      }
139
    );
140
  }
141

142
  finalizeState() {
UNCOV
143
    super.finalizeState();
!
144
    const {
145
      weightsTransform,
146
      weightsTexture,
147
      maxWeightTransform,
148
      maxWeightsTexture,
149
      triPositionBuffer,
150
      triTexCoordBuffer,
151
      colorTexture
152
    } = this.state;
!
153
    /* eslint-disable no-unused-expressions */
154
    weightsTransform && weightsTransform.delete();
Branches [[9, 0], [9, 1]] missed. !
155
    weightsTexture && weightsTexture.delete();
Branches [[10, 0], [10, 1]] missed. !
156
    maxWeightTransform && maxWeightTransform.delete();
Branches [[11, 0], [11, 1]] missed. !
157
    maxWeightsTexture && maxWeightsTexture.delete();
Branches [[12, 0], [12, 1]] missed. !
158
    triPositionBuffer && triPositionBuffer.delete();
Branches [[13, 0], [13, 1]] missed. !
159
    triTexCoordBuffer && triTexCoordBuffer.delete();
Branches [[14, 0], [14, 1]] missed. !
160
    colorTexture && colorTexture.delete();
Branches [[15, 0], [15, 1]] missed. !
161
    /* eslint-enable no-unused-expressions */
162
  }
163

164
  // PRIVATE
165

166
  // override Composite layer private method to create AttributeManager instance
167
  _getAttributeManager() {
168
    return new AttributeManager(this.context.gl, {
!
169
      id: this.props.id,
170
      stats: this.context.stats
171
    });
172
  }
173

174
  _getChangeFlags(opts) {
175
    const {oldProps, props} = opts;
!
176
    const changeFlags = {};
!
177
    if (this._isDataChanged(opts)) {
Branches [[16, 0], [16, 1]] missed. !
178
      changeFlags.dataChanged = true;
!
179
    }
180
    if (oldProps.radiusPixels !== props.radiusPixels) {
Branches [[17, 0], [17, 1]] missed. !
181
      changeFlags.uniformsChanged = true;
!
182
    }
183
    changeFlags.viewportChanged = opts.changeFlags.viewportChanged;
!
184

185
    const {zoom} = this.state;
!
186
    if (!opts.context.viewport || opts.context.viewport.zoom !== zoom) {
Branches [[18, 0], [18, 1], [19, 0], [19, 1]] missed. !
187
      changeFlags.viewportZoomChanged = true;
!
188
    }
189

190
    return changeFlags;
!
191
  }
192

193
  _isDataChanged({changeFlags}) {
194
    if (changeFlags.dataChanged) {
Branches [[20, 0], [20, 1]] missed. !
195
      return true;
!
196
    }
197
    if (
Branches [[21, 0], [21, 1]] missed. !
198
      changeFlags.updateTriggersChanged &&
Branches [[22, 0], [22, 1], [22, 2], [22, 3]] missed.
199
      (changeFlags.updateTriggersChanged.all ||
200
        changeFlags.updateTriggersChanged.getPosition ||
201
        changeFlags.updateTriggersChanged.getWeight)
202
    ) {
203
      return true;
!
204
    }
205
    return false;
!
206
  }
207

208
  _setupAttributes() {
209
    const attributeManager = this.getAttributeManager();
!
210
    attributeManager.add({
!
211
      positions: {size: 3, accessor: 'getPosition'},
212
      weights: {size: 1, accessor: 'getWeight'}
213
    });
214
  }
215

216
  _setupResources() {
217
    const {gl} = this.context;
!
218
    const {textureSize} = this.state;
!
219
    const weightsTexture = getFloatTexture(gl, {
!
220
      width: textureSize,
221
      height: textureSize,
222
      parameters: TEXTURE_PARAMETERS
223
    });
224
    const maxWeightsTexture = getFloatTexture(gl); // 1 X 1 texture
!
225
    const weightsTransform = new Transform(gl, {
!
226
      id: `${this.id}-weights-transform`,
227
      vs: weights_vs,
228
      _fs: weights_fs,
229
      modules: ['project32'],
230
      elementCount: 1,
231
      _targetTexture: weightsTexture,
232
      _targetTextureVarying: 'weightsTexture'
233
    });
234

235
    this.setState({
!
236
      weightsTexture,
237
      maxWeightsTexture,
238
      weightsTransform,
239
      model: weightsTransform.model,
240
      maxWeightTransform: new Transform(gl, {
241
        id: `${this.id}-max-weights-transform`,
242
        _sourceTextures: {
243
          inTexture: weightsTexture
244
        },
245
        _targetTexture: maxWeightsTexture,
246
        _targetTextureVarying: 'outTexture',
247
        vs: vs_max,
248
        elementCount: textureSize * textureSize
249
      }),
250
      zoom: null,
251
      triPositionBuffer: new Buffer(gl, {
252
        byteLength: 48,
253
        accessor: {size: 3}
254
      }),
255
      triTexCoordBuffer: new Buffer(gl, {
256
        byteLength: 48,
257
        accessor: {size: 2}
258
      })
259
    });
260
  }
261

262
  _updateMaxWeightValue() {
263
    const {maxWeightTransform} = this.state;
!
264
    maxWeightTransform.run({
!
265
      parameters: {
266
        blend: true,
267
        depthTest: false,
268
        blendFunc: [GL.ONE, GL.ONE],
269
        blendEquation: GL.MAX
270
      }
271
    });
272
  }
273

274
  // Computes world bounds area that needs to be processed for generate heatmap
275
  _updateBounds(forceUpdate = false) {
Branches [[23, 0]] missed.
276
    const {textureSize} = this.state;
!
277
    const {viewport} = this.context;
!
278

279
    // Unproject all 4 corners of the current screen coordinates into world coordinates (lng/lat)
280
    // Takes care of viewport has non zero bearing/pitch (i.e axis not aligned with world coordiante system)
281
    const viewportCorners = [
!
282
      viewport.unproject([0, 0]),
283
      viewport.unproject([viewport.width, 0]),
284
      viewport.unproject([viewport.width, viewport.height]),
285
      viewport.unproject([0, viewport.height])
286
    ];
287

288
    // #1: get world bounds for current viewport extends
289
    const visibleWorldBounds = getBounds(viewportCorners); // TODO: Change to visible bounds
!
290
    // #2 : convert world bounds to common (Flat) bounds
291
    const visibleCommonBounds = this._worldToCommonBounds(visibleWorldBounds);
!
292

293
    const newState = {visibleWorldBounds, viewportCorners};
!
294
    let boundsChanged = false;
!
295

296
    if (
Branches [[24, 0], [24, 1]] missed. !
297
      forceUpdate ||
Branches [[25, 0], [25, 1], [25, 2]] missed.
298
      !this.state.worldBounds ||
299
      !boundsContain(this.state.worldBounds, visibleWorldBounds)
300
    ) {
301
      // #3: extend common bounds to match aspect ratio with viewport
302
      const scaledCommonBounds = scaleToAspectRatio(
!
303
        visibleCommonBounds,
304
        textureSize * RESOLUTION,
305
        textureSize * RESOLUTION
306
      );
307

308
      // #4 :convert aligned common bounds to world bounds
309
      const worldBounds = this._commonToWorldBounds(scaledCommonBounds);
!
310

311
      // Clip webmercator projection limits
312
      if (this.props.coordinateSystem === COORDINATE_SYSTEM.LNGLAT) {
Branches [[26, 0], [26, 1]] missed. !
313
        worldBounds[1] = Math.max(worldBounds[1], -85.051129);
!
314
        worldBounds[3] = Math.min(worldBounds[3], 85.051129);
!
315
        worldBounds[0] = Math.max(worldBounds[0], -360);
!
316
        worldBounds[2] = Math.min(worldBounds[2], 360);
!
317
      }
318

319
      // #5: now convert world bounds to common using Layer's coordiante system and origin
320
      const normalizedCommonBounds = this._worldToCommonBounds(worldBounds, {
!
321
        scaleToAspect: true,
322
        normalize: true,
323
        width: textureSize * RESOLUTION,
324
        height: textureSize * RESOLUTION
325
      });
326

327
      newState.worldBounds = worldBounds;
!
328
      newState.normalizedCommonBounds = normalizedCommonBounds;
!
329

330
      boundsChanged = true;
!
331
    }
332
    this.setState(newState);
!
333
    return boundsChanged;
!
334
  }
335

336
  _updateTextureRenderingBounds() {
337
    // Just render visible portion of the texture
338
    const {
339
      triPositionBuffer,
340
      triTexCoordBuffer,
341
      normalizedCommonBounds,
342
      viewportCorners
343
    } = this.state;
!
344

345
    const {viewport} = this.context;
!
346
    const commonBounds = normalizedCommonBounds.map(x => x * viewport.scale);
!
347

348
    triPositionBuffer.subData(packVertices(viewportCorners, 3));
!
349

350
    const textureBounds = viewportCorners.map(p =>
!
351
      getTextureCoordinates(viewport.projectPosition(p), commonBounds)
!
352
    );
353
    triTexCoordBuffer.subData(packVertices(textureBounds, 2));
!
354
  }
355

356
  _updateColorTexture(opts) {
357
    const {colorRange} = opts.props;
!
358
    let {colorTexture} = this.state;
!
359
    const colors = colorRangeToFlatArray(colorRange, true);
!
360

361
    if (colorTexture) {
Branches [[27, 0], [27, 1]] missed. !
362
      colorTexture.setImageData({
!
363
        data: colors,
364
        width: colorRange.length
365
      });
366
    } else {
367
      colorTexture = getFloatTexture(this.context.gl, {
!
368
        data: colors,
369
        width: colorRange.length,
370
        parameters: TEXTURE_PARAMETERS
371
      });
372
    }
373
    this.setState({colorTexture});
!
374
  }
375

376
  _updateWeightmap() {
377
    const {radiusPixels} = this.props;
!
378
    const {weightsTransform, worldBounds, textureSize} = this.state;
!
379

380
    // base Layer class doesn't update attributes for composite layers, hence manually trigger it.
381
    this._updateAttributes(this.props);
!
382

383
    const moduleParameters = Object.assign(Object.create(this.props), {
!
384
      viewport: this.context.viewport,
385
      pickingActive: 0
386
    });
387

388
    // #5: convert world bounds to common using Layer's coordiante system and origin
389
    const commonBounds = this._worldToCommonBounds(worldBounds, {
!
390
      useLayerCoordinateSystem: true,
391
      scaleToAspect: true,
392
      width: textureSize * RESOLUTION,
393
      height: textureSize * RESOLUTION
394
    });
395

396
    const uniforms = Object.assign({}, weightsTransform.model.getModuleUniforms(moduleParameters), {
!
397
      radiusPixels,
398
      commonBounds,
399
      textureWidth: textureSize
400
    });
401
    // Attribute manager sets data array count as instaceCount on model
402
    // we need to set that as elementCount on 'weightsTransform'
403
    weightsTransform.update({
!
404
      elementCount: this.getNumInstances()
405
    });
406
    weightsTransform.run({
!
407
      uniforms,
408
      parameters: {
409
        blend: true,
410
        depthTest: false,
411
        blendFunc: [GL.ONE, GL.ONE],
412
        blendEquation: GL.FUNC_ADD
413
      },
414
      clearRenderTarget: true
415
    });
416
    this._updateMaxWeightValue();
!
417

418
    this.setState({lastUpdate: Date.now()});
!
419
  }
420

421
  _debouncedUpdateWeightmap(fromTimer = false) {
Branches [[28, 0]] missed.
422
    let {updateTimer} = this.state;
!
423
    const timeSinceLastUpdate = Date.now() - this.state.lastUpdate;
!
424

425
    if (fromTimer) {
Branches [[29, 0], [29, 1]] missed. !
426
      updateTimer = null;
!
427
    }
428

429
    if (timeSinceLastUpdate >= ZOOM_DEBOUNCE) {
Branches [[30, 0], [30, 1]] missed. !
430
      // update
431
      this._updateBounds(true);
!
432
      this._updateWeightmap();
!
433
      this._updateTextureRenderingBounds();
!
434
    } else if (!updateTimer) {
Branches [[31, 0], [31, 1]] missed. !
435
      updateTimer = setTimeout(
!
436
        this._debouncedUpdateWeightmap.bind(this, true),
437
        ZOOM_DEBOUNCE - timeSinceLastUpdate
438
      );
439
    }
440

441
    this.setState({updateTimer});
!
442
  }
443

444
  // input: worldBounds: [minLong, minLat, maxLong, maxLat]
445
  // input: opts.useLayerCoordinateSystem : layers coordiante system is used
446
  // optput: commonBounds: [minX, minY, maxX, maxY]
447
  _worldToCommonBounds(worldBounds, opts = {}) {
Branches [[32, 0]] missed.
448
    const {useLayerCoordinateSystem = false, scaleToAspect = false, width, height} = opts;
Branches [[33, 0], [34, 0]] missed. !
449
    const [minLong, minLat, maxLong, maxLat] = worldBounds;
!
450
    const {viewport} = this.context;
!
451

452
    let topLeftCommon;
453
    let bottomRightCommon;
454

455
    // Y-axis is flipped between World and Common bounds
456
    if (useLayerCoordinateSystem) {
Branches [[35, 0], [35, 1]] missed. !
457
      topLeftCommon = this.projectPosition([minLong, maxLat, 0]);
!
458
      bottomRightCommon = this.projectPosition([maxLong, minLat, 0]);
!
459
    } else {
460
      topLeftCommon = viewport.projectPosition([minLong, maxLat, 0]);
!
461
      bottomRightCommon = viewport.projectPosition([maxLong, minLat, 0]);
!
462
    }
463
    // Ignore z component
464
    let commonBounds = topLeftCommon.slice(0, 2).concat(bottomRightCommon.slice(0, 2));
!
465
    if (scaleToAspect) {
Branches [[36, 0], [36, 1]] missed. !
466
      commonBounds = scaleToAspectRatio(commonBounds, width, height);
!
467
    }
468
    if (opts.normalize) {
Branches [[37, 0], [37, 1]] missed. !
469
      commonBounds = commonBounds.map(x => x / viewport.scale);
!
470
    }
471
    return commonBounds;
!
472
  }
473

474
  // input commonBounds: [xMin, yMin, xMax, yMax]
475
  // output worldBounds: [minLong, minLat, maxLong, maxLat]
476
  _commonToWorldBounds(commonBounds) {
477
    const [xMin, yMin, xMax, yMax] = commonBounds;
!
478
    const {viewport} = this.context;
!
479
    const topLeftWorld = viewport.unprojectPosition([xMin, yMax]);
!
480
    const bottomRightWorld = viewport.unprojectPosition([xMax, yMin]);
!
481

482
    return topLeftWorld.slice(0, 2).concat(bottomRightWorld.slice(0, 2));
!
483
  }
484
}
485

486
HeatmapLayer.layerName = 'HeatmapLayer';
1×
487
HeatmapLayer.defaultProps = defaultProps;
1×
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