• 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

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

1✔
5
/* eslint-disable max-statements, complexity */
1✔
6
import TransitionManager, {TransitionProps} from './transition-manager';
1✔
7
import LinearInterpolator from '../transitions/linear-interpolator';
1✔
8
import {IViewState} from './view-state';
1✔
9
import {ConstructorOf} from '../types/types';
1✔
10

1✔
11
import type Viewport from '../viewports/viewport';
1✔
12

1✔
13
import type {EventManager, MjolnirEvent, MjolnirGestureEvent, MjolnirWheelEvent, MjolnirKeyEvent} from 'mjolnir.js';
1✔
14
import type {Timeline} from '@luma.gl/engine';
1✔
15

1✔
16
const NO_TRANSITION_PROPS = {
1✔
17
  transitionDuration: 0
1✔
18
} as const;
1✔
19

1✔
20
const DEFAULT_INERTIA = 300;
1✔
21
const INERTIA_EASING = t => 1 - (1 - t) * (1 - t);
1✔
22

1✔
23
const EVENT_TYPES = {
1✔
24
  WHEEL: ['wheel'],
1✔
25
  PAN: ['panstart', 'panmove', 'panend'],
1✔
26
  PINCH: ['pinchstart', 'pinchmove', 'pinchend'],
1✔
27
  MULTI_PAN: ['multipanstart', 'multipanmove', 'multipanend'],
1✔
28
  DOUBLE_CLICK: ['dblclick'],
1✔
29
  KEYBOARD: ['keydown']
1✔
30
} as const;
1✔
31

1✔
32
/** Configuration of how user input is handled */
1✔
33
export type ControllerOptions = {
1✔
34
  /** Enable zooming with mouse wheel. Default `true`. */
1✔
35
  scrollZoom?: boolean | {
1✔
36
    /** Scaler that translates wheel delta to the change of viewport scale. Default `0.01`. */
1✔
37
    speed?: number;
1✔
38
    /** Smoothly transition to the new zoom. If enabled, will provide a slightly lagged but smoother experience. Default `false`. */
1✔
39
    smooth?: boolean
1✔
40
  };
1✔
41
  /** Enable panning with pointer drag. Default `true` */
1✔
42
  dragPan?: boolean;
1✔
43
  /** Enable rotating with pointer drag. Default `true` */
1✔
44
  dragRotate?: boolean;
1✔
45
  /** Enable zooming with double click. Default `true` */
1✔
46
  doubleClickZoom?: boolean;
1✔
47
  /** Enable zooming with multi-touch. Default `true` */
1✔
48
  touchZoom?: boolean;
1✔
49
  /** Enable rotating with multi-touch. Use two-finger rotating gesture for horizontal and three-finger swiping gesture for vertical rotation. Default `false` */
1✔
50
  touchRotate?: boolean;
1✔
51
  /** Enable interaction with keyboard. Default `true`. */
1✔
52
  keyboard?:
1✔
53
    | boolean
1✔
54
    | {
1✔
55
        /** Speed of zoom using +/- keys. Default `2` */
1✔
56
        zoomSpeed?: number;
1✔
57
        /** Speed of movement using arrow keys, in pixels. */
1✔
58
        moveSpeed?: number;
1✔
59
        /** Speed of rotation using shift + left/right arrow keys, in degrees. Default 15. */
1✔
60
        rotateSpeedX?: number;
1✔
61
        /** Speed of rotation using shift + up/down arrow keys, in degrees. Default 10. */
1✔
62
        rotateSpeedY?: number;
1✔
63
      };
1✔
64
  /** Drag behavior without pressing function keys, one of `pan` and `rotate`. */
1✔
65
  dragMode?: 'pan' | 'rotate';
1✔
66
  /** Enable inertia after panning/pinching. If a number is provided, indicates the duration of time over which the velocity reduces to zero, in milliseconds. Default `false`. */
1✔
67
  inertia?: boolean | number;
1✔
68
};
1✔
69

1✔
70
export type ControllerProps = {
1✔
71
  /** Identifier of the controller */
1✔
72
  id: string;
1✔
73
  /** Viewport x position */
1✔
74
  x: number;
1✔
75
  /** Viewport y position */
1✔
76
  y: number;
1✔
77
  /** Viewport width */
1✔
78
  width: number;
1✔
79
  /** Viewport height */
1✔
80
  height: number;
1✔
81
} & ControllerOptions & TransitionProps;
1✔
82

1✔
83
/** The state of a controller */
1✔
84
export type InteractionState = {
1✔
85
  /** If the view state is in transition */
1✔
86
  inTransition?: boolean;
1✔
87
  /** If the user is dragging */
1✔
88
  isDragging?: boolean;
1✔
89
  /** If the view is being panned, either from user input or transition */
1✔
90
  isPanning?: boolean;
1✔
91
  /** If the view is being rotated, either from user input or transition */
1✔
92
  isRotating?: boolean;
1✔
93
  /** If the view is being zoomed, either from user input or transition */
1✔
94
  isZooming?: boolean;
1✔
95
  /** World coordinate [lng, lat, altitude] of rotation pivot point when rotating */
1✔
96
  rotationPivotPosition?: [number, number, number];
1✔
97
}
1✔
98

