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

uber / deck.gl / 13873

19 Sep 2019 - 20:02 coverage increased (+2.8%) to 82.702%
13873

Pull #3639

travis-ci-com

9181eb84f9c35729a3bad740fb7f9d93?size=18&default=identiconweb-flow
Update
Pull Request #3639: Set default pydeck notebook width to 700px

3398 of 4611 branches covered (73.69%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

488 existing lines in 85 files now uncovered.

7192 of 8194 relevant lines covered (87.77%)

4273.96 hits per line

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

81.75
/modules/core/src/viewports/viewport.js
1
// Copyright (c) 2015 - 2017 Uber Technologies, Inc.
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
import log from '../utils/log';
22
import {createMat4, extractCameraVectors, getFrustumPlanes} from '../utils/math-utils';
23

24
import {Matrix4, Vector3, equals} from 'math.gl';
25
import * as mat4 from 'gl-matrix/mat4';
26

27
import {
28
  getDistanceScales,
29
  getMeterZoom,
30
  lngLatToWorld,
31
  worldToLngLat,
32
  worldToPixels,
33
  pixelsToWorld
34
} from 'viewport-mercator-project';
35

36
import assert from '../utils/assert';
37

38
const DEGREES_TO_RADIANS = Math.PI / 180;
1×
39

40
const IDENTITY = createMat4();
26×
41

42
const ZERO_VECTOR = [0, 0, 0];
1×
43

44
const DEFAULT_ZOOM = 0;
1×
45

46
const ERR_ARGUMENT = 'Illegal argument to Viewport';
1×
47

48
export default class Viewport {
49
  /**
50
   * @classdesc
51
   * Manages coordinate system transformations for deck.gl.
52
   *
53
   * Note: The Viewport is immutable in the sense that it only has accessors.
54
   * A new viewport instance should be created if any parameters have changed.
55
   */
56
  constructor(opts = {}) {
57
    const {
58
      id = null,
59
      // Window width/height in pixels (for pixel projection)
60
      x = 0,
61
      y = 0,
62
      width = 1,
63
      height = 1
64
    } = opts;
1×
65

66
    this.id = id || this.constructor.displayName || 'viewport';
Branches [[6, 2]] missed. 1×
67

68
    this.x = x;
1×
69
    this.y = y;
1×
70
    // Silently allow apps to send in w,h = 0,0
71
    this.width = width || 1;
1×
72
    this.height = height || 1;
1×
73
    this._frustumPlanes = {};
1×
74

75
    this._initViewMatrix(opts);
1×
76
    this._initProjectionMatrix(opts);
1×
77
    this._initPixelMatrices();
1×
78

79
    // Bind methods for easy access
80
    this.equals = this.equals.bind(this);
1×
81
    this.project = this.project.bind(this);
1×
82
    this.unproject = this.unproject.bind(this);
1×
83
    this.projectPosition = this.projectPosition.bind(this);
1×
84
    this.unprojectPosition = this.unprojectPosition.bind(this);
1×
85
    this.projectFlat = this.projectFlat.bind(this);
1×
86
    this.unprojectFlat = this.unprojectFlat.bind(this);
1×
87
    this.getMatrices = this.getMatrices.bind(this);
1×
88
  }
89

90
  // Two viewports are equal if width and height are identical, and if
91
  // their view and projection matrices are (approximately) equal.
92
  equals(viewport) {
93
    if (!(viewport instanceof Viewport)) {
Branches [[9, 0]] missed. 1×
94
      return false;
1×
95
    }
96

97
    return (
1×
98
      viewport.width === this.width &&
99
      viewport.height === this.height &&
100
      viewport.scale === this.scale &&
101
      equals(viewport.projectionMatrix, this.projectionMatrix) &&
102
      equals(viewport.viewMatrix, this.viewMatrix)
103
    );
104
    // TODO - check distance scales?
105
  }
106

107
  /**
108
   * Projects xyz (possibly latitude and longitude) to pixel coordinates in window
109
   * using viewport projection parameters
110
   * - [longitude, latitude] to [x, y]
111
   * - [longitude, latitude, Z] => [x, y, z]
112
   * Note: By default, returns top-left coordinates for canvas/SVG type render
113
   *
114
   * @param {Array} lngLatZ - [lng, lat] or [lng, lat, Z]
115
   * @param {Object} opts - options
116
   * @param {Object} opts.topLeft=true - Whether projected coords are top left
117
   * @return {Array} - [x, y] or [x, y, z] in top left coords
118
   */
119
  project(xyz, {topLeft = true} = {}) {
120
    const worldPosition = this.projectPosition(xyz);
1×
121
    const coord = worldToPixels(worldPosition, this.pixelProjectionMatrix);
1×
122

123
    const [x, y] = coord;
147×
124
    const y2 = topLeft ? y : this.height - y;
Branches [[13, 1]] missed. 147×
125
    return xyz.length === 2 ? [x, y2] : [x, y2, coord[2]];
147×
126
  }
127

128
  /**
129
   * Unproject pixel coordinates on screen onto world coordinates,
130
   * (possibly [lon, lat]) on map.
131
   * - [x, y] => [lng, lat]
132
   * - [x, y, z] => [lng, lat, Z]
133
   * @param {Array} xyz -
134
   * @param {Object} opts - options
135
   * @param {Object} opts.topLeft=true - Whether origin is top left
136
   * @return {Array|null} - [lng, lat, Z] or [X, Y, Z]
137
   */
138
  unproject(xyz, {topLeft = true, targetZ} = {}) {
139
    const [x, y, z] = xyz;
147×
140

141
    const y2 = topLeft ? y : this.height - y;
Branches [[17, 1]] missed. 147×
142
    const targetZWorld = targetZ && targetZ * this.distanceScales.pixelsPerMeter[2];
Branches [[18, 1]] missed. 147×
143
    const coord = pixelsToWorld([x, y2, z], this.pixelUnprojectionMatrix, targetZWorld);
147×
144
    const [X, Y, Z] = this.unprojectPosition(coord);
147×
145

146
    if (Number.isFinite(z)) {
147×
147
      return [X, Y, Z];
147×
148
    }
149
    return Number.isFinite(targetZ) ? [X, Y, targetZ] : [X, Y];
Branches [[20, 0]] missed. 147×
150
  }
151

152
  // NON_LINEAR PROJECTION HOOKS
153
  // Used for web meractor projection
154

155
  projectPosition(xyz) {
156
    const [X, Y] = this.projectFlat(xyz);
147×
157
    const Z = (xyz[2] || 0) * this.distanceScales.pixelsPerMeter[2];
147×
158
    return [X, Y, Z];
147×
159
  }
160

161
  unprojectPosition(xyz) {
162
    const [X, Y] = this.unprojectFlat(xyz);
147×
163
    const Z = (xyz[2] || 0) * this.distanceScales.metersPerPixel[2];
147×
164
    return [X, Y, Z];
147×
165
  }
166

167
  /**
168
   * Project [lng,lat] on sphere onto [x,y] on 512*512 Mercator Zoom 0 tile.
169
   * Performs the nonlinear part of the web mercator projection.
170
   * Remaining projection is done with 4x4 matrices which also handles
171
   * perspective.
172
   * @param {Array} lngLat - [lng, lat] coordinates
173
   *   Specifies a point on the sphere to project onto the map.
174
   * @return {Array} [x,y] coordinates.
175
   */
176
  projectFlat(xyz, scale = this.scale) {
177
    if (this.isGeospatial) {
147×
178
      return lngLatToWorld(xyz, scale);
615×
179
    }
UNCOV
180
    const {pixelsPerMeter} = this.distanceScales;
!
181
    return [xyz[0] * pixelsPerMeter[0], xyz[1] * pixelsPerMeter[1]];
615×
182
  }
183

184
  /**
185
   * Unproject world point [x,y] on map onto {lat, lon} on sphere
186
   * @param {object|Vector} xy - object with {x,y} members
187
   *  representing point on projected map plane
188
   * @return {GeoCoordinates} - object with {lat,lon} of point on sphere.
189
   *   Has toArray method if you need a GeoJSON Array.
190
   *   Per cartographic tradition, lat and lon are specified as degrees.
191
   */
192
  unprojectFlat(xyz, scale = this.scale) {
193
    if (this.isGeospatial) {
12,683×
194
      return worldToLngLat(xyz, scale);
12,683×
195
    }
196
    const {metersPerPixel} = this.distanceScales;
12,683×
197
    return [xyz[0] * metersPerPixel[0], xyz[1] * metersPerPixel[1]];
12,683×
198
  }
199

200
  getDistanceScales(coordinateOrigin = null) {
201
    if (coordinateOrigin) {
12,683×
202
      return getDistanceScales({
114×
203
        longitude: coordinateOrigin[0],
204
        latitude: coordinateOrigin[1],
205
        scale: this.scale,
206
        highPrecision: true
207
      });
208
    }
209
    return this.distanceScales;
114×
210
  }
211

212
  getMatrices({modelMatrix = null} = {}) {
Branches [[29, 0], [30, 0]] missed.
213
    let modelViewProjectionMatrix = this.viewProjectionMatrix;
114×
214
    let pixelProjectionMatrix = this.pixelProjectionMatrix;
114×
215
    let pixelUnprojectionMatrix = this.pixelUnprojectionMatrix;
114×
216

217
    if (modelMatrix) {
Branches [[31, 0], [31, 1]] missed. 114×
218
      modelViewProjectionMatrix = mat4.multiply([], this.viewProjectionMatrix, modelMatrix);
28×
219
      pixelProjectionMatrix = mat4.multiply([], this.pixelProjectionMatrix, modelMatrix);
86×
220
      pixelUnprojectionMatrix = mat4.invert([], pixelProjectionMatrix);
12,735×
221
    }
222

223
    const matrices = Object.assign({
12,735×
224
      modelViewProjectionMatrix,
225
      viewProjectionMatrix: this.viewProjectionMatrix,
226
      viewMatrix: this.viewMatrix,
227
      projectionMatrix: this.projectionMatrix,
228

229
      // project/unproject between pixels and world
230
      pixelProjectionMatrix,
231
      pixelUnprojectionMatrix,
232

233
      width: this.width,
234
      height: this.height,
235
      scale: this.scale
236
    });
237

238
    return matrices;
12,735×
239
  }
240

241
  containsPixel({x, y, width = 1, height = 1}) {
242
    return (
121×
243
      x < this.x + this.width &&
244
      this.x < x + width &&
245
      y < this.y + this.height &&
246
      this.y < y + height
247
    );
248
  }
249

250
  // Extract frustum planes in common space
251
  getFrustumPlanes() {
252
    if (this._frustumPlanes.near) {
121×
253
      return this._frustumPlanes;
121×
254
    }
255

256
    const {near, far, fovyRadians, aspect} = this.projectionProps;
28,029×
257

258
    Object.assign(
27,972×
259
      this._frustumPlanes,
260
      getFrustumPlanes({
261
        aspect,
262
        near,
263
        far,
264
        fovyRadians,
265
        position: this.cameraPosition,
266
        direction: this.cameraDirection,
267
        up: this.cameraUp,
268
        right: this.cameraRight
269
      })
270
    );
271

272
    return this._frustumPlanes;
57×
273
  }
274

275
  // EXPERIMENTAL METHODS
276

277
  getCameraPosition() {
278
    return this.cameraPosition;
57×
279
  }
280

281
  getCameraDirection() {
282
    return this.cameraDirection;
1,280×
283
  }
284

285
  getCameraUp() {
286
    return this.cameraUp;
1,264×
287
  }
288

289
  // INTERNAL METHODS
290

291
  // TODO - these are duplicating WebMercator methods
292
  _addMetersToLngLat(lngLatZ, xyz) {
293
    const [lng, lat, Z = 0] = lngLatZ;
Branches [[36, 0]] missed. 16×
294
    const [deltaLng, deltaLat, deltaZ = 0] = this._metersToLngLatDelta(xyz);
Branches [[37, 0]] missed. 16×
295
    return lngLatZ.length === 2
Branches [[38, 0], [38, 1]] missed. 86×
296
      ? [lng + deltaLng, lat + deltaLat]
297
      : [lng + deltaLng, lat + deltaLat, Z + deltaZ];
298
  }
299

300
  _metersToLngLatDelta(xyz) {
301
    const [x, y, z = 0] = xyz;
Branches [[39, 0]] missed. 28×
302
    assert(Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z), ERR_ARGUMENT);
Branches [[40, 0], [40, 1], [40, 2]] missed. 58×
303
    const {pixelsPerMeter, degreesPerPixel} = this.distanceScales;
!
304
    const deltaLng = x * pixelsPerMeter[0] * degreesPerPixel[0];
!
305
    const deltaLat = y * pixelsPerMeter[1] * degreesPerPixel[1];
!
306
    return xyz.length === 2 ? [deltaLng, deltaLat] : [deltaLng, deltaLat, z];
Branches [[41, 0], [41, 1]] missed. !
307
  }
308

309
  _createProjectionMatrix({orthographic, fovyRadians, aspect, focalDistance, near, far}) {
UNCOV
310
    assert(Number.isFinite(fovyRadians));
!
UNCOV
311
    return orthographic
Branches [[42, 0]] missed. !
312
      ? new Matrix4().orthographic({fovy: fovyRadians, aspect, focalDistance, near, far})
313
      : new Matrix4().perspective({fovy: fovyRadians, aspect, near, far});
314
  }
315

316
  /* eslint-disable complexity, max-statements */
317
  _initViewMatrix(opts) {
318
    const {
319
      // view matrix
320
      viewMatrix = IDENTITY,
321

322
      longitude = null, // Anchor: lng lat zoom makes viewport work w/ geospatial coordinate systems
323
      latitude = null,
324
      zoom = null,
325

326
      position = null, // Anchor position offset (in meters for geospatial viewports)
327
      modelMatrix = null, // A model matrix to be applied to position, to match the layer props API
328
      focalDistance = 1, // Only needed for orthographic views
329

330
      distanceScales = null
UNCOV
331
    } = opts;
!
332

333
    // Check if we have a geospatial anchor
UNCOV
334
    this.isGeospatial = Number.isFinite(latitude) && Number.isFinite(longitude);
!
335

UNCOV
336
    this.zoom = zoom;
!
337
    if (!Number.isFinite(this.zoom)) {
25×
338
      this.zoom = this.isGeospatial
7×
339
        ? getMeterZoom({latitude}) + Math.log2(focalDistance)
340
        : DEFAULT_ZOOM;
341
    }
342
    const scale = Math.pow(2, this.zoom);
3×
343
    this.scale = scale;
4×
344

345
    // Calculate distance scales if lng/lat/zoom are provided
346
    this.distanceScales = this.isGeospatial
4×
347
      ? getDistanceScales({latitude, longitude, scale: this.scale})
348
      : distanceScales || {
349
          pixelsPerMeter: [scale, scale, scale],
350
          metersPerPixel: [1 / scale, 1 / scale, 1 / scale]
351
        };
352

353
    this.focalDistance = focalDistance;
4×
354

UNCOV
355
    this.distanceScales.metersPerPixel = new Vector3(this.distanceScales.metersPerPixel);
!
UNCOV
356
    this.distanceScales.pixelsPerMeter = new Vector3(this.distanceScales.pixelsPerMeter);
!
357

UNCOV
358
    this.position = ZERO_VECTOR;
!
UNCOV
359
    this.meterOffset = ZERO_VECTOR;
!
UNCOV
360
    if (position) {
!
361
      // Apply model matrix if supplied
UNCOV
362
      this.position = position;
!
UNCOV
363
      this.modelMatrix = modelMatrix;
!
UNCOV
364
      this.meterOffset = modelMatrix ? modelMatrix.transformVector(position) : position;
Branches [[57, 0]] missed. !
365
    }
366

UNCOV
367
    if (this.isGeospatial) {
!
368
      // Determine camera center
UNCOV
369
      this.longitude = longitude;
!
UNCOV
370
      this.latitude = latitude;
!
UNCOV
371
      this.center = this._getCenterInWorld({longitude, latitude});
!
372

373
      // Flip Y to match the orientation of the Mercator plane
374
      this.viewMatrixUncentered = mat4.scale([], viewMatrix, [1, -1, 1]);
122×
375
    } else {
376
      this.center = position ? this.projectPosition(position) : [0, 0, 0];
122×
377
      this.viewMatrixUncentered = viewMatrix;
147×
378
    }
379
    // Make a centered version of the matrix for projection modes without an offset
380
    this.viewMatrix = new Matrix4()
147×
381
      // Apply the uncentered view matrix
382
      .multiplyRight(this.viewMatrixUncentered)
383
      // And center it
384
      .translate(new Vector3(this.center || ZERO_VECTOR).negate());
Branches [[60, 1]] missed.
385
  }
386
  /* eslint-enable complexity, max-statements */
387

388
  _getCenterInWorld({longitude, latitude}) {
389
    const {meterOffset, scale, distanceScales} = this;
147×
390

391
    // Make a centered version of the matrix for projection modes without an offset
392
    const center2d = this.projectFlat([longitude, latitude], scale);
147×
393
    const center = new Vector3(center2d[0], center2d[1], 0);
17×
394

395
    if (meterOffset) {
Branches [[61, 1]] missed. 147×
396
      const pixelPosition = new Vector3(meterOffset)
147×
397
        // Convert to pixels in current zoom
398
        .scale(distanceScales.pixelsPerMeter);
399
      center.add(pixelPosition);
147×
400
    }
401

402
    return center;
147×
403
  }
404

405
  _initProjectionMatrix(opts) {
406
    const {
407
      // Projection matrix
408
      projectionMatrix = null,
409

410
      // Projection matrix parameters, used if projectionMatrix not supplied
411
      orthographic = false,
412
      fovyRadians,
413
      fovyDegrees,
414
      fovy,
415
      near = 0.1, // Distance of near clipping plane
416
      far = 1000, // Distance of far clipping plane
417
      focalDistance = 1, // Only needed for orthographic views
418
      orthographicFocalDistance
419
    } = opts;
147×
420

421
    const radians = fovyRadians || (fovyDegrees || fovy || 75) * DEGREES_TO_RADIANS;
147×
422

423
    this.projectionProps = {
147×
424
      orthographic,
425
      fovyRadians: radians,
426
      aspect: this.width / this.height,
427
      focalDistance: orthographicFocalDistance || focalDistance,
428
      near,
429
      far
430
    };
431

432
    this.projectionMatrix = projectionMatrix || this._createProjectionMatrix(this.projectionProps);
147×
433
  }
434

435
  _initPixelMatrices() {
436
    // Note: As usual, matrix operations should be applied in "reverse" order
437
    // since vectors will be multiplied in from the right during transformation
438
    const vpm = createMat4();
147×
439
    mat4.multiply(vpm, vpm, this.projectionMatrix);
40×
440
    mat4.multiply(vpm, vpm, this.viewMatrix);
40×
441
    this.viewProjectionMatrix = vpm;
40×
442

443
    // console.log('VPM', this.viewMatrix, this.projectionMatrix, this.viewProjectionMatrix);
444

445
    // Calculate inverse view matrix
446
    this.viewMatrixInverse = mat4.invert([], this.viewMatrix) || this.viewMatrix;
Branches [[71, 1]] missed. 147×
447

448
    // Decompose camera directions
449
    const {eye, direction, up, right} = extractCameraVectors({
107×
450
      viewMatrix: this.viewMatrix,
451
      viewMatrixInverse: this.viewMatrixInverse
452
    });
453
    this.cameraPosition = eye;
107×
454
    this.cameraDirection = direction;
107×
455
    this.cameraUp = up;
107×
456
    this.cameraRight = right;
40×
457

458
    // console.log(this.cameraPosition, this.cameraDirection, this.cameraUp);
459

460
    /*
461
     * Builds matrices that converts preprojected lngLats to screen pixels
462
     * and vice versa.
463
     * Note: Currently returns bottom-left coordinates!
464
     * Note: Starts with the GL projection matrix and adds steps to the
465
     *       scale and translate that matrix onto the window.
466
     * Note: WebGL controls clip space to screen projection with gl.viewport
467
     *       and does not need this step.
468
     */
469

470
    // matrix for conversion from world location to screen (pixel) coordinates
471
    const viewportMatrix = createMat4(); // matrix from NDC to viewport.
40×
472
    const pixelProjectionMatrix = createMat4(); // matrix from world space to viewport.
147×
473
    mat4.scale(viewportMatrix, viewportMatrix, [this.width / 2, -this.height / 2, 1]);
107×
474
    mat4.translate(viewportMatrix, viewportMatrix, [1, -1, 0]);
107×
475
    mat4.multiply(pixelProjectionMatrix, viewportMatrix, this.viewProjectionMatrix);
107×
476
    this.pixelProjectionMatrix = pixelProjectionMatrix;
107×
477
    this.viewportMatrix = viewportMatrix;
107×
478

479
    this.pixelUnprojectionMatrix = mat4.invert(createMat4(), this.pixelProjectionMatrix);
107×
480
    if (!this.pixelUnprojectionMatrix) {
Branches [[72, 0]] missed. 107×
481
      log.warn('Pixel project matrix not invertible')();
147×
482
      // throw new Error('Pixel project matrix not invertible');
483
    }
484
  }
485
}
486

487
Viewport.displayName = 'Viewport';
147×
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