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

visgl / deck.gl / 22499841084

27 Feb 2026 07:02PM UTC coverage: 91.033% (-0.05%) from 91.082%
22499841084

push

github

web-flow
chore: fix website build (#10049)

* fix website build

* fix(examples): update collision-filter example for turf v7

Replace deprecated `lineDistance` with `length` to fix breaking change
from @turf/turf v7 upgrade. Also update example's package.json to use
turf v7 for consistency.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Chris Gervang <chrisgervang@users.noreply.github.com>
Co-authored-by: Chris Gervang <chris.gervang@joby.aero>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

6941 of 7645 branches covered (90.79%)

Branch coverage included in aggregate %.

57208 of 62823 relevant lines covered (91.06%)

14281.77 hits per line

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

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

1✔
5
import {clamp} from '@math.gl/core';
1✔
6
import Controller, {ControllerProps, InteractionState} from './controller';
1✔
7
import ViewState from './view-state';
1✔
8
import {normalizeViewportProps} from '@math.gl/web-mercator';
1✔
9
import assert from '../utils/assert';
1✔
10

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

1✔
14
const PITCH_MOUSE_THRESHOLD = 5;
1✔
15
const PITCH_ACCEL = 1.2;
1✔
16

1✔
17
export type MapStateProps = {
1✔
18
  /** Mapbox viewport properties */
1✔
19
  /** The width of the viewport */
1✔
20
  width: number;
1✔
21
  /** The height of the viewport */
1✔
22
  height: number;
1✔
23
  /** The latitude at the center of the viewport */
1✔
24
  latitude: number;
1✔
25
  /** The longitude at the center of the viewport */
1✔
26
  longitude: number;
1✔
27
  /** The tile zoom level of the map. */
1✔
28
  zoom: number;
1✔
29
  /** The bearing of the viewport in degrees */
1✔
30
  bearing?: number;
1✔
31
  /** The pitch of the viewport in degrees */
1✔
32
  pitch?: number;
1✔
33
  /**
1✔
34
   * Specify the altitude of the viewport camera
1✔
35
   * Unit: map heights, default 1.5
1✔
36
   * Non-public API, see https://github.com/mapbox/mapbox-gl-js/issues/1137
1✔
37
   */
1✔
38
  altitude?: number;
1✔
39
  /** Viewport position */
1✔
40
  position?: [number, number, number];
1✔
41

1✔
42
  /** Viewport constraints */
1✔
43
  maxZoom?: number;
1✔
44
  minZoom?: number;
1✔
45
  maxPitch?: number;
1✔
46
  minPitch?: number;
1✔
47

1✔
48
  /** Normalize viewport props to fit map height into viewport. Default `true` */
1✔
49
  normalize?: boolean;
1✔
50
};
1✔
51

1✔
52
export type MapStateInternal = {
1✔
53
  /** Interaction states, required to calculate change during transform */
1✔
54
  /* The point on map being grabbed when the operation first started */
1✔
55
  startPanLngLat?: [number, number];
1✔
56
  /* Center of the zoom when the operation first started */
1✔
57
  startZoomLngLat?: [number, number];
1✔
58
  /* Pointer position when rotation started */
1✔
59
  startRotatePos?: [number, number];
1✔
60
  /* The lng/lat/altitude point at the rotation pivot (where rotation started) */
1✔
61
  startRotateLngLat?: [number, number, number];
1✔
62
  /** Bearing when current perspective rotate operation started */
1✔
63
  startBearing?: number;
1✔
64
  /** Pitch when current perspective rotate operation started */
1✔
65
  startPitch?: number;
1✔
66
  /** Zoom when current zoom operation started */
1✔
67
  startZoom?: number;
1✔
68
};
1✔
69

1✔
70
/* Utils */
1✔
71

1✔
72
export class MapState extends ViewState<MapState, MapStateProps, MapStateInternal> {
1✔
73
  makeViewport: (props: Record<string, any>) => Viewport;
1✔
74

1✔
75
  constructor(
1✔
76
    options: MapStateProps &
679✔
77
      MapStateInternal & {
679✔
78
        makeViewport: (props: Record<string, any>) => Viewport;
679✔
79
      }
679✔
80
  ) {
679✔
81
    const {
679✔
82
      /** Mapbox viewport properties */
679✔
83
      /** The width of the viewport */
679✔
84
      width,
679✔
85
      /** The height of the viewport */
679✔
86
      height,
679✔
87
      /** The latitude at the center of the viewport */
679✔
88
      latitude,
679✔
89
      /** The longitude at the center of the viewport */
679✔
90
      longitude,
679✔
91
      /** The tile zoom level of the map. */
679✔
92
      zoom,
679✔
93
      /** The bearing of the viewport in degrees */
679✔
94
      bearing = 0,
679✔
95
      /** The pitch of the viewport in degrees */
679✔
96
      pitch = 0,
679✔
97
      /**
679✔
98
       * Specify the altitude of the viewport camera
679✔
99
       * Unit: map heights, default 1.5
679✔
100
       * Non-public API, see https://github.com/mapbox/mapbox-gl-js/issues/1137
679✔
101
       */
679✔
102
      altitude = 1.5,
679✔
103
      /** Viewport position */
679✔
104
      position = [0, 0, 0],
679✔
105

679✔
106
      /** Viewport constraints */
679✔
107
      maxZoom = 20,
679✔
108
      minZoom = 0,
679✔
109
      maxPitch = 60,
679✔
110
      minPitch = 0,
679✔
111

679✔
112
      /** Interaction states, required to calculate change during transform */
679✔
113
      /* The point on map being grabbed when the operation first started */
679✔
114
      startPanLngLat,
679✔
115
      /* Center of the zoom when the operation first started */
679✔
116
      startZoomLngLat,
679✔
117
      /* Pointer position when rotation started */
679✔
118
      startRotatePos,
679✔
119
      /* The lng/lat point at the rotation pivot (where rotation started) */
679✔
120
      startRotateLngLat,
679✔
121
      /** Bearing when current perspective rotate operation started */
679✔
122
      startBearing,
679✔
123
      /** Pitch when current perspective rotate operation started */
679✔
124
      startPitch,
679✔
125
      /** Zoom when current zoom operation started */
679✔
126
      startZoom,
679✔
127

679✔
128
      /** Normalize viewport props to fit map height into viewport */
679✔
129
      normalize = true
679✔
130
    } = options;
679✔
131

679✔
132
    assert(Number.isFinite(longitude)); // `longitude` must be supplied
679✔
133
    assert(Number.isFinite(latitude)); // `latitude` must be supplied
679✔
134
    assert(Number.isFinite(zoom)); // `zoom` must be supplied
679✔
135

679✔
136
    super(
679✔
137
      {
679✔
138
        width,
679✔
139
        height,
679✔
140
        latitude,
679✔
141
        longitude,
679✔
142
        zoom,
679✔
143
        bearing,
679✔
144
        pitch,
679✔
145
        altitude,
679✔
146
        maxZoom,
679✔
147
        minZoom,
679✔
148
        maxPitch,
679✔
149
        minPitch,
679✔
150
        normalize,
679✔
151
        position
679✔
152
      },
679✔
153
      {
679✔
154
        startPanLngLat,
679✔
155
        startZoomLngLat,
679✔
156
        startRotatePos,
679✔
157
        startRotateLngLat,
679✔
158
        startBearing,
679✔
159
        startPitch,
679✔
160
        startZoom
679✔
161
      }
679✔
162
    );
679✔
163

679✔
164
    this.makeViewport = options.makeViewport;
679✔
165
  }
679✔
166

1✔
167
  /**
1✔
168
   * Start panning
1✔
169
   * @param {[Number, Number]} pos - position on screen where the pointer grabs
1✔
170
   */
1✔
171
  panStart({pos}: {pos: [number, number]}): MapState {
1✔
172
    return this._getUpdatedState({
5✔
173
      startPanLngLat: this._unproject(pos)
5✔
174
    });
5✔
175
  }
5✔
176

1✔
177
  /**
1✔
178
   * Pan
1✔
179
   * @param {[Number, Number]} pos - position on screen where the pointer is
1✔
180
   * @param {[Number, Number], optional} startPos - where the pointer grabbed at
1✔
181
   *   the start of the operation. Must be supplied of `panStart()` was not called
1✔
182
   */
1✔
183
  pan({pos, startPos}: {pos: [number, number]; startPos?: [number, number]}): MapState {
1✔
184
    const startPanLngLat = this.getState().startPanLngLat || this._unproject(startPos);
15✔
185

15✔
186
    if (!startPanLngLat) {
15!
187
      return this;
×
188
    }
×
189

15✔
190
    const viewport = this.makeViewport(this.getViewportProps());
15✔
191
    const newProps = viewport.panByPosition(startPanLngLat, pos);
15✔
192

15✔
193
    return this._getUpdatedState(newProps);
15✔
194
  }
15✔
195

1✔
196
  /**
1✔
197
   * End panning
1✔
198
   * Must call if `panStart()` was called
1✔
199
   */
1✔
200
  panEnd(): MapState {
1✔
201
    return this._getUpdatedState({
5✔
202
      startPanLngLat: null
5✔
203
    });
5✔
204
  }
5✔
205

1✔
206
  /**
1✔
207
   * Start rotating
1✔
208
   * @param {[Number, Number]} pos - position on screen where the center is
1✔
209
   * @param {Number} altitude - optional altitude for rotation pivot
1✔
210
   *   - undefined: rotate around viewport center (no pivot point)
1✔
211
   *   - 0: rotate around pointer position at ground level
1✔
212
   *   - other value: rotate around pointer position at specified altitude
1✔
213
   */
1✔
214
  rotateStart({pos, altitude}: {pos: [number, number]; altitude?: number}): MapState {
1✔
215
    return this._getUpdatedState({
16✔
216
      startRotatePos: pos,
16✔
217
      startRotateLngLat: altitude !== undefined ? this._unproject3D(pos, altitude) : undefined,
16!
218
      startBearing: this.getViewportProps().bearing,
16✔
219
      startPitch: this.getViewportProps().pitch
16✔
220
    });
16✔
221
  }
16✔
222

1✔
223
  /**
1✔
224
   * Rotate
1✔
225
   * @param {[Number, Number]} pos - position on screen where the center is
1✔
226
   */
1✔
227
  rotate({
1✔
228
    pos,
10✔
229
    deltaAngleX = 0,
10✔
230
    deltaAngleY = 0
10✔
231
  }: {
10✔
232
    pos?: [number, number];
10✔
233
    deltaAngleX?: number;
10✔
234
    deltaAngleY?: number;
10✔
235
  }): MapState {
10✔
236
    const {startRotatePos, startRotateLngLat, startBearing, startPitch} = this.getState();
10✔
237

10✔
238
    if (!startRotatePos || startBearing === undefined || startPitch === undefined) {
10!
239
      return this;
×
240
    }
×
241
    let newRotation;
10✔
242
    if (pos) {
10✔
243
      newRotation = this._getNewRotation(pos, startRotatePos, startPitch, startBearing);
8✔
244
    } else {
10✔
245
      newRotation = {
2✔
246
        bearing: startBearing + deltaAngleX,
2✔
247
        pitch: startPitch + deltaAngleY
2✔
248
      };
2✔
249
    }
2✔
250

10✔
251
    // If we have a pivot point, adjust the camera position to keep the pivot point fixed
10✔
252
    if (startRotateLngLat) {
10!
253
      const rotatedViewport = this.makeViewport({
×
254
        ...this.getViewportProps(),
×
255
        ...newRotation
×
256
      });
×
257
      // Use panByPosition3D if available (WebMercatorViewport), otherwise fall back to panByPosition
×
258
      const panMethod = 'panByPosition3D' in rotatedViewport ? 'panByPosition3D' : 'panByPosition';
×
259
      return this._getUpdatedState({
×
260
        ...newRotation,
×
261
        ...rotatedViewport[panMethod](startRotateLngLat, startRotatePos)
×
262
      });
×
263
    }
×
264

10✔
265
    return this._getUpdatedState(newRotation);
10✔
266
  }
10✔
267

1✔
268
  /**
1✔
269
   * End rotating
1✔
270
   * Must call if `rotateStart()` was called
1✔
271
   */
1✔
272
  rotateEnd(): MapState {
1✔
273
    return this._getUpdatedState({
15✔
274
      startRotatePos: null,
15✔
275
      startRotateLngLat: null,
15✔
276
      startBearing: null,
15✔
277
      startPitch: null
15✔
278
    });
15✔
279
  }
15✔
280

1✔
281
  /**
1✔
282
   * Start zooming
1✔
283
   * @param {[Number, Number]} pos - position on screen where the center is
1✔
284
   */
1✔
285
  zoomStart({pos}: {pos: [number, number]}): MapState {
1✔
286
    return this._getUpdatedState({
5✔
287
      startZoomLngLat: this._unproject(pos),
5✔
288
      startZoom: this.getViewportProps().zoom
5✔
289
    });
5✔
290
  }
5✔
291

1✔
292
  /**
1✔
293
   * Zoom
1✔
294
   * @param {[Number, Number]} pos - position on screen where the current center is
1✔
295
   * @param {[Number, Number]} startPos - the center position at
1✔
296
   *   the start of the operation. Must be supplied of `zoomStart()` was not called
1✔
297
   * @param {Number} scale - a number between [0, 1] specifying the accumulated
1✔
298
   *   relative scale.
1✔
299
   */
1✔
300
  zoom({
1✔
301
    pos,
26✔
302
    startPos,
26✔
303
    scale
26✔
304
  }: {
26✔
305
    pos: [number, number];
26✔
306
    startPos?: [number, number];
26✔
307
    scale: number;
26✔
308
  }): MapState {
26✔
309
    // Make sure we zoom around the current mouse position rather than map center
26✔
310
    let {startZoom, startZoomLngLat} = this.getState();
26✔
311

26✔
312
    if (!startZoomLngLat) {
26✔
313
      // We have two modes of zoom:
23✔
314
      // scroll zoom that are discrete events (transform from the current zoom level),
23✔
315
      // and pinch zoom that are continuous events (transform from the zoom level when
23✔
316
      // pinch started).
23✔
317
      // If startZoom state is defined, then use the startZoom state;
23✔
318
      // otherwise assume discrete zooming
23✔
319
      startZoom = this.getViewportProps().zoom;
23✔
320
      startZoomLngLat = this._unproject(startPos) || this._unproject(pos);
23✔
321
    }
23✔
322
    if (!startZoomLngLat) {
26!
323
      return this;
×
324
    }
×
325

26✔
326
    const {maxZoom, minZoom} = this.getViewportProps();
26✔
327
    let zoom = (startZoom as number) + Math.log2(scale);
26✔
328
    zoom = clamp(zoom, minZoom, maxZoom);
26✔
329

26✔
330
    const zoomedViewport = this.makeViewport({...this.getViewportProps(), zoom});
26✔
331

26✔
332
    return this._getUpdatedState({
26✔
333
      zoom,
26✔
334
      ...zoomedViewport.panByPosition(startZoomLngLat, pos)
26✔
335
    });
26✔
336
  }
26✔
337

1✔
338
  /**
1✔
339
   * End zooming
1✔
340
   * Must call if `zoomStart()` was called
1✔
341
   */
1✔
342
  zoomEnd(): MapState {
1✔
343
    return this._getUpdatedState({
5✔
344
      startZoomLngLat: null,
5✔
345
      startZoom: null
5✔
346
    });
5✔
347
  }
5✔
348

1✔
349
  zoomIn(speed: number = 2): MapState {
1✔
350
    return this._zoomFromCenter(speed);
11✔
351
  }
11✔
352

1✔
353
  zoomOut(speed: number = 2): MapState {
1✔
354
    return this._zoomFromCenter(1 / speed);
10✔
355
  }
10✔
356

1✔
357
  moveLeft(speed: number = 100): MapState {
1✔
358
    return this._panFromCenter([speed, 0]);
4✔
359
  }
4✔
360

1✔
361
  moveRight(speed: number = 100): MapState {
1✔
362
    return this._panFromCenter([-speed, 0]);
3✔
363
  }
3✔
364

1✔
365
  moveUp(speed: number = 100): MapState {
1✔
366
    return this._panFromCenter([0, speed]);
4✔
367
  }
4✔
368

1✔
369
  moveDown(speed: number = 100): MapState {
1✔
370
    return this._panFromCenter([0, -speed]);
3✔
371
  }
3✔
372

1✔
373
  rotateLeft(speed: number = 15): MapState {
1✔
374
    return this._getUpdatedState({
4✔
375
      bearing: this.getViewportProps().bearing - speed
4✔
376
    });
4✔
377
  }
4✔
378

1✔
379
  rotateRight(speed: number = 15): MapState {
1✔
380
    return this._getUpdatedState({
3✔
381
      bearing: this.getViewportProps().bearing + speed
3✔
382
    });
3✔
383
  }
3✔
384

1✔
385
  rotateUp(speed: number = 10): MapState {
1✔
386
    return this._getUpdatedState({
4✔
387
      pitch: this.getViewportProps().pitch + speed
4✔
388
    });
4✔
389
  }
4✔
390

1✔
391
  rotateDown(speed: number = 10): MapState {
1✔
392
    return this._getUpdatedState({
3✔
393
      pitch: this.getViewportProps().pitch - speed
3✔
394
    });
3✔
395
  }
3✔
396

1✔
397
  shortestPathFrom(viewState: MapState): MapStateProps {
1✔
398
    // const endViewStateProps = new this.ControllerState(endProps).shortestPathFrom(startViewstate);
53✔
399
    const fromProps = viewState.getViewportProps();
53✔
400
    const props = {...this.getViewportProps()};
53✔
401
    const {bearing, longitude} = props;
53✔
402

53✔
403
    if (Math.abs(bearing - fromProps.bearing) > 180) {
53✔
404
      props.bearing = bearing < 0 ? bearing + 360 : bearing - 360;
1!
405
    }
1✔
406
    if (Math.abs(longitude - fromProps.longitude) > 180) {
53✔
407
      props.longitude = longitude < 0 ? longitude + 360 : longitude - 360;
1!
408
    }
1✔
409
    return props;
53✔
410
  }
53✔
411

1✔
412
  // Apply any constraints (mathematical or defined by _viewportProps) to map state
1✔
413
  applyConstraints(props: Required<MapStateProps>): Required<MapStateProps> {
1✔
414
    // Ensure zoom is within specified range
576✔
415
    const {maxZoom, minZoom, zoom} = props;
576✔
416
    props.zoom = clamp(zoom, minZoom, maxZoom);
576✔
417

576✔
418
    // Ensure pitch is within specified range
576✔
419
    const {maxPitch, minPitch, pitch} = props;
576✔
420
    props.pitch = clamp(pitch, minPitch, maxPitch);
576✔
421

576✔
422
    // Normalize viewport props to fit map height into viewport
576✔
423
    const {normalize = true} = props;
576✔
424
    if (normalize) {
576✔
425
      Object.assign(props, normalizeViewportProps(props));
575✔
426
    }
575✔
427

576✔
428
    return props;
576✔
429
  }
576✔
430

1✔
431
  /* Private methods */
1✔
432

1✔
433
  _zoomFromCenter(scale) {
1✔
434
    const {width, height} = this.getViewportProps();
21✔
435
    return this.zoom({
21✔
436
      pos: [width / 2, height / 2],
21✔
437
      scale
21✔
438
    });
21✔
439
  }
21✔
440

1✔
441
  _panFromCenter(offset) {
1✔
442
    const {width, height} = this.getViewportProps();
14✔
443
    return this.pan({
14✔
444
      startPos: [width / 2, height / 2],
14✔
445
      pos: [width / 2 + offset[0], height / 2 + offset[1]]
14✔
446
    });
14✔
447
  }
14✔
448

1✔
449
  _getUpdatedState(newProps): MapState {
1✔
450
    // @ts-ignore
133✔
451
    return new this.constructor({
133✔
452
      makeViewport: this.makeViewport,
133✔
453
      ...this.getViewportProps(),
133✔
454
      ...this.getState(),
133✔
455
      ...newProps
133✔
456
    });
133✔
457
  }
133✔
458

1✔
459
  _unproject(pos?: [number, number]): [number, number] | undefined {
1✔
460
    const viewport = this.makeViewport(this.getViewportProps());
70✔
461
    // @ts-ignore
70✔
462
    return pos && viewport.unproject(pos);
70✔
463
  }
70✔
464

1✔
465
  _unproject3D(pos: [number, number], altitude: number): [number, number, number] {
1✔
466
    const viewport = this.makeViewport(this.getViewportProps());
×
467
    return viewport.unproject(pos, {targetZ: altitude}) as [number, number, number];
×
468
  }
×
469

1✔
470
  _getNewRotation(
1✔
471
    pos: [number, number],
8✔
472
    startPos: [number, number],
8✔
473
    startPitch: number,
8✔
474
    startBearing: number
8✔
475
  ): {
8✔
476
    pitch: number;
8✔
477
    bearing: number;
8✔
478
  } {
8✔
479
    const deltaX = pos[0] - startPos[0];
8✔
480
    const deltaY = pos[1] - startPos[1];
8✔
481
    const centerY = pos[1];
8✔
482
    const startY = startPos[1];
8✔
483
    const {width, height} = this.getViewportProps();
8✔
484

8✔
485
    const deltaScaleX = deltaX / width;
8✔
486
    let deltaScaleY = 0;
8✔
487

8✔
488
    if (deltaY > 0) {
8✔
489
      if (Math.abs(height - startY) > PITCH_MOUSE_THRESHOLD) {
4✔
490
        // Move from 0 to -1 as we drag upwards
2✔
491
        deltaScaleY = (deltaY / (startY - height)) * PITCH_ACCEL;
2✔
492
      }
2✔
493
    } else if (deltaY < 0) {
4✔
494
      if (startY > PITCH_MOUSE_THRESHOLD) {
4✔
495
        // Move from 0 to 1 as we drag upwards
4✔
496
        deltaScaleY = 1 - centerY / startY;
4✔
497
      }
4✔
498
    }
4✔
499
    // clamp deltaScaleY to [-1, 1] so that rotation is constrained between minPitch and maxPitch.
8✔
500
    // deltaScaleX does not need to be clamped as bearing does not have constraints.
8✔
501
    deltaScaleY = clamp(deltaScaleY, -1, 1);
8✔
502

8✔
503
    const {minPitch, maxPitch} = this.getViewportProps();
8✔
504

8✔
505
    const bearing = startBearing + 180 * deltaScaleX;
8✔
506
    let pitch = startPitch;
8✔
507
    if (deltaScaleY > 0) {
8✔
508
      // Gradually increase pitch
4✔
509
      pitch = startPitch + deltaScaleY * (maxPitch - startPitch);
4✔
510
    } else if (deltaScaleY < 0) {
4✔
511
      // Gradually decrease pitch
2✔
512
      pitch = startPitch - deltaScaleY * (minPitch - startPitch);
2✔
513
    }
2✔
514

8✔
515
    return {
8✔
516
      pitch,
8✔
517
      bearing
8✔
518
    };
8✔
519
  }
8✔
520
}
1✔
521

1✔
522
export default class MapController extends Controller<MapState> {
1✔
523
  ControllerState = MapState;
1✔
524

1✔
525
  transition = {
1✔
526
    transitionDuration: 300,
1✔
527
    transitionInterpolator: new LinearInterpolator({
1✔
528
      transitionProps: {
1✔
529
        compare: ['longitude', 'latitude', 'zoom', 'bearing', 'pitch', 'position'],
1✔
530
        required: ['longitude', 'latitude', 'zoom']
1✔
531
      }
1✔
532
    })
1✔
533
  };
1✔
534

1✔
535
  dragMode: 'pan' | 'rotate' = 'pan';
1✔
536

1✔
537
  /**
1✔
538
   * Rotation pivot behavior:
1✔
539
   * - 'center': Rotate around viewport center (default)
1✔
540
   * - '2d': Rotate around pointer position at ground level (z=0)
1✔
541
   * - '3d': Rotate around 3D picked point (requires pickPosition callback)
1✔
542
   */
1✔
543
  protected rotationPivot: 'center' | '2d' | '3d' = 'center';
1✔
544

1✔
545
  /**
1✔
546
   * Internal callback to access deck picking engine. Populated by ViewManager
1✔
547
   */
1✔
548
  protected pickPosition?: (x: number, y: number) => {coordinate?: number[]} | null;
1✔
549

1✔
550
  constructor(opts: ConstructorParameters<typeof Controller>[0]) {
1✔
551
    super(opts);
13✔
552
    this.pickPosition = opts.pickPosition;
13✔
553
  }
13✔
554

1✔
555
  setProps(props: ControllerProps & MapStateProps & {rotationPivot?: 'center' | '2d' | '3d'}) {
1✔
556
    if ('rotationPivot' in props) {
402!
557
      this.rotationPivot = props.rotationPivot || 'center';
×
558
    }
×
559
    props.position = props.position || [0, 0, 0];
402✔
560
    const oldProps = this.props;
402✔
561

402✔
562
    super.setProps(props);
402✔
563

402✔
564
    const dimensionChanged = !oldProps || oldProps.height !== props.height;
402✔
565
    if (dimensionChanged) {
402✔
566
      // Dimensions changed, normalize the props
12✔
567
      this.updateViewport(
12✔
568
        new this.ControllerState({
12✔
569
          makeViewport: this.makeViewport,
12✔
570
          ...props,
12✔
571
          ...this.state
12✔
572
        })
12✔
573
      );
12✔
574
    }
12✔
575
  }
402✔
576

1✔
577
  protected updateViewport(
1✔
578
    newControllerState: MapState,
98✔
579
    extraProps: Record<string, any> | null = null,
98✔
580
    interactionState: InteractionState = {}
98✔
581
  ): void {
98✔
582
    // Inject rotation pivot position during rotation for visual feedback
98✔
583
    const state = newControllerState.getState();
98✔
584
    if (interactionState.isDragging && state.startRotateLngLat) {
98!
585
      interactionState = {
×
586
        ...interactionState,
×
587
        rotationPivotPosition: state.startRotateLngLat
×
588
      };
×
589
    } else if (interactionState.isDragging === false) {
98✔
590
      // Clear pivot when drag ends
18✔
591
      interactionState = {...interactionState, rotationPivotPosition: undefined};
18✔
592
    }
18✔
593

98✔
594
    super.updateViewport(newControllerState, extraProps, interactionState);
98✔
595
  }
98✔
596

1✔
597
  /** Add altitude to rotateStart params based on rotationPivot mode */
1✔
598
  protected _getRotateStartParams(pos: [number, number]): {
1✔
599
    pos: [number, number];
5✔
600
    altitude?: number;
5✔
601
  } {
5✔
602
    let altitude: number | undefined;
5✔
603
    if (this.rotationPivot === '2d') {
5!
604
      altitude = 0;
×
605
    } else if (this.rotationPivot === '3d') {
5!
606
      if (this.pickPosition) {
×
607
        const {x, y} = this.props;
×
608
        const pickResult = this.pickPosition(x + pos[0], y + pos[1]);
×
609
        if (pickResult && pickResult.coordinate && pickResult.coordinate.length >= 3) {
×
610
          altitude = pickResult.coordinate[2];
×
611
        }
×
612
      }
×
613
    }
×
614
    return {pos, altitude};
5✔
615
  }
5✔
616
}
1✔
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