1✔
99
/** Parameters passed to the onViewStateChange callback */
1✔
100
export type ViewStateChangeParameters<ViewStateT = any> = {
1✔
101
  viewId: string;
1✔
102
  /** The next view state, either from user input or transition */
1✔
103
  viewState: ViewStateT;
1✔
104
  /** Object describing the nature of the view state change */
1✔
105
  interactionState: InteractionState;
1✔
106
  /** The current view state */
1✔
107
  oldViewState?: ViewStateT;
1✔
108
}
1✔
109

1✔
110
const pinchEventWorkaround: any = {};
1✔
111

1✔
112
export default abstract class Controller<ControllerState extends IViewState<ControllerState>> {
1✔
113
  abstract get ControllerState(): ConstructorOf<ControllerState>;
1✔
114
  abstract get transition(): TransitionProps;
1✔
115

1✔
116
  // @ts-expect-error (2564) - not assigned in the constructor
1✔
117
  protected props: ControllerProps;
1✔
118
  protected state: Record<string, any> = {};
1✔
119

1✔
120
  protected transitionManager: TransitionManager<ControllerState>;
1✔
121
  protected eventManager: EventManager;
1✔
122
  protected onViewStateChange: (params: ViewStateChangeParameters) => void;
1✔
123
  protected onStateChange: (state: InteractionState) => void;
1✔
124
  protected makeViewport: (opts: Record<string, any>) => Viewport;
1✔
125

1✔
126
  private _controllerState?: ControllerState;
1✔
127
  private _events: Record<string, boolean> = {};
1✔
128
  private _interactionState: InteractionState = {
1✔
129
    isDragging: false
1✔
130
  };
1✔
131
  private _customEvents: string[] = [];
1✔
132
  private _eventStartBlocked: any = null;
1✔
133
  private _panMove: boolean = false;
1✔
134

1✔
135
  protected invertPan: boolean = false;
1✔
136
  protected dragMode: 'pan' | 'rotate' = 'rotate';
1✔
137
  protected inertia: number = 0;
1✔
138
  protected scrollZoom: boolean | {speed?: number; smooth?: boolean} = true;
1✔
139
  protected dragPan: boolean = true;
1✔
140
  protected dragRotate: boolean = true;
1✔
141
  protected doubleClickZoom: boolean = true;
1✔
142
  protected touchZoom: boolean = true;
1✔
143
  protected touchRotate: boolean = false;
1✔
144
  protected keyboard:
1✔
145
    | boolean
1✔
146
    | {
1✔
147
        zoomSpeed?: number; //  speed of zoom using +/- keys. Default 2.
1✔
148
        moveSpeed?: number; //  speed of movement using arrow keys, in pixels.
1✔
149
        rotateSpeedX?: number; //  speed of rotation using shift + left/right arrow keys, in degrees. Default 15.
1✔
150
        rotateSpeedY?: number; //  speed of rotation using shift + up/down arrow keys, in degrees. Default 10.
1✔
151
      } = true;
1✔
152

1✔
153
  constructor(opts: {
1✔
154
    timeline: Timeline,
25✔
155
    eventManager: EventManager;
25✔
156
    makeViewport: (opts: Record<string, any>) => Viewport;
25✔
157
    onViewStateChange: (params: ViewStateChangeParameters) => void;
25✔
158
    onStateChange: (state: InteractionState) => void;
25✔
159
    pickPosition?: (x: number, y: number) => {coordinate?: number[]} | null;
25✔
160
  }) {
25✔
161
    this.transitionManager = new TransitionManager<ControllerState>({
25✔
162
      ...opts,
25✔
163
      getControllerState: props => new this.ControllerState(props),
25✔
164
      onViewStateChange: this._onTransition.bind(this),
25✔
165
      onStateChange: this._setInteractionState.bind(this)
25✔
166
    });
25✔
167

25✔
168
    this.handleEvent = this.handleEvent.bind(this);
25✔
169

25✔
170
    this.eventManager = opts.eventManager;
25✔
171
    this.onViewStateChange = opts.onViewStateChange || (() => {});
25✔
172
    this.onStateChange = opts.onStateChange || (() => {});
25✔
173
    this.makeViewport = opts.makeViewport;
25✔
174
  }
25✔
175

1✔
176
  set events(customEvents) {
1✔
177
    this.toggleEvents(this._customEvents, false);
1✔
178
    this.toggleEvents(customEvents, true);
1✔
179
    this._customEvents = customEvents;
1✔
180
    // Make sure default events are not overwritten
1✔
181
    if (this.props) {
1!
182
      this.setProps(this.props);
×
183
    }
×
184
  }
1✔
185

1✔
186
  finalize() {
1✔
187
    for (const eventName in this._events) {
10✔
188
      if (this._events[eventName]) {
36✔
189
        // @ts-ignore (2345) event type string cannot be assifned to enum
27✔
190
        // eslint-disable-next-line @typescript-eslint/unbound-method
27✔
191
        this.eventManager?.off(eventName, this.handleEvent);
27✔
192
      }
27✔
193
    }
36✔
194
    this.transitionManager.finalize();
10✔
195
  }
10✔
196

1✔
197
  /**
1✔
198
   * Callback for events
1✔
199
   */
1✔
200
  handleEvent(event: MjolnirEvent) {
1✔
201
    // Force recalculate controller state
386✔
202
    this._controllerState = undefined;
386✔
203
    const eventStartBlocked = this._eventStartBlocked;
386✔
204

386✔
205
    switch (event.type) {
386✔
206
      case 'panstart':
386✔
207
        return eventStartBlocked ? false : this._onPanStart(event);
39!
208
      case 'panmove':
386✔
209
        return this._onPan(event);
41✔
210
      case 'panend':
386✔
211
        return this._onPanEnd(event);
39✔
212
      case 'pinchstart':
386✔
213
        return eventStartBlocked ? false : this._onPinchStart(event);
20!
214
      case 'pinchmove':
386✔
215
        return this._onPinch(event);
20✔
216
      case 'pinchend':
386✔
217
        return this._onPinchEnd(event);
20✔
218
      case 'multipanstart':
386✔
219
        return eventStartBlocked ? false : this._onMultiPanStart(event);
18!
220
      case 'multipanmove':
386✔
221
        return this._onMultiPan(event);
18✔
222
      case 'multipanend':
386✔
223
        return this._onMultiPanEnd(event);
18✔
224
      case 'dblclick':
386✔
225
        return this._onDoubleClick(event);
23✔
226
      case 'wheel':
386✔
227
        return this._onWheel(event as MjolnirWheelEvent);
25✔
228
      case 'keydown':
386✔
229
        return this._onKeyDown(event as MjolnirKeyEvent);
105✔
230
      default:
386!
231
        return false;
×
232
    }
386✔
233
  }
386✔
234

1✔
235
  /* Event utils */
1✔
236
  // Event object: http://hammerjs.github.io/api/#event-object
1✔
237
  get controllerState(): ControllerState {
1✔
238
    this._controllerState = this._controllerState || new this.ControllerState({
988✔
239
      makeViewport: this.makeViewport,
252✔
240
      ...this.props,
252✔
241
      ...this.state
252✔
242
    });
252✔
243
    return this._controllerState ;
988✔
244
  }
988✔
245

1✔
246
  getCenter(event: MjolnirGestureEvent | MjolnirWheelEvent) : [number, number] {
1✔
247
    const {x, y} = this.props;
139✔
248
    const {offsetCenter} = event;
139✔
249
    return [offsetCenter.x - x, offsetCenter.y - y];
139✔
250
  }
139✔
251

1✔
252
  isPointInBounds(pos: [number, number], event: MjolnirEvent): boolean {
1✔
253
    const {width, height} = this.props;
111✔
254
    if (event && event.handled) {
111✔
255
      return false;
7✔
256
    }
7✔
257

104✔
258
    const inside = pos[0] >= 0 && pos[0] <= width && pos[1] >= 0 && pos[1] <= height;
111✔
259
    if (inside && event) {
111✔
260
      event.stopPropagation();
69✔
261
    }
69✔
262
    return inside;
104✔
263
  }
111✔
264

1✔
265
  isFunctionKeyPressed(event: MjolnirEvent): boolean {
1✔
266
    const {srcEvent} = event;
132✔
267
    return Boolean(srcEvent.metaKey || srcEvent.altKey || srcEvent.ctrlKey || srcEvent.shiftKey);
132✔
268
  }
132✔
269

1✔
270
  isDragging(): boolean {
1✔
271
    return this._interactionState.isDragging || false;
141✔
272
  }
141✔
273

1✔
274
  // When a multi-touch event ends, e.g. pinch, not all pointers are lifted at the same time.
1✔
275
  // This triggers a brief `pan` event.
1✔
276
  // Calling this method will temporarily disable *start events to avoid conflicting transitions.
1✔
277
  blockEvents(timeout: number): void {
1✔
278
    /* global setTimeout */
11✔
279
    const timer = setTimeout(() => {
11✔
280
      if (this._eventStartBlocked === timer) {
11✔
281
        this._eventStartBlocked = null;
11✔
282
      }
11✔
283
    }, timeout);
11✔
284
    this._eventStartBlocked = timer;
11✔
285
  }
11✔
286

1✔
287
  /**
1✔
288
   * Extract interactivity options
1✔
289
   */
1✔
290
  setProps(props: ControllerProps) {
1✔
291
    if (props.dragMode) {
860!
292
      this.dragMode = props.dragMode;
×
293
    }
×
294
    this.props = props;
860✔
295

860✔
296
    if (!('transitionInterpolator' in props)) {
860✔
297
      // Add default transition interpolator
176✔
298
      props.transitionInterpolator = this._getTransitionProps().transitionInterpolator;
176✔
299
    }
176✔
300

860✔
301
    this.transitionManager.processViewStateChange(props);
860✔
302

860✔
303
    const {inertia} = props;
860✔
304
    this.inertia = Number.isFinite(inertia) ? (inertia as number) : (inertia === true ? DEFAULT_INERTIA : 0);
860!
305

860✔
306
    // TODO - make sure these are not reset on every setProps
860✔
307
    const {
860✔
308
      scrollZoom = true,
860✔
309
      dragPan = true,
860✔
310
      dragRotate = true,
860✔
311
      doubleClickZoom = true,
860✔
312
      touchZoom = true,
860✔
313
      touchRotate = false,
860✔
314
      keyboard = true
860✔
315
    } = props;
860✔
316

860✔
317
    // Register/unregister events
860✔
318
    const isInteractive = Boolean(this.onViewStateChange);
860✔
319
    this.toggleEvents(EVENT_TYPES.WHEEL, isInteractive && scrollZoom);
860✔
320
    // We always need the pan events to set the correct isDragging state, even if dragPan & dragRotate are both false
860✔
321
    this.toggleEvents(EVENT_TYPES.PAN, isInteractive);
860✔
322
    this.toggleEvents(EVENT_TYPES.PINCH, isInteractive && (touchZoom || touchRotate));
860✔
323
    this.toggleEvents(EVENT_TYPES.MULTI_PAN, isInteractive && touchRotate);
860✔
324
    this.toggleEvents(EVENT_TYPES.DOUBLE_CLICK, isInteractive && doubleClickZoom);
860✔
325
    this.toggleEvents(EVENT_TYPES.KEYBOARD, isInteractive && keyboard);
860✔
326

860✔
327
    // Interaction toggles
860✔
328
    this.scrollZoom = scrollZoom;
860✔
329
    this.dragPan = dragPan;
860✔
330
    this.dragRotate = dragRotate;
860✔
331
    this.doubleClickZoom = doubleClickZoom;
860✔
332
    this.touchZoom = touchZoom;
860✔
333
    this.touchRotate = touchRotate;
860✔
334
    this.keyboard = keyboard;
860✔
335
  }
860✔
336

1✔
337
  updateTransition() {
1✔
338
    this.transitionManager.updateTransition();
536✔
339
  }
536✔
340

1✔
341
  toggleEvents(eventNames, enabled) {
1✔
342
    if (this.eventManager) {
5,162✔
343
      eventNames.forEach(eventName => {
956✔
344
        if (this._events[eventName] !== enabled) {
1,910✔
345
          this._events[eventName] = enabled;
50✔
346
          if (enabled) {
50✔
347
            // eslint-disable-next-line @typescript-eslint/unbound-method
37✔
348
            this.eventManager.on(eventName, this.handleEvent);
37✔
349
          } else {
50✔
350
            // eslint-disable-next-line @typescript-eslint/unbound-method
13✔
351
            this.eventManager.off(eventName, this.handleEvent);
13✔
352
          }
13✔
353
        }
50✔
354
      });
956✔
355
    }
956✔
356
  }
5,162✔
357

1✔
358
  // Private Methods
1✔
359

1✔
360
  /* Callback util */
1✔
361
  // formats map state and invokes callback function
1✔
362
  protected updateViewport(newControllerState: ControllerState, extraProps: Record<string, any> | null = null, interactionState: InteractionState = {}) {
1✔
363
    const viewState = {...newControllerState.getViewportProps(), ...extraProps};
246✔
364

246✔
365
    // TODO - to restore diffing, we need to include interactionState
246✔
366
    const changed = this.controllerState !== newControllerState;
246✔
367
    // const oldViewState = this.controllerState.getViewportProps();
246✔
368
    // const changed = Object.keys(viewState).some(key => oldViewState[key] !== viewState[key]);
246✔
369

246✔
370
    this.state = newControllerState.getState();
246✔
371
    this._setInteractionState(interactionState);
246✔
372

246✔
373
    if (changed) {
246✔
374
      const oldViewState = this.controllerState && this.controllerState.getViewportProps();
246✔
375
      if (this.onViewStateChange) {
246✔
376
        this.onViewStateChange({viewState, interactionState: this._interactionState, oldViewState, viewId: this.props.id});
246✔
377
      }
246✔
378
    }
246✔
379
  }
246✔
380

1✔
381
  private _onTransition(params: {viewState: Record<string, any>, oldViewState: Record<string, any>}) {
1✔
382
    this.onViewStateChange({...params, interactionState: this._interactionState, viewId: this.props.id});
464✔
383
  }
464✔
384

1✔
385
  private _setInteractionState(newStates: InteractionState) {
1✔
386
    Object.assign(this._interactionState, newStates);
435✔
387
    this.onStateChange(this._interactionState);
435✔
388
  }
435✔
389

1✔
390
  /* Event handlers */
1✔
391
  // Default handler for the `panstart` event.
1✔
392
  protected _onPanStart(event: MjolnirGestureEvent): boolean {
1✔
393
    const pos = this.getCenter(event);
39✔
394
    if (!this.isPointInBounds(pos, event)) {
39✔
395
      return false;
14✔
396
    }
14✔
397
    let alternateMode = this.isFunctionKeyPressed(event) || event.rightButton || false;
39✔
398
    if (this.invertPan || this.dragMode === 'pan') {
39✔
399
      // invertPan is replaced by props.dragMode, keeping for backward compatibility
19✔
400
      alternateMode = !alternateMode;
19✔
401
    }
19✔
402

25✔
403
    const newControllerState = alternateMode
25✔
404
      ? this.controllerState.panStart({pos})
13✔
405
      : this.controllerState.rotateStart(this._getRotateStartParams(pos));
12✔
406
    this._panMove = alternateMode;
39✔
407
    this.updateViewport(newControllerState, NO_TRANSITION_PROPS, {isDragging: true});
39✔
408
    return true;
39✔
409
  }
39✔
410

1✔
411
  /** Returns parameters for rotateStart. Override to add extra params (e.g. altitude). */
1✔
412
  protected _getRotateStartParams(pos: [number, number]): {pos: [number, number]} {
1✔
413
    return {pos};
7✔
414
  }
7✔
415

1✔
416
  // Default handler for the `panmove` and `panend` event.
1✔
417
  protected _onPan(event: MjolnirGestureEvent): boolean {
1✔
418
    if (!this.isDragging()) {
41✔
419
      return false;
14✔
420
    }
14✔
421
    return this._panMove ? this._onPanMove(event) : this._onPanRotate(event);
41✔
422
  }
41✔
423

1✔
424
  protected _onPanEnd(event: MjolnirGestureEvent): boolean {
1✔
425
    if (!this.isDragging()) {
39✔
426
      return false;
14✔
427
    }
14✔
428
    return this._panMove ? this._onPanMoveEnd(event) : this._onPanRotateEnd(event);
39✔
429
  }
39✔
430

1✔
431
  // Default handler for panning to move.
1✔
432
  // Called by `_onPan` when panning without function key pressed.
1✔
433
  protected _onPanMove(event: MjolnirGestureEvent): boolean {
1✔
434
    if (!this.dragPan) {
14✔
435
      return false;
6✔
436
    }
6✔
437
    const pos = this.getCenter(event);
8✔
438
    const newControllerState = this.controllerState.pan({pos});
8✔
439
    this.updateViewport(newControllerState, NO_TRANSITION_PROPS, {
8✔
440
      isDragging: true,
8✔
441
      isPanning: true
8✔
442
    });
8✔
443
    return true;
8✔
444
  }
14✔
445

1✔
446
  protected _onPanMoveEnd(event: MjolnirGestureEvent): boolean {
1✔
447
    const {inertia} = this;
13✔
448
    if (this.dragPan && inertia && event.velocity) {
13✔
449
      const pos = this.getCenter(event);
1✔
450
      const endPos: [number, number] = [
1✔
451
        pos[0] + (event.velocityX * inertia) / 2,
1✔
452
        pos[1] + (event.velocityY * inertia) / 2
1✔
453
      ];
1✔
454
      const newControllerState = this.controllerState.pan({pos: endPos}).panEnd();
1✔
455
      this.updateViewport(
1✔
456
        newControllerState,
1✔
457
        {
1✔
458
          ...this._getTransitionProps(),
1✔
459
          transitionDuration: inertia,
1✔
460
          transitionEasing: INERTIA_EASING
1✔
461
        },
1✔
462
        {
1✔
463
          isDragging: false,
1✔
464
          isPanning: true
1✔
465
        }
1✔
466
      );
1✔
467
    } else {
13✔
468
      const newControllerState = this.controllerState.panEnd();
12✔
469
      this.updateViewport(newControllerState, null, {
12✔
470
        isDragging: false,
12✔
471
        isPanning: false
12✔
472
      });
12✔
473
    }
12✔
474
    return true;
13✔
475
  }
13✔
476

1✔
477
  // Default handler for panning to rotate.
1✔
478
  // Called by `_onPan` when panning with function key pressed.
1✔
479
  protected _onPanRotate(event: MjolnirGestureEvent): boolean {
1✔
480
    if (!this.dragRotate) {
11✔
481
      return false;
5✔
482
    }
5✔
483

6✔
484
    const pos = this.getCenter(event);
6✔
485
    const newControllerState = this.controllerState.rotate({pos});
6✔
486
    this.updateViewport(newControllerState, NO_TRANSITION_PROPS, {
6✔
487
      isDragging: true,
6✔
488
      isRotating: true
6✔
489
    });
6✔
490
    return true;
6✔
491
  }
11✔
492

1✔
493
  protected _onPanRotateEnd(event): boolean {
1✔
494
    const {inertia} = this;
12✔
495
    if (this.dragRotate && inertia && event.velocity) {
12✔
496
      const pos = this.getCenter(event);
1✔
497
      const endPos: [number, number] = [
1✔
498
        pos[0] + (event.velocityX * inertia) / 2,
1✔
499
        pos[1] + (event.velocityY * inertia) / 2
1✔
500
      ];
1✔
501
      const newControllerState = this.controllerState.rotate({pos: endPos}).rotateEnd();
1✔
502
      this.updateViewport(
1✔
503
        newControllerState,
1✔
504
        {
1✔
505
          ...this._getTransitionProps(),
1✔
506
          transitionDuration: inertia,
1✔
507
          transitionEasing: INERTIA_EASING
1✔
508
        },
1✔
509
        {
1✔
510
          isDragging: false,
1✔
511
          isRotating: true
1✔
512
        }
1✔
513
      );
1✔
514
    } else {
12✔
515
      const newControllerState = this.controllerState.rotateEnd();
11✔
516
      this.updateViewport(newControllerState, null, {
11✔
517
        isDragging: false,
11✔
518
        isRotating: false
11✔
519
      });
11✔
520
    }
11✔
521
    return true;
12✔
522
  }
12✔
523

1✔
524
  // Default handler for the `wheel` event.
1✔
525
  protected _onWheel(event: MjolnirWheelEvent): boolean {
1✔
526
    if (!this.scrollZoom) {
25✔
527
      return false;
7✔
528
    }
7✔
529

18✔
530
    const pos = this.getCenter(event);
18✔
531
    if (!this.isPointInBounds(pos, event)) {
25✔
532
      return false;
7✔
533
    }
7✔
534
    event.srcEvent.preventDefault();
11✔
535

11✔
536
    const {speed = 0.01, smooth = false} = this.scrollZoom === true ? {} : this.scrollZoom;
25!
537
    const {delta} = event;
25✔
538

25✔
539
    // Map wheel delta to relative scale
25✔
540
    let scale = 2 / (1 + Math.exp(-Math.abs(delta * speed)));
25✔
541
    if (delta < 0 && scale !== 0) {
25✔
542
      scale = 1 / scale;
9✔
543
    }
9✔
544

11✔
545
    const transitionProps = smooth
11!
546
      ? {...this._getTransitionProps({around: pos}), transitionDuration: 250}
✔
547
      : NO_TRANSITION_PROPS;
11✔
548

25✔
549
    const newControllerState = this.controllerState.zoom({pos, scale});
25✔
550
    this.updateViewport(
25✔
551
      newControllerState,
25✔
552
      transitionProps,
25✔
553
      {
25✔
554
        isZooming: true,
25✔
555
        isPanning: true
25✔
556
      }
25✔
557
    );
25✔
558

25✔
559
    // When there's no transition (duration = 0), immediately reset interaction state
25✔
560
    // since _onTransitionEnd callback won't fire
25✔
561
    if (!smooth) {
25✔
562
      this._setInteractionState({isZooming: false, isPanning: false});
11✔
563
    }
11✔
564
    return true;
11✔
565
  }
25✔
566

1✔
567
  protected _onMultiPanStart(event: MjolnirGestureEvent): boolean {
1✔
568
    const pos = this.getCenter(event);
18✔
569
    if (!this.isPointInBounds(pos, event)) {
18✔
570
      return false;
7✔
571
    }
7✔
572
    const newControllerState = this.controllerState.rotateStart({pos});
11✔
573
    this.updateViewport(newControllerState, NO_TRANSITION_PROPS, {isDragging: true});
11✔
574
    return true;
11✔
575
  }
18✔
576

1✔
577
  protected _onMultiPan(event: MjolnirGestureEvent): boolean {
1✔
578
    if (!this.touchRotate) {
18✔
579
      return false;
8✔
580
    }
8✔
581
    if (!this.isDragging()) {
18✔
582
      return false;
6✔
583
    }
6✔
584

4✔
585
    const pos = this.getCenter(event);
4✔
586
    pos[0] -= event.deltaX;
4✔
587

4✔
588
    const newControllerState = this.controllerState.rotate({pos});
4✔
589
    this.updateViewport(newControllerState, NO_TRANSITION_PROPS, {
4✔
590
      isDragging: true,
4✔
591
      isRotating: true
4✔
592
    });
4✔
593
    return true;
4✔
594
  }
18✔
595

1✔
596
  protected _onMultiPanEnd(event: MjolnirGestureEvent): boolean {
1✔
597
    if (!this.isDragging()) {
18✔
598
      return false;
7✔
599
    }
7✔
600
    const {inertia} = this;
11✔
601
    if (this.touchRotate && inertia && event.velocityY) {
18✔
602
      const pos = this.getCenter(event);
1✔
603
      const endPos: [number, number] = [pos[0], (pos[1] += (event.velocityY * inertia) / 2)];
1✔
604
      const newControllerState = this.controllerState.rotate({pos: endPos});
1✔
605
      this.updateViewport(
1✔
606
        newControllerState,
1✔
607
        {
1✔
608
          ...this._getTransitionProps(),
1✔
609
          transitionDuration: inertia,
1✔
610
          transitionEasing: INERTIA_EASING
1✔
611
        },
1✔
612
        {
1✔
613
          isDragging: false,
1✔
614
          isRotating: true
1✔
615
        }
1✔
616
      );
1✔
617
      this.blockEvents(inertia);
1✔
618
    } else {
18✔
619
      const newControllerState = this.controllerState.rotateEnd();
10✔
620
      this.updateViewport(newControllerState, null, {
10✔
621
        isDragging: false,
10✔
622
        isRotating: false
10✔
623
      });
10✔
624
    }
10✔
625
    return true;
11✔
626
  }
18✔
627

1✔
628
  // Default handler for the `pinchstart` event.
1✔
629
  protected _onPinchStart(event: MjolnirGestureEvent): boolean {
1✔
630
    const pos = this.getCenter(event);
20✔
631
    if (!this.isPointInBounds(pos, event)) {
20✔
632
      return false;
7✔
633
    }
7✔
634

13✔
635
    const newControllerState = this.controllerState.zoomStart({pos}).rotateStart({pos});
13✔
636
    // hack - hammer's `rotation` field doesn't seem to produce the correct angle
13✔
637
    pinchEventWorkaround._startPinchRotation = event.rotation;
13✔
638
    pinchEventWorkaround._lastPinchEvent = event;
13✔
639
    this.updateViewport(newControllerState, NO_TRANSITION_PROPS, {isDragging: true});
13✔
640
    return true;
13✔
641
  }
20✔
642

1✔
643
  // Default handler for the `pinchmove` and `pinchend` events.
1✔
644
  protected _onPinch(event: MjolnirGestureEvent): boolean {
1✔
645
    if (!this.touchZoom && !this.touchRotate) {
20✔
646
      return false;
7✔
647
    }
7✔
648
    if (!this.isDragging()) {
20✔
649
      return false;
7✔
650
    }
7✔
651

6✔
652
    let newControllerState = this.controllerState;
6✔
653
    if (this.touchZoom) {
6✔
654
      const {scale} = event;
6✔
655
      const pos = this.getCenter(event);
6✔
656
      newControllerState = newControllerState.zoom({pos, scale});
6✔
657
    }
6✔
658
    if (this.touchRotate) {
6✔
659
      const {rotation} = event;
6✔
660
      newControllerState = newControllerState.rotate({
6✔
661
        deltaAngleX: pinchEventWorkaround._startPinchRotation - rotation
6✔
662
      });
6✔
663
    }
6✔
664

6✔
665
    this.updateViewport(newControllerState, NO_TRANSITION_PROPS, {
6✔
666
      isDragging: true,
6✔
667
      isPanning: this.touchZoom,
6✔
668
      isZooming: this.touchZoom,
6✔
669
      isRotating: this.touchRotate
6✔
670
    });
6✔
671
    pinchEventWorkaround._lastPinchEvent = event;
6✔
672
    return true;
6✔
673
  }
20✔
674

1✔
675
  protected _onPinchEnd(event: MjolnirGestureEvent): boolean {
1✔
676
    if (!this.isDragging()) {
20✔
677
      return false;
7✔
678
    }
7✔
679
    const {inertia} = this;
13✔
680
    const {_lastPinchEvent} = pinchEventWorkaround;
13✔
681
    if (this.touchZoom && inertia && _lastPinchEvent && event.scale !== _lastPinchEvent.scale) {
20✔
682
      const pos = this.getCenter(event);
1✔
683
      let newControllerState = this.controllerState.rotateEnd();
1✔
684
      const z = Math.log2(event.scale);
1✔
685
      const velocityZ =
1✔
686
        (z - Math.log2(_lastPinchEvent.scale)) / (event.deltaTime - _lastPinchEvent.deltaTime);
1✔
687
      const endScale = Math.pow(2, z + (velocityZ * inertia) / 2);
1✔
688
      newControllerState = newControllerState.zoom({pos, scale: endScale}).zoomEnd();
1✔
689

1✔
690
      this.updateViewport(
1✔
691
        newControllerState,
1✔
692
        {
1✔
693
          ...this._getTransitionProps({around: pos}),
1✔
694
          transitionDuration: inertia,
1✔
695
          transitionEasing: INERTIA_EASING
1✔
696
        },
1✔
697
        {
1✔
698
          isDragging: false,
1✔
699
          isPanning: this.touchZoom,
1✔
700
          isZooming: this.touchZoom,
1✔
701
          isRotating: false
1✔
702
        }
1✔
703
      );
1✔
704
      this.blockEvents(inertia);
1✔
705
    } else {
20✔
706
      const newControllerState = this.controllerState.zoomEnd().rotateEnd();
12✔
707
      this.updateViewport(newControllerState, null, {
12✔
708
        isDragging: false,
12✔
709
        isPanning: false,
12✔
710
        isZooming: false,
12✔
711
        isRotating: false
12✔
712
      });
12✔
713
    }
12✔
714
    pinchEventWorkaround._startPinchRotation = null;
13✔
715
    pinchEventWorkaround._lastPinchEvent = null;
13✔
716
    return true;
13✔
717
  }
20✔
718

1✔
719
  // Default handler for the `dblclick` event.
1✔
720
  protected _onDoubleClick(event: MjolnirGestureEvent): boolean {
1✔
721
    if (!this.doubleClickZoom) {
23✔
722
      return false;
7✔
723
    }
7✔
724
    const pos = this.getCenter(event);
16✔
725
    if (!this.isPointInBounds(pos, event)) {
23✔
726
      return false;
7✔
727
    }
7✔
728

9✔
729
    const isZoomOut = this.isFunctionKeyPressed(event);
9✔
730

9✔
731
    const newControllerState = this.controllerState.zoom({pos, scale: isZoomOut ? 0.5 : 2});
23✔
732
    this.updateViewport(newControllerState, this._getTransitionProps({around: pos}), {
23✔
733
      isZooming: true,
23✔
734
      isPanning: true
23✔
735
    });
23✔
736
    this.blockEvents(100);
23✔
737
    return true;
23✔
738
  }
23✔
739

1✔
740
  // Default handler for the `keydown` event
1✔
741
  protected _onKeyDown(event: MjolnirKeyEvent): boolean {
1✔
742
    if (!this.keyboard) {
105✔
743
      return false;
7✔
744
    }
7✔
745
    const funcKey = this.isFunctionKeyPressed(event);
98✔
746
    // @ts-ignore
98✔
747
    const {zoomSpeed, moveSpeed, rotateSpeedX, rotateSpeedY} = this.keyboard === true ? {} : this.keyboard;
105✔
748
    const {controllerState} = this;
105✔
749
    let newControllerState;
105✔
750
    const interactionState: InteractionState = {};
105✔
751

105✔
752
    switch (event.srcEvent.code) {
105✔
753
      case 'Minus':
105✔
754
        newControllerState = funcKey
15✔
755
          ? controllerState.zoomOut(zoomSpeed).zoomOut(zoomSpeed)
7✔
756
          : controllerState.zoomOut(zoomSpeed);
8✔
757
        interactionState.isZooming = true;
15✔
758
        break;
15✔
759
      case 'Equal':
105✔
760
        newControllerState = funcKey
15✔
761
          ? controllerState.zoomIn(zoomSpeed).zoomIn(zoomSpeed)
8✔
762
          : controllerState.zoomIn(zoomSpeed);
7✔
763
        interactionState.isZooming = true;
15✔
764
        break;
15✔
765
      case 'ArrowLeft':
105✔
766
        if (funcKey) {
17✔
767
          newControllerState = controllerState.rotateLeft(rotateSpeedX);
8✔
768
          interactionState.isRotating = true;
8✔
769
        } else {
17✔
770
          newControllerState = controllerState.moveLeft(moveSpeed);
9✔
771
          interactionState.isPanning = true;
9✔
772
        }
9✔
773
        break;
17✔
774
      case 'ArrowRight':
105✔
775
        if (funcKey) {
14✔
776
          newControllerState = controllerState.rotateRight(rotateSpeedX);
7✔
777
          interactionState.isRotating = true;
7✔
778
        } else {
7✔
779
          newControllerState = controllerState.moveRight(moveSpeed);
7✔
780
          interactionState.isPanning = true;
7✔
781
        }
7✔
782
        break;
14✔
783
      case 'ArrowUp':
105✔
784
        if (funcKey) {
17✔
785
          newControllerState = controllerState.rotateUp(rotateSpeedY);
8✔
786
          interactionState.isRotating = true;
8✔
787
        } else {
17✔
788
          newControllerState = controllerState.moveUp(moveSpeed);
9✔
789
          interactionState.isPanning = true;
9✔
790
        }
9✔
791
        break;
17✔
792
      case 'ArrowDown':
105✔
793
        if (funcKey) {
14✔
794
          newControllerState = controllerState.rotateDown(rotateSpeedY);
7✔
795
          interactionState.isRotating = true;
7✔
796
        } else {
7✔
797
          newControllerState = controllerState.moveDown(moveSpeed);
7✔
798
          interactionState.isPanning = true;
7✔
799
        }
7✔
800
        break;
14✔
801
      default:
105✔
802
        return false;
6✔
803
    }
105✔
804
    this.updateViewport(newControllerState, this._getTransitionProps(), interactionState);
92✔
805
    return true;
92✔
806
  }
105✔
807

1✔
808
  protected _getTransitionProps(opts?: any): TransitionProps {
1✔
809
    const {transition} = this;
281✔
810

281✔
811
    if (!transition || !transition.transitionInterpolator) {
281✔
812
      return NO_TRANSITION_PROPS;
1✔
813
    }
1✔
814

280✔
815
    // Enables Transitions on double-tap and key-down events.
280✔
816
    return opts
280✔
817
      ? {
10✔
818
        ...transition,
10✔
819
        transitionInterpolator: new LinearInterpolator({
10✔
820
          ...opts,
10✔
821
          ...(transition.transitionInterpolator as LinearInterpolator).opts,
10✔
822
          makeViewport: this.controllerState.makeViewport
10✔
823
        })
10✔
824
      }
10✔
825
      : transition;
270✔
826
  }
281✔
827
}
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