• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

visgl / deck.gl / 23689953389

28 Mar 2026 05:00PM UTC coverage: 79.878% (-11.0%) from 90.907%
23689953389

push

github

web-flow
chore(test): vitest migration (#9969)

3050 of 3709 branches covered (82.23%)

Branch coverage included in aggregate %.

131 of 169 new or added lines in 11 files covered. (77.51%)

171 existing lines in 60 files now uncovered.

13988 of 17621 relevant lines covered (79.38%)

26917.67 hits per line

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

88.61
/modules/core/src/controllers/map-controller.ts
1
// deck.gl
2
// SPDX-License-Identifier: MIT
3
// Copyright (c) vis.gl contributors
4

5
import {clamp} from '@math.gl/core';
6
import Controller, {ControllerProps, InteractionState} from './controller';
7
import ViewState from './view-state';
8
import {worldToLngLat, lngLatToWorld as _lngLatToWorld} from '@math.gl/web-mercator';
9
import assert from '../utils/assert';
10
import {mod} from '../utils/math-utils';
11

12
import LinearInterpolator from '../transitions/linear-interpolator';
13
import type Viewport from '../viewports/viewport';
14

15
const PITCH_MOUSE_THRESHOLD = 5;
4✔
16
const PITCH_ACCEL = 1.2;
4✔
17
const WEB_MERCATOR_TILE_SIZE = 512;
4✔
18
const WEB_MERCATOR_MAX_BOUNDS = [
4✔
19
  [-Infinity, -90],
20
  [Infinity, 90]
21
] satisfies ControllerProps['maxBounds'];
22

23
/** The web mercator utility `lngLatToWorld` throws if invalid coordinates are provided.
24
 * This wrapper clamps user input to calculate common positions safely. */
25
function lngLatToWorld([lng, lat]: number[]): number[] {
26
  if (Math.abs(lat) > 90) {
1,902✔
27
    lat = Math.sign(lat) * 90;
×
28
  }
29
  if (Number.isFinite(lng)) {
1,902✔
30
    const [x, y] = _lngLatToWorld([lng, lat]);
1,902✔
31
    return [x, clamp(y, 0, WEB_MERCATOR_TILE_SIZE)];
4✔
32
  }
33
  const [, y] = _lngLatToWorld([0, lat]);
1,898✔
34
  return [lng, clamp(y, 0, WEB_MERCATOR_TILE_SIZE)];
1,898✔
35
}
36

37
export type MapStateProps = {
38
  /** Mapbox viewport properties */
39
  /** The width of the viewport */
40
  width: number;
41
  /** The height of the viewport */
42
  height: number;
43
  /** The latitude at the center of the viewport */
44
  latitude: number;
45
  /** The longitude at the center of the viewport */
46
  longitude: number;
47
  /** The tile zoom level of the map. */
48
  zoom: number;
49
  /** The bearing of the viewport in degrees */
50
  bearing?: number;
51
  /** The pitch of the viewport in degrees */
52
  pitch?: number;
53
  /**
54
   * Specify the altitude of the viewport camera
55
   * Unit: map heights, default 1.5
56
   * Non-public API, see https://github.com/mapbox/mapbox-gl-js/issues/1137
57
   */
58
  altitude?: number;
59
  /** Viewport position */
60
  position?: [number, number, number];
61

62
  /** Viewport constraints */
63
  maxZoom?: number;
64
  minZoom?: number;
65
  maxPitch?: number;
66
  minPitch?: number;
67

68
  /** Normalize viewport props to fit map height into viewport. Default `true` */
69
  normalize?: boolean;
70

71
  maxBounds?: ControllerProps['maxBounds'];
72
};
73

74
export type MapStateInternal = {
75
  /** Interaction states, required to calculate change during transform */
76
  /* The point on map being grabbed when the operation first started */
77
  startPanLngLat?: [number, number];
78
  /* Center of the zoom when the operation first started */
79
  startZoomLngLat?: [number, number];
80
  /* Pointer position when rotation started */
81
  startRotatePos?: [number, number];
82
  /* The lng/lat/altitude point at the rotation pivot (where rotation started) */
83
  startRotateLngLat?: [number, number, number];
84
  /** Bearing when current perspective rotate operation started */
85
  startBearing?: number;
86
  /** Pitch when current perspective rotate operation started */
87
  startPitch?: number;
88
  /** Zoom when current zoom operation started */
89
  startZoom?: number;
90
};
91

92
/* Utils */
93

94
export class MapState extends ViewState<MapState, MapStateProps, MapStateInternal> {
95
  /* get optional altitude for rotation pivot
96
   *   - undefined: rotate around viewport center (no pivot point)
97
   *   - 0: rotate around pointer position at ground level
98
   *   - other value: rotate around pointer position at specified altitude
99
   */
100
  getAltitude?: (pos: [number, number]) => number | undefined;
101

102
  constructor(
103
    options: MapStateProps &
104
      MapStateInternal & {
105
        makeViewport: (props: Record<string, any>) => Viewport;
106
        getAltitude?: (pos: [number, number]) => number | undefined;
107
      }
108
  ) {
109
    const {
110
      /** Mapbox viewport properties */
111
      /** The width of the viewport */
112
      width,
113
      /** The height of the viewport */
114
      height,
115
      /** The latitude at the center of the viewport */
116
      latitude,
117
      /** The longitude at the center of the viewport */
118
      longitude,
119
      /** The tile zoom level of the map. */
120
      zoom,
121
      /** The bearing of the viewport in degrees */
122
      bearing = 0,
123
      /** The pitch of the viewport in degrees */
124
      pitch = 0,
125
      /**
126
       * Specify the altitude of the viewport camera
127
       * Unit: map heights, default 1.5
128
       * Non-public API, see https://github.com/mapbox/mapbox-gl-js/issues/1137
129
       */
130
      altitude = 1.5,
131
      /** Viewport position */
132
      position = [0, 0, 0],
133

134
      /** Viewport constraints */
135
      maxZoom = 20,
136
      minZoom = 0,
137
      maxPitch = 60,
138
      minPitch = 0,
139

140
      /** Interaction states, required to calculate change during transform */
141
      /* The point on map being grabbed when the operation first started */
142
      startPanLngLat,
143
      /* Center of the zoom when the operation first started */
144
      startZoomLngLat,
145
      /* Pointer position when rotation started */
146
      startRotatePos,
147
      /* The lng/lat point at the rotation pivot (where rotation started) */
148
      startRotateLngLat,
149
      /** Bearing when current perspective rotate operation started */
150
      startBearing,
151
      /** Pitch when current perspective rotate operation started */
152
      startPitch,
153
      /** Zoom when current zoom operation started */
154
      startZoom,
155

156
      /** Normalize viewport props to fit map height into viewport */
157
      normalize = true
158
    } = options;
580✔
159

160
    assert(Number.isFinite(longitude)); // `longitude` must be supplied
580✔
161
    assert(Number.isFinite(latitude)); // `latitude` must be supplied
580✔
162
    assert(Number.isFinite(zoom)); // `zoom` must be supplied
580✔
163

164
    const maxBounds = options.maxBounds || (normalize ? WEB_MERCATOR_MAX_BOUNDS : null);
580✔
165

166
    super(
580✔
167
      {
168
        width,
169
        height,
170
        latitude,
171
        longitude,
172
        zoom,
173
        bearing,
174
        pitch,
175
        altitude,
176
        maxZoom,
177
        minZoom,
178
        maxPitch,
179
        minPitch,
180
        normalize,
181
        position,
182
        maxBounds
183
      },
184
      {
185
        startPanLngLat,
186
        startZoomLngLat,
187
        startRotatePos,
188
        startRotateLngLat,
189
        startBearing,
190
        startPitch,
191
        startZoom
192
      },
193
      options.makeViewport
194
    );
195

196
    this.getAltitude = options.getAltitude;
580✔
197
  }
198

199
  /**
200
   * Start panning
201
   * @param {[Number, Number]} pos - position on screen where the pointer grabs
202
   */
203
  panStart({pos}: {pos: [number, number]}): MapState {
204
    return this._getUpdatedState({
4✔
205
      startPanLngLat: this._unproject(pos)
206
    });
207
  }
208

209
  /**
210
   * Pan
211
   * @param {[Number, Number]} pos - position on screen where the pointer is
212
   * @param {[Number, Number], optional} startPos - where the pointer grabbed at
213
   *   the start of the operation. Must be supplied of `panStart()` was not called
214
   */
215
  pan({pos, startPos}: {pos: [number, number]; startPos?: [number, number]}): MapState {
216
    const startPanLngLat = this.getState().startPanLngLat || this._unproject(startPos);
11✔
217

218
    if (!startPanLngLat) {
11✔
219
      return this;
×
220
    }
221

222
    const viewport = this.makeViewport(this.getViewportProps());
11✔
223
    const newProps = viewport.panByPosition(startPanLngLat, pos);
11✔
224

225
    return this._getUpdatedState(newProps);
11✔
226
  }
227

228
  /**
229
   * End panning
230
   * Must call if `panStart()` was called
231
   */
232
  panEnd(): MapState {
233
    return this._getUpdatedState({
4✔
234
      startPanLngLat: null
235
    });
236
  }
237

238
  /**
239
   * Start rotating
240
   * @param {[Number, Number]} pos - position on screen where the center is
241
   */
242
  rotateStart({pos}: {pos: [number, number]}): MapState {
243
    const altitude = this.getAltitude?.(pos);
15✔
244

245
    return this._getUpdatedState({
15✔
246
      startRotatePos: pos,
247
      startRotateLngLat: altitude !== undefined ? this._unproject3D(pos, altitude) : undefined,
248
      startBearing: this.getViewportProps().bearing,
249
      startPitch: this.getViewportProps().pitch
250
    });
251
  }
252

253
  /**
254
   * Rotate
255
   * @param {[Number, Number]} pos - position on screen where the center is
256
   */
257
  rotate({
258
    pos,
259
    deltaAngleX = 0,
260
    deltaAngleY = 0
261
  }: {
262
    pos?: [number, number];
263
    deltaAngleX?: number;
264
    deltaAngleY?: number;
265
  }): MapState {
266
    const {startRotatePos, startRotateLngLat, startBearing, startPitch} = this.getState();
8✔
267

268
    if (!startRotatePos || startBearing === undefined || startPitch === undefined) {
8✔
269
      return this;
×
270
    }
271
    let newRotation;
272
    if (pos) {
8✔
273
      newRotation = this._getNewRotation(pos, startRotatePos, startPitch, startBearing);
6✔
274
    } else {
275
      newRotation = {
2✔
276
        bearing: startBearing + deltaAngleX,
277
        pitch: startPitch + deltaAngleY
278
      };
279
    }
280

281
    // If we have a pivot point, adjust the camera position to keep the pivot point fixed
282
    if (startRotateLngLat) {
8✔
283
      const rotatedViewport = this.makeViewport({
×
284
        ...this.getViewportProps(),
285
        ...newRotation
286
      });
287
      // Use panByPosition3D if available (WebMercatorViewport), otherwise fall back to panByPosition
288
      const panMethod = 'panByPosition3D' in rotatedViewport ? 'panByPosition3D' : 'panByPosition';
×
289
      return this._getUpdatedState({
×
290
        ...newRotation,
291
        ...rotatedViewport[panMethod](startRotateLngLat, startRotatePos)
292
      });
293
    }
294

295
    return this._getUpdatedState(newRotation);
8✔
296
  }
297

298
  /**
299
   * End rotating
300
   * Must call if `rotateStart()` was called
301
   */
302
  rotateEnd(): MapState {
303
    return this._getUpdatedState({
14✔
304
      startRotatePos: null,
305
      startRotateLngLat: null,
306
      startBearing: null,
307
      startPitch: null
308
    });
309
  }
310

311
  /**
312
   * Start zooming
313
   * @param {[Number, Number]} pos - position on screen where the center is
314
   */
315
  zoomStart({pos}: {pos: [number, number]}): MapState {
316
    return this._getUpdatedState({
5✔
317
      startZoomLngLat: this._unproject(pos),
318
      startZoom: this.getViewportProps().zoom
319
    });
320
  }
321

322
  /**
323
   * Zoom
324
   * @param {[Number, Number]} pos - position on screen where the current center is
325
   * @param {[Number, Number]} startPos - the center position at
326
   *   the start of the operation. Must be supplied of `zoomStart()` was not called
327
   * @param {Number} scale - a number between [0, 1] specifying the accumulated
328
   *   relative scale.
329
   */
330
  zoom({
331
    pos,
332
    startPos,
333
    scale
334
  }: {
335
    pos: [number, number];
336
    startPos?: [number, number];
337
    scale: number;
338
  }): MapState {
339
    // Make sure we zoom around the current mouse position rather than map center
340
    let {startZoom, startZoomLngLat} = this.getState();
23✔
341

342
    if (!startZoomLngLat) {
23✔
343
      // We have two modes of zoom:
344
      // scroll zoom that are discrete events (transform from the current zoom level),
345
      // and pinch zoom that are continuous events (transform from the zoom level when
346
      // pinch started).
347
      // If startZoom state is defined, then use the startZoom state;
348
      // otherwise assume discrete zooming
349
      startZoom = this.getViewportProps().zoom;
20✔
350
      startZoomLngLat = this._unproject(startPos) || this._unproject(pos);
20✔
351
    }
352
    if (!startZoomLngLat) {
23✔
353
      return this;
×
354
    }
355

356
    const zoom = this._constrainZoom((startZoom as number) + Math.log2(scale));
23✔
357
    const zoomedViewport = this.makeViewport({...this.getViewportProps(), zoom});
23✔
358

359
    return this._getUpdatedState({
23✔
360
      zoom,
361
      ...zoomedViewport.panByPosition(startZoomLngLat, pos)
362
    });
363
  }
364

365
  /**
366
   * End zooming
367
   * Must call if `zoomStart()` was called
368
   */
369
  zoomEnd(): MapState {
370
    return this._getUpdatedState({
5✔
371
      startZoomLngLat: null,
372
      startZoom: null
373
    });
374
  }
375

376
  zoomIn(speed: number = 2): MapState {
377
    return this._zoomFromCenter(speed);
9✔
378
  }
379

380
  zoomOut(speed: number = 2): MapState {
381
    return this._zoomFromCenter(1 / speed);
12✔
382
  }
383

384
  moveLeft(speed: number = 100): MapState {
385
    return this._panFromCenter([speed, 0]);
3✔
386
  }
387

388
  moveRight(speed: number = 100): MapState {
389
    return this._panFromCenter([-speed, 0]);
3✔
390
  }
391

392
  moveUp(speed: number = 100): MapState {
393
    return this._panFromCenter([0, speed]);
3✔
394
  }
395

396
  moveDown(speed: number = 100): MapState {
397
    return this._panFromCenter([0, -speed]);
3✔
398
  }
399

400
  rotateLeft(speed: number = 15): MapState {
401
    return this._getUpdatedState({
3✔
402
      bearing: this.getViewportProps().bearing - speed
403
    });
404
  }
405

406
  rotateRight(speed: number = 15): MapState {
407
    return this._getUpdatedState({
3✔
408
      bearing: this.getViewportProps().bearing + speed
409
    });
410
  }
411

412
  rotateUp(speed: number = 10): MapState {
413
    return this._getUpdatedState({
3✔
414
      pitch: this.getViewportProps().pitch + speed
415
    });
416
  }
417

418
  rotateDown(speed: number = 10): MapState {
419
    return this._getUpdatedState({
3✔
420
      pitch: this.getViewportProps().pitch - speed
421
    });
422
  }
423

424
  shortestPathFrom(viewState: MapState): MapStateProps {
425
    // const endViewStateProps = new this.ControllerState(endProps).shortestPathFrom(startViewstate);
426
    const fromProps = viewState.getViewportProps();
46✔
427
    const props = {...this.getViewportProps()};
46✔
428
    const {bearing, longitude} = props;
46✔
429

430
    if (Math.abs(bearing - fromProps.bearing) > 180) {
46✔
431
      props.bearing = bearing < 0 ? bearing + 360 : bearing - 360;
1✔
432
    }
433
    if (Math.abs(longitude - fromProps.longitude) > 180) {
46✔
434
      props.longitude = longitude < 0 ? longitude + 360 : longitude - 360;
1✔
435
    }
436
    return props;
46✔
437
  }
438

439
  // Apply any constraints (mathematical or defined by _viewportProps) to map state
440
  applyConstraints(props: Required<MapStateProps>): Required<MapStateProps> {
441
    // Ensure pitch is within specified range
442
    const {maxPitch, minPitch, pitch, longitude, bearing, normalize, maxBounds} = props;
465✔
443

444
    if (normalize) {
465✔
445
      if (longitude < -180 || longitude > 180) {
464✔
446
        props.longitude = mod(longitude + 180, 360) - 180;
1✔
447
      }
448
      if (bearing < -180 || bearing > 180) {
464✔
449
        props.bearing = mod(bearing + 180, 360) - 180;
1✔
450
      }
451
    }
452
    props.pitch = clamp(pitch, minPitch, maxPitch);
465✔
453

454
    props.zoom = this._constrainZoom(props.zoom, props);
465✔
455

456
    if (maxBounds) {
465✔
457
      const bl = lngLatToWorld(maxBounds[0]);
464✔
458
      const tr = lngLatToWorld(maxBounds[1]);
464✔
459
      // calculate center and zoom ranges at pitch=0 and bearing=0
460
      // to maintain visual stability when rotating
461
      const scale = 2 ** props.zoom;
464✔
462
      const halfWidth = props.width / 2 / scale;
464✔
463
      const halfHeight = props.height / 2 / scale;
464✔
464
      const [minLng, minLat] = worldToLngLat([bl[0] + halfWidth, bl[1] + halfHeight]);
465✔
465
      const [maxLng, maxLat] = worldToLngLat([tr[0] - halfWidth, tr[1] - halfHeight]);
465✔
466
      props.longitude = clamp(props.longitude, minLng, maxLng);
464✔
467
      props.latitude = clamp(props.latitude, minLat, maxLat);
464✔
468
    }
469

470
    return props;
465✔
471
  }
472

473
  /* Private methods */
474

475
  _constrainZoom(zoom: number, props?: Required<MapStateProps>): number {
476
    props ||= this.getViewportProps();
488✔
477
    const {maxZoom, maxBounds} = props;
488✔
478

479
    const shouldApplyMaxBounds = maxBounds !== null && props.width > 0 && props.height > 0;
488✔
480
    let {minZoom} = props;
488✔
481

482
    if (shouldApplyMaxBounds) {
488✔
483
      const bl = lngLatToWorld(maxBounds[0]);
487✔
484
      const tr = lngLatToWorld(maxBounds[1]);
487✔
485
      const w = tr[0] - bl[0];
487✔
486
      const h = tr[1] - bl[1];
487✔
487
      // ignore bound size of 0 or Infinity
488
      if (Number.isFinite(w) && w > 0) {
487✔
489
        minZoom = Math.max(minZoom, Math.log2(props.width / w));
1✔
490
      }
491
      if (Number.isFinite(h) && h > 0) {
487✔
492
        minZoom = Math.max(minZoom, Math.log2(props.height / h));
487✔
493
      }
494
      if (minZoom > maxZoom) minZoom = maxZoom;
487✔
495
    }
496
    return clamp(zoom, minZoom, maxZoom);
488✔
497
  }
498

499
  _zoomFromCenter(scale) {
500
    const {width, height} = this.getViewportProps();
21✔
501
    return this.zoom({
21✔
502
      pos: [width / 2, height / 2],
503
      scale
504
    });
505
  }
506

507
  _panFromCenter(offset) {
508
    const {width, height} = this.getViewportProps();
12✔
509
    return this.pan({
12✔
510
      startPos: [width / 2, height / 2],
511
      pos: [width / 2 + offset[0], height / 2 + offset[1]]
512
    });
513
  }
514

515
  _getUpdatedState(newProps): MapState {
516
    // @ts-ignore
517
    return new this.constructor({
119✔
518
      makeViewport: this.makeViewport,
519
      ...this.getViewportProps(),
520
      ...this.getState(),
521
      ...newProps
522
    });
523
  }
524

525
  _unproject(pos?: [number, number]): [number, number] | undefined {
526
    const viewport = this.makeViewport(this.getViewportProps());
61✔
527
    // @ts-ignore
528
    return pos && viewport.unproject(pos);
61✔
529
  }
530

531
  _unproject3D(pos: [number, number], altitude: number): [number, number, number] {
532
    const viewport = this.makeViewport(this.getViewportProps());
×
533
    return viewport.unproject(pos, {targetZ: altitude}) as [number, number, number];
×
534
  }
535

536
  _getNewRotation(
537
    pos: [number, number],
538
    startPos: [number, number],
539
    startPitch: number,
540
    startBearing: number
541
  ): {
542
    pitch: number;
543
    bearing: number;
544
  } {
545
    const deltaX = pos[0] - startPos[0];
6✔
546
    const deltaY = pos[1] - startPos[1];
6✔
547
    const centerY = pos[1];
6✔
548
    const startY = startPos[1];
6✔
549
    const {width, height} = this.getViewportProps();
6✔
550

551
    const deltaScaleX = deltaX / width;
6✔
552
    let deltaScaleY = 0;
6✔
553

554
    if (deltaY > 0) {
6✔
555
      if (Math.abs(height - startY) > PITCH_MOUSE_THRESHOLD) {
2✔
556
        // Move from 0 to -1 as we drag upwards
UNCOV
557
        deltaScaleY = (deltaY / (startY - height)) * PITCH_ACCEL;
×
558
      }
559
    } else if (deltaY < 0) {
4✔
560
      if (startY > PITCH_MOUSE_THRESHOLD) {
4✔
561
        // Move from 0 to 1 as we drag upwards
562
        deltaScaleY = 1 - centerY / startY;
4✔
563
      }
564
    }
565
    // clamp deltaScaleY to [-1, 1] so that rotation is constrained between minPitch and maxPitch.
566
    // deltaScaleX does not need to be clamped as bearing does not have constraints.
567
    deltaScaleY = clamp(deltaScaleY, -1, 1);
6✔
568

569
    const {minPitch, maxPitch} = this.getViewportProps();
6✔
570

571
    const bearing = startBearing + 180 * deltaScaleX;
6✔
572
    let pitch = startPitch;
6✔
573
    if (deltaScaleY > 0) {
6✔
574
      // Gradually increase pitch
575
      pitch = startPitch + deltaScaleY * (maxPitch - startPitch);
4✔
576
    } else if (deltaScaleY < 0) {
2✔
577
      // Gradually decrease pitch
UNCOV
578
      pitch = startPitch - deltaScaleY * (minPitch - startPitch);
×
579
    }
580

581
    return {
6✔
582
      pitch,
583
      bearing
584
    };
585
  }
586
}
587

588
export default class MapController extends Controller<MapState> {
51✔
589
  ControllerState = MapState;
51✔
590

591
  transition = {
51✔
592
    transitionDuration: 300,
593
    transitionInterpolator: new LinearInterpolator({
594
      transitionProps: {
595
        compare: ['longitude', 'latitude', 'zoom', 'bearing', 'pitch', 'position'],
596
        required: ['longitude', 'latitude', 'zoom']
597
      }
598
    })
599
  };
600

601
  dragMode: 'pan' | 'rotate' = 'pan';
51✔
602

603
  /**
604
   * Rotation pivot behavior:
605
   * - 'center': Rotate around viewport center (default)
606
   * - '2d': Rotate around pointer position at ground level (z=0)
607
   * - '3d': Rotate around 3D picked point (requires pickPosition callback)
608
   */
609
  protected rotationPivot: 'center' | '2d' | '3d' = 'center';
51✔
610

611
  setProps(
612
    props: ControllerProps &
613
      MapStateProps & {
614
        rotationPivot?: 'center' | '2d' | '3d';
615
        getAltitude?: (pos: [number, number]) => number | undefined;
616
      }
617
  ) {
618
    if ('rotationPivot' in props) {
326✔
619
      this.rotationPivot = props.rotationPivot || 'center';
×
620
    }
621
    // this will be passed to MapState constructor
622
    props.getAltitude = this._getAltitude;
326✔
623
    props.position = props.position || [0, 0, 0];
326✔
624
    props.maxBounds =
326✔
625
      props.maxBounds || (props.normalize === false ? null : WEB_MERCATOR_MAX_BOUNDS);
626

627
    super.setProps(props);
326✔
628
  }
629

630
  protected updateViewport(
631
    newControllerState: MapState,
632
    extraProps: Record<string, any> | null = null,
633
    interactionState: InteractionState = {}
634
  ): void {
635
    // Inject rotation pivot position during rotation for visual feedback
636
    const state = newControllerState.getState();
125✔
637
    if (interactionState.isDragging && state.startRotateLngLat) {
125✔
638
      interactionState = {
×
639
        ...interactionState,
640
        rotationPivotPosition: state.startRotateLngLat
641
      };
642
    } else if (interactionState.isDragging === false) {
125✔
643
      // Clear pivot when drag ends
644
      interactionState = {...interactionState, rotationPivotPosition: undefined};
16✔
645
    }
646

647
    super.updateViewport(newControllerState, extraProps, interactionState);
125✔
648
  }
649

650
  /** Add altitude to rotateStart params based on rotationPivot mode */
651
  protected _getAltitude = (pos: [number, number]): number | undefined => {
51✔
652
    if (this.rotationPivot === '2d') {
8✔
653
      return 0;
×
654
    } else if (this.rotationPivot === '3d') {
8✔
655
      if (this.pickPosition) {
×
656
        const {x, y} = this.props;
×
657
        const pickResult = this.pickPosition(x + pos[0], y + pos[1]);
×
658
        if (pickResult && pickResult.coordinate && pickResult.coordinate.length >= 3) {
×
659
          return pickResult.coordinate[2];
×
660
        }
661
      }
662
    }
663
    return undefined;
8✔
664
  };
665
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc