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

visgl / react-map-gl / 18133561790

30 Sep 2025 02:34PM UTC coverage: 84.943% (-0.06%) from 85.0%
18133561790

Pull #2560

github

web-flow
Merge 1880f69d9 into 948fd2e4f
Pull Request #2560: Invoke setter with default when prop missing

941 of 1167 branches covered (80.63%)

Branch coverage included in aggregate %.

26 of 30 new or added lines in 2 files covered. (86.67%)

4 existing lines in 2 files now uncovered.

5586 of 6517 relevant lines covered (85.71%)

67.92 hits per line

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

77.07
/modules/react-mapbox/src/mapbox/mapbox.ts
1
import {transformToViewState, compareViewStateWithTransform} from '../utils/transform';
1✔
2
import {ProxyTransform, createProxyTransform} from './proxy-transform';
1✔
3
import {normalizeStyle} from '../utils/style-utils';
1✔
4
import {deepEqual} from '../utils/deep-equal';
1✔
5

1✔
6
import type {
1✔
7
  ViewState,
1✔
8
  Point,
1✔
9
  PointLike,
1✔
10
  PaddingOptions,
1✔
11
  ImmutableLike,
1✔
12
  LngLatBoundsLike,
1✔
13
  MapGeoJSONFeature
1✔
14
} from '../types/common';
1✔
15
import type {
1✔
16
  StyleSpecification,
1✔
17
  LightSpecification,
1✔
18
  TerrainSpecification,
1✔
19
  FogSpecification,
1✔
20
  ProjectionSpecification
1✔
21
} from '../types/style-spec';
1✔
22
import type {MapInstance} from '../types/lib';
1✔
23
import type {Transform} from '../types/internal';
1✔
24
import type {
1✔
25
  MapCallbacks,
1✔
26
  ViewStateChangeEvent,
1✔
27
  MapEvent,
1✔
28
  ErrorEvent,
1✔
29
  MapMouseEvent
1✔
30
} from '../types/events';
1✔
31

1✔
32
export type MapboxProps = Partial<ViewState> &
1✔
33
  MapCallbacks & {
1✔
34
    // Init options
1✔
35
    mapboxAccessToken?: string;
1✔
36

1✔
37
    /** Camera options used when constructing the Map instance */
1✔
38
    initialViewState?: Partial<ViewState> & {
1✔
39
      /** The initial bounds of the map. If bounds is specified, it overrides longitude, latitude and zoom options. */
1✔
40
      bounds?: LngLatBoundsLike;
1✔
41
      /** A fitBounds options object to use only when setting the bounds option. */
1✔
42
      fitBoundsOptions?: {
1✔
43
        offset?: PointLike;
1✔
44
        minZoom?: number;
1✔
45
        maxZoom?: number;
1✔
46
        padding?: number | PaddingOptions;
1✔
47
      };
1✔
48
    };
1✔
49

1✔
50
    /** If provided, render into an external WebGL context */
1✔
51
    gl?: WebGLRenderingContext;
1✔
52

1✔
53
    /** For external controller to override the camera state */
1✔
54
    viewState?: ViewState & {
1✔
55
      width: number;
1✔
56
      height: number;
1✔
57
    };
1✔
58

1✔
59
    // Styling
1✔
60

1✔
61
    /** Mapbox style */
1✔
62
    mapStyle?: string | StyleSpecification | ImmutableLike<StyleSpecification>;
1✔
63
    /** Enable diffing when the map style changes
1✔
64
     * @default true
1✔
65
     */
1✔
66
    styleDiffing?: boolean;
1✔
67
    /** The projection property of the style. Must conform to the Projection Style Specification.
1✔
68
     * @default 'mercator'
1✔
69
     */
1✔
70
    projection?: ProjectionSpecification | ProjectionSpecification['name'];
1✔
71
    /** The fog property of the style. Must conform to the Fog Style Specification .
1✔
72
     * If `undefined` is provided, removes the fog from the map. */
1✔
73
    fog?: FogSpecification;
1✔
74
    /** Light properties of the map. */
1✔
75
    light?: LightSpecification;
1✔
76
    /** Terrain property of the style. Must conform to the Terrain Style Specification .
1✔
77
     * If `undefined` is provided, removes terrain from the map. */
1✔
78
    terrain?: TerrainSpecification;
1✔
79

1✔
80
    /** Default layers to query on pointer events */
1✔
81
    interactiveLayerIds?: string[];
1✔
82
    /** CSS cursor */
1✔
83
    cursor?: string;
1✔
84
  };
1✔
85

1✔
86
const DEFAULT_STYLE = {version: 8, sources: {}, layers: []} as StyleSpecification;
1✔
87

1✔
88
const DEFAULT_SETTINGS = {
1✔
89
  minZoom: 0,
1✔
90
  maxZoom: 22,
1✔
91
  minPitch: 0,
1✔
92
  maxPitch: 85,
1✔
93
  maxBounds: [-180, -85.051129, 180, 85.051129],
1✔
94
  projection: 'mercator',
1✔
95
  renderWorldCopies: true
1✔
96
};
1✔
97

1✔
98
const pointerEvents = {
1✔
99
  mousedown: 'onMouseDown',
1✔
100
  mouseup: 'onMouseUp',
1✔
101
  mouseover: 'onMouseOver',
1✔
102
  mousemove: 'onMouseMove',
1✔
103
  click: 'onClick',
1✔
104
  dblclick: 'onDblClick',
1✔
105
  mouseenter: 'onMouseEnter',
1✔
106
  mouseleave: 'onMouseLeave',
1✔
107
  mouseout: 'onMouseOut',
1✔
108
  contextmenu: 'onContextMenu',
1✔
109
  touchstart: 'onTouchStart',
1✔
110
  touchend: 'onTouchEnd',
1✔
111
  touchmove: 'onTouchMove',
1✔
112
  touchcancel: 'onTouchCancel'
1✔
113
};
1✔
114
const cameraEvents = {
1✔
115
  movestart: 'onMoveStart',
1✔
116
  move: 'onMove',
1✔
117
  moveend: 'onMoveEnd',
1✔
118
  dragstart: 'onDragStart',
1✔
119
  drag: 'onDrag',
1✔
120
  dragend: 'onDragEnd',
1✔
121
  zoomstart: 'onZoomStart',
1✔
122
  zoom: 'onZoom',
1✔
123
  zoomend: 'onZoomEnd',
1✔
124
  rotatestart: 'onRotateStart',
1✔
125
  rotate: 'onRotate',
1✔
126
  rotateend: 'onRotateEnd',
1✔
127
  pitchstart: 'onPitchStart',
1✔
128
  pitch: 'onPitch',
1✔
129
  pitchend: 'onPitchEnd'
1✔
130
};
1✔
131
const otherEvents = {
1✔
132
  wheel: 'onWheel',
1✔
133
  boxzoomstart: 'onBoxZoomStart',
1✔
134
  boxzoomend: 'onBoxZoomEnd',
1✔
135
  boxzoomcancel: 'onBoxZoomCancel',
1✔
136
  resize: 'onResize',
1✔
137
  load: 'onLoad',
1✔
138
  render: 'onRender',
1✔
139
  idle: 'onIdle',
1✔
140
  remove: 'onRemove',
1✔
141
  data: 'onData',
1✔
142
  styledata: 'onStyleData',
1✔
143
  sourcedata: 'onSourceData',
1✔
144
  error: 'onError'
1✔
145
};
1✔
146
const settingNames = [
1✔
147
  'minZoom',
1✔
148
  'maxZoom',
1✔
149
  'minPitch',
1✔
150
  'maxPitch',
1✔
151
  'maxBounds',
1✔
152
  'projection',
1✔
153
  'renderWorldCopies'
1✔
154
];
1✔
155
const handlerNames = [
1✔
156
  'scrollZoom',
1✔
157
  'boxZoom',
1✔
158
  'dragRotate',
1✔
159
  'dragPan',
1✔
160
  'keyboard',
1✔
161
  'doubleClickZoom',
1✔
162
  'touchZoomRotate',
1✔
163
  'touchPitch'
1✔
164
];
1✔
165

1✔
166
/**
1✔
167
 * A wrapper for mapbox-gl's Map class
1✔
168
 */
1✔
169
export default class Mapbox {
1✔
170
  private _MapClass: {new (options: any): MapInstance};
1✔
171
  /** mapboxgl.Map instance */
1✔
172
  private _map: MapInstance = null;
1✔
173
  /** User-supplied props */
1✔
174
  props: MapboxProps;
1✔
175

1✔
176
  /** The transform that replaces native map.transform to resolve changes vs. React props
1✔
177
   * See proxy-transform.ts
1✔
178
   */
1✔
179
  private _proxyTransform: ProxyTransform;
1✔
180

1✔
181
  // Internal states
1✔
182
  /** Making updates driven by React props. Do not trigger React callbacks to avoid infinite loop */
1✔
183
  private _internalUpdate: boolean = false;
1✔
184
  /** Map is currently rendering */
1✔
185
  private _inRender: boolean = false;
1✔
186
  /** Map features under the pointer */
1✔
187
  private _hoveredFeatures: MapGeoJSONFeature[] = null;
1✔
188
  /** View state changes driven by React props
1✔
189
   * They still need to fire move/etc. events because controls such as marker/popup
1✔
190
   * subscribe to the move event internally to update their position
1✔
191
   * React callbacks like onMove are not called for these */
1✔
192
  private _deferredEvents: {
1✔
193
    move: boolean;
1✔
194
    zoom: boolean;
1✔
195
    pitch: boolean;
1✔
196
    rotate: boolean;
1✔
197
  } = {
1✔
198
    move: false,
1✔
199
    zoom: false,
1✔
200
    pitch: false,
1✔
201
    rotate: false
1✔
202
  };
1✔
203

1✔
204
  static savedMaps: Mapbox[] = [];
1✔
205

1✔
206
  constructor(
1✔
207
    MapClass: {new (options: any): MapInstance},
13✔
208
    props: MapboxProps,
13✔
209
    container: HTMLDivElement
13✔
210
  ) {
13✔
211
    this._MapClass = MapClass;
13✔
212
    this.props = props;
13✔
213
    this._initialize(container);
13✔
214
  }
13✔
215

1✔
216
  get map(): MapInstance {
1✔
217
    return this._map;
13✔
218
  }
13✔
219

1✔
220
  get transform(): Transform {
1✔
221
    return this._map.transform;
34✔
222
  }
34✔
223

1✔
224
  setProps(props: MapboxProps) {
1✔
225
    const oldProps = this.props;
44✔
226
    this.props = props;
44✔
227

44✔
228
    const settingsChanged = this._updateSettings(props, oldProps);
44✔
229
    if (settingsChanged) {
44!
230
      this._createProxyTransform(this._map);
×
231
    }
×
232
    const sizeChanged = this._updateSize(props);
44✔
233
    const viewStateChanged = this._updateViewState(props, true);
44✔
234
    this._updateStyle(props, oldProps);
44✔
235
    this._updateStyleComponents(props, oldProps);
44✔
236
    this._updateHandlers(props, oldProps);
44✔
237

44✔
238
    // If 1) view state has changed to match props and
44✔
239
    //    2) the props change is not triggered by map events,
44✔
240
    // it's driven by an external state change. Redraw immediately
44✔
241
    if (settingsChanged || sizeChanged || (viewStateChanged && !this._map.isMoving())) {
44✔
242
      this.redraw();
1✔
243
    }
1✔
244
  }
44✔
245

1✔
246
  static reuse(props: MapboxProps, container: HTMLDivElement): Mapbox {
1✔
247
    const that = Mapbox.savedMaps.pop();
×
248
    if (!that) {
×
249
      return null;
×
250
    }
×
251

×
252
    const map = that.map;
×
253
    // When reusing the saved map, we need to reparent the map(canvas) and other child nodes
×
254
    // intoto the new container from the props.
×
255
    // Step 1: reparenting child nodes from old container to new container
×
256
    const oldContainer = map.getContainer();
×
257
    container.className = oldContainer.className;
×
258
    while (oldContainer.childNodes.length > 0) {
×
259
      container.appendChild(oldContainer.childNodes[0]);
×
260
    }
×
261
    // Step 2: replace the internal container with new container from the react component
×
262
    // @ts-ignore
×
263
    map._container = container;
×
264

×
265
    // Step 4: apply new props
×
266
    that.setProps({...props, styleDiffing: false});
×
267
    map.resize();
×
268
    const {initialViewState} = props;
×
269
    if (initialViewState) {
×
270
      if (initialViewState.bounds) {
×
271
        map.fitBounds(initialViewState.bounds, {...initialViewState.fitBoundsOptions, duration: 0});
×
272
      } else {
×
273
        that._updateViewState(initialViewState, false);
×
274
      }
×
275
    }
×
276

×
277
    // Simulate load event
×
278
    if (map.isStyleLoaded()) {
×
279
      map.fire('load');
×
280
    } else {
×
281
      map.once('styledata', () => map.fire('load'));
×
282
    }
×
283

×
284
    // Force reload
×
285
    // @ts-ignore
×
286
    map._update();
×
287
    return that;
×
288
  }
×
289

1✔
290
  /* eslint-disable complexity,max-statements */
1✔
291
  _initialize(container: HTMLDivElement) {
1✔
292
    const {props} = this;
13✔
293
    const {mapStyle = DEFAULT_STYLE} = props;
13✔
294
    const mapOptions = {
13✔
295
      ...props,
13✔
296
      ...props.initialViewState,
13✔
297
      accessToken: props.mapboxAccessToken || getAccessTokenFromEnv() || null,
13✔
298
      container,
13✔
299
      style: normalizeStyle(mapStyle)
13✔
300
    };
13✔
301

13✔
302
    const viewState = mapOptions.initialViewState || mapOptions.viewState || mapOptions;
13✔
303
    Object.assign(mapOptions, {
13✔
304
      center: [viewState.longitude || 0, viewState.latitude || 0],
13✔
305
      zoom: viewState.zoom || 0,
13✔
306
      pitch: viewState.pitch || 0,
13✔
307
      bearing: viewState.bearing || 0
13✔
308
    });
13✔
309

13✔
310
    if (props.gl) {
13!
311
      // eslint-disable-next-line
×
312
      const getContext = HTMLCanvasElement.prototype.getContext;
×
313
      // Hijack canvas.getContext to return our own WebGLContext
×
314
      // This will be called inside the mapboxgl.Map constructor
×
315
      // @ts-expect-error
×
316
      HTMLCanvasElement.prototype.getContext = () => {
×
317
        // Unhijack immediately
×
318
        HTMLCanvasElement.prototype.getContext = getContext;
×
319
        return props.gl;
×
320
      };
×
321
    }
×
322

13✔
323
    const map = new this._MapClass(mapOptions);
13✔
324
    // Props that are not part of constructor options
13✔
325
    if (viewState.padding) {
13!
326
      map.setPadding(viewState.padding);
×
327
    }
×
328
    if (props.cursor) {
13!
329
      map.getCanvas().style.cursor = props.cursor;
×
330
    }
×
331
    this._createProxyTransform(map);
13✔
332

13✔
333
    // Hack
13✔
334
    // Insert code into map's render cycle
13✔
335
    // eslint-disable-next-line @typescript-eslint/unbound-method
13✔
336
    const renderMap = map._render;
13✔
337
    map._render = (arg: number) => {
13✔
338
      // Hijacked to set this state flag
63✔
339
      this._inRender = true;
63✔
340
      renderMap.call(map, arg);
63✔
341
      this._inRender = false;
63✔
342
    };
63✔
343
    // eslint-disable-next-line @typescript-eslint/unbound-method
13✔
344
    const runRenderTaskQueue = map._renderTaskQueue.run;
13✔
345
    map._renderTaskQueue.run = (arg: number) => {
13✔
346
      // This is where camera updates from input handler/animation happens
63✔
347
      // And where all view state change events are fired
63✔
348
      this._proxyTransform.$internalUpdate = true;
63✔
349
      runRenderTaskQueue.call(map._renderTaskQueue, arg);
63✔
350
      this._proxyTransform.$internalUpdate = false;
63✔
351
      this._fireDefferedEvents();
63✔
352
    };
63✔
353
    // eslint-disable-next-line @typescript-eslint/unbound-method
13✔
354
    const jumpTo = map.jumpTo;
13✔
355
    map.jumpTo = (...args: Parameters<MapInstance['jumpTo']>) => {
13✔
356
      // This method will fire view state change events immediately
×
357
      this._proxyTransform.$internalUpdate = true;
×
358
      jumpTo.apply(map, args);
×
359
      this._proxyTransform.$internalUpdate = false;
×
360
      return map;
×
361
    };
×
362
    // Insert code into map's event pipeline
13✔
363
    // eslint-disable-next-line @typescript-eslint/unbound-method
13✔
364
    const fireEvent = map.fire;
13✔
365
    map.fire = this._fireEvent.bind(this, fireEvent);
13✔
366

13✔
367
    // add listeners
13✔
368
    map.on('styledata', () => {
13✔
369
      this._updateStyleComponents(this.props, {});
32✔
370
    });
13✔
371
    map.on('sourcedata', () => {
13✔
372
      this._updateStyleComponents(this.props, {});
14✔
373
    });
13✔
374
    for (const eventName in pointerEvents) {
13✔
375
      map.on(eventName, this._onPointerEvent);
182✔
376
    }
182✔
377
    for (const eventName in cameraEvents) {
13✔
378
      map.on(eventName, this._onCameraEvent);
195✔
379
    }
195✔
380
    for (const eventName in otherEvents) {
13✔
381
      map.on(eventName, this._onEvent);
169✔
382
    }
169✔
383
    this._map = map;
13✔
384
  }
13✔
385
  /* eslint-enable complexity,max-statements */
1✔
386

1✔
387
  recycle() {
1✔
388
    // Clean up unnecessary elements before storing for reuse.
×
389
    const container = this.map.getContainer();
×
390
    const children = container.querySelector('[mapboxgl-children]');
×
391
    children?.remove();
×
392

×
393
    Mapbox.savedMaps.push(this);
×
394
  }
×
395

1✔
396
  destroy() {
1✔
397
    this._map.remove();
12✔
398
  }
12✔
399

1✔
400
  // Force redraw the map now. Typically resize() and jumpTo() is reflected in the next
1✔
401
  // render cycle, which is managed by Mapbox's animation loop.
1✔
402
  // This removes the synchronization issue caused by requestAnimationFrame.
1✔
403
  redraw() {
1✔
404
    const map = this._map as any;
1✔
405
    // map._render will throw error if style does not exist
1✔
406
    // https://github.com/mapbox/mapbox-gl-js/blob/fb9fc316da14e99ff4368f3e4faa3888fb43c513
1✔
407
    //   /src/ui/map.js#L1834
1✔
408
    if (!this._inRender && map.style) {
1✔
409
      // cancel the scheduled update
1✔
410
      if (map._frame) {
1✔
411
        map._frame.cancel();
1✔
412
        map._frame = null;
1✔
413
      }
1✔
414
      // the order is important - render() may schedule another update
1✔
415
      map._render();
1✔
416
    }
1✔
417
  }
1✔
418

1✔
419
  _createProxyTransform(map: any) {
1✔
420
    const proxyTransform = createProxyTransform(map.transform);
13✔
421
    map.transform = proxyTransform;
13✔
422
    map.painter.transform = proxyTransform;
13✔
423
    this._proxyTransform = proxyTransform;
13✔
424
  }
13✔
425

1✔
426
  /* Trigger map resize if size is controlled
1✔
427
     @param {object} nextProps
1✔
428
     @returns {bool} true if size has changed
1✔
429
   */
1✔
430
  _updateSize(nextProps: MapboxProps): boolean {
1✔
431
    // Check if size is controlled
44✔
432
    const {viewState} = nextProps;
44✔
433
    if (viewState) {
44!
434
      const map = this._map;
×
435
      if (viewState.width !== map.transform.width || viewState.height !== map.transform.height) {
×
436
        map.resize();
×
437
        return true;
×
438
      }
×
439
    }
×
440
    return false;
44✔
441
  }
44✔
442

1✔
443
  // Adapted from map.jumpTo
1✔
444
  /* Update camera to match props
1✔
445
     @param {object} nextProps
1✔
446
     @param {bool} triggerEvents - should fire camera events
1✔
447
     @returns {bool} true if anything is changed
1✔
448
   */
1✔
449
  _updateViewState(nextProps: MapboxProps, triggerEvents: boolean): boolean {
1✔
450
    const viewState: Partial<ViewState> = nextProps.viewState || nextProps;
44✔
451
    const tr = this._proxyTransform;
44✔
452
    const {zoom, pitch, bearing} = tr;
44✔
453
    const changed = compareViewStateWithTransform(this._proxyTransform, viewState);
44✔
454
    tr.$reactViewState = viewState;
44✔
455

44✔
456
    if (changed && triggerEvents) {
44✔
457
      const deferredEvents = this._deferredEvents;
13✔
458
      // Delay DOM control updates to the next render cycle
13✔
459
      deferredEvents.move = true;
13✔
460
      deferredEvents.zoom ||= zoom !== tr.zoom;
13✔
461
      deferredEvents.rotate ||= bearing !== tr.bearing;
13✔
462
      deferredEvents.pitch ||= pitch !== tr.pitch;
13✔
463
    }
13✔
464

44✔
465
    return changed;
44✔
466
  }
44✔
467

1✔
468
  /* Update camera constraints and projection settings to match props
1✔
469
     @param {object} nextProps
1✔
470
     @param {object} currProps
1✔
471
     @returns {bool} true if anything is changed
1✔
472
   */
1✔
473
  _updateSettings(nextProps: MapboxProps, currProps: MapboxProps): boolean {
1✔
474
    const map = this._map;
44✔
475
    let changed = false;
44✔
476
    for (const propName of settingNames) {
44✔
477
      const propPresent = propName in nextProps || propName in currProps;
308✔
478

308✔
479
      if (propPresent && !deepEqual(nextProps[propName], currProps[propName])) {
308!
480
        changed = true;
×
NEW
481
        const nextValue = propName in nextProps ? nextProps[propName] : DEFAULT_SETTINGS[propName];
×
482
        const setter = map[`set${propName[0].toUpperCase()}${propName.slice(1)}`];
×
NEW
483
        setter?.call(map, nextValue);
×
484
      }
×
485
    }
308✔
486
    return changed;
44✔
487
  }
44✔
488

1✔
489
  /* Update map style to match props
1✔
490
     @param {object} nextProps
1✔
491
     @param {object} currProps
1✔
492
     @returns {bool} true if style is changed
1✔
493
   */
1✔
494
  _updateStyle(nextProps: MapboxProps, currProps: MapboxProps): boolean {
1✔
495
    if (nextProps.cursor !== currProps.cursor) {
44!
496
      this._map.getCanvas().style.cursor = nextProps.cursor || '';
×
497
    }
×
498
    if (nextProps.mapStyle !== currProps.mapStyle) {
44✔
499
      const {mapStyle = DEFAULT_STYLE, styleDiffing = true} = nextProps;
2✔
500
      const options: any = {
2✔
501
        diff: styleDiffing
2✔
502
      };
2✔
503
      if ('localIdeographFontFamily' in nextProps) {
2!
504
        // @ts-ignore Mapbox specific prop
×
505
        options.localIdeographFontFamily = nextProps.localIdeographFontFamily;
×
506
      }
×
507
      this._map.setStyle(normalizeStyle(mapStyle), options);
2✔
508
      return true;
2✔
509
    }
2✔
510
    return false;
42✔
511
  }
44✔
512

1✔
513
  /* Update fog, light and terrain to match props
1✔
514
     @param {object} nextProps
1✔
515
     @param {object} currProps
1✔
516
     @returns {bool} true if anything is changed
1✔
517
   */
1✔
518
  _updateStyleComponents(nextProps: MapboxProps, currProps: MapboxProps): boolean {
1✔
519
    const map = this._map;
90✔
520
    let changed = false;
90✔
521
    if (map.isStyleLoaded()) {
90✔
522
      if ('light' in nextProps && map.setLight && !deepEqual(nextProps.light, currProps.light)) {
82!
523
        changed = true;
×
524
        map.setLight(nextProps.light);
×
525
      }
×
526
      if ('fog' in nextProps && map.setFog && !deepEqual(nextProps.fog, currProps.fog)) {
82!
527
        changed = true;
×
528
        map.setFog(nextProps.fog);
×
529
      }
×
530
      if (
82✔
531
        'terrain' in nextProps &&
82!
532
        map.setTerrain &&
×
533
        !deepEqual(nextProps.terrain, currProps.terrain)
×
534
      ) {
82!
535
        if (!nextProps.terrain || map.getSource(nextProps.terrain.source)) {
×
536
          changed = true;
×
537
          map.setTerrain(nextProps.terrain);
×
538
        }
×
539
      }
×
540
    }
82✔
541
    return changed;
90✔
542
  }
90✔
543

1✔
544
  /* Update interaction handlers to match props
1✔
545
     @param {object} nextProps
1✔
546
     @param {object} currProps
1✔
547
     @returns {bool} true if anything is changed
1✔
548
   */
1✔
549
  _updateHandlers(nextProps: MapboxProps, currProps: MapboxProps): boolean {
1✔
550
    const map = this._map;
44✔
551
    let changed = false;
44✔
552
    for (const propName of handlerNames) {
44✔
553
      const newValue = nextProps[propName] ?? true;
352✔
554
      const oldValue = currProps[propName] ?? true;
352✔
555
      if (!deepEqual(newValue, oldValue)) {
352!
556
        changed = true;
×
557
        if (newValue) {
×
558
          map[propName].enable(newValue);
×
559
        } else {
×
560
          map[propName].disable();
×
561
        }
×
562
      }
×
563
    }
352✔
564
    return changed;
44✔
565
  }
44✔
566

1✔
567
  _onEvent = (e: MapEvent) => {
1✔
568
    // @ts-ignore
186✔
569
    const cb = this.props[otherEvents[e.type]];
186✔
570
    if (cb) {
186✔
571
      cb(e);
38✔
572
    } else if (e.type === 'error') {
186!
573
      console.error((e as ErrorEvent).error); // eslint-disable-line
×
574
    }
×
575
  };
186✔
576

1✔
577
  private _queryRenderedFeatures(point: Point) {
1✔
578
    const map = this._map;
×
579
    const {interactiveLayerIds = []} = this.props;
×
580
    try {
×
581
      return map.queryRenderedFeatures(point, {
×
582
        layers: interactiveLayerIds.filter(map.getLayer.bind(map))
×
583
      });
×
584
    } catch {
×
585
      // May fail if style is not loaded
×
586
      return [];
×
587
    }
×
588
  }
×
589

1✔
590
  _updateHover(e: MapMouseEvent) {
1✔
591
    const {props} = this;
×
592
    const shouldTrackHoveredFeatures =
×
593
      props.interactiveLayerIds && (props.onMouseMove || props.onMouseEnter || props.onMouseLeave);
×
594

×
595
    if (shouldTrackHoveredFeatures) {
×
596
      const eventType = e.type;
×
597
      const wasHovering = this._hoveredFeatures?.length > 0;
×
598
      const features = this._queryRenderedFeatures(e.point);
×
599
      const isHovering = features.length > 0;
×
600

×
601
      if (!isHovering && wasHovering) {
×
602
        e.type = 'mouseleave';
×
603
        this._onPointerEvent(e);
×
604
      }
×
605
      this._hoveredFeatures = features;
×
606
      if (isHovering && !wasHovering) {
×
607
        e.type = 'mouseenter';
×
608
        this._onPointerEvent(e);
×
609
      }
×
610
      e.type = eventType;
×
611
    } else {
×
612
      this._hoveredFeatures = null;
×
613
    }
×
614
  }
×
615

1✔
616
  _onPointerEvent = (e: MapMouseEvent) => {
1✔
617
    if (e.type === 'mousemove' || e.type === 'mouseout') {
×
618
      this._updateHover(e);
×
619
    }
×
620

×
621
    // @ts-ignore
×
622
    const cb = this.props[pointerEvents[e.type]];
×
623
    if (cb) {
×
624
      if (this.props.interactiveLayerIds && e.type !== 'mouseover' && e.type !== 'mouseout') {
×
625
        e.features = this._hoveredFeatures || this._queryRenderedFeatures(e.point);
×
626
      }
×
627
      cb(e);
×
628
      delete e.features;
×
629
    }
×
630
  };
×
631

1✔
632
  _onCameraEvent = (e: ViewStateChangeEvent) => {
1✔
633
    if (!this._internalUpdate) {
74✔
634
      // @ts-ignore
72✔
635
      const cb = this.props[cameraEvents[e.type]];
72✔
636
      const tr = this._proxyTransform;
72✔
637
      if (cb) {
72✔
638
        e.viewState = transformToViewState(tr.$proposedTransform ?? tr);
18✔
639
        cb(e);
18✔
640
      }
18✔
641
      if (e.type === 'moveend') {
72✔
642
        tr.$proposedTransform = null;
4✔
643
      }
4✔
644
    }
72✔
645
    if (e.type in this._deferredEvents) {
74✔
646
      this._deferredEvents[e.type] = false;
58✔
647
    }
58✔
648
  };
74✔
649

1✔
650
  _fireEvent(baseFire: Function, event: string | MapEvent, properties?: object) {
1✔
651
    const map = this._map;
348✔
652
    const tr = this._proxyTransform;
348✔
653

348✔
654
    // Always expose the controlled transform to controls/end user
348✔
655
    const internal = tr.$internalUpdate;
348✔
656
    try {
348✔
657
      tr.$internalUpdate = false;
348✔
658
      baseFire.call(map, event, properties);
348✔
659
    } finally {
348✔
660
      tr.$internalUpdate = internal;
348✔
661
    }
348✔
662

348✔
663
    return map;
348✔
664
  }
348✔
665

1✔
666
  // If there are camera changes driven by props, invoke camera events so that DOM controls are synced
1✔
667
  _fireDefferedEvents() {
1✔
668
    const map = this._map;
63✔
669
    this._internalUpdate = true;
63✔
670
    for (const eventType in this._deferredEvents) {
63✔
671
      if (this._deferredEvents[eventType]) {
252✔
672
        map.fire(eventType);
2✔
673
      }
2✔
674
    }
252✔
675
    this._internalUpdate = false;
63✔
676
  }
63✔
677
}
1✔
678

1✔
679
/**
1✔
680
 * Access token can be provided via one of:
1✔
681
 *   mapboxAccessToken prop
1✔
682
 *   access_token query parameter
1✔
683
 *   MapboxAccessToken environment variable
1✔
684
 *   REACT_APP_MAPBOX_ACCESS_TOKEN environment variable
1✔
685
 * @returns access token
1✔
686
 */
1✔
687
function getAccessTokenFromEnv(): string {
1✔
688
  let accessToken = null;
1✔
689

1✔
690
  /* global location, process */
1✔
691
  if (typeof location !== 'undefined') {
1✔
692
    const match = /access_token=([^&\/]*)/.exec(location.search);
1✔
693
    accessToken = match && match[1];
1!
694
  }
1✔
695

1✔
696
  // Note: This depends on bundler plugins (e.g. webpack) importing environment correctly
1✔
697
  try {
1✔
698
    // eslint-disable-next-line no-process-env
1✔
699
    accessToken = accessToken || process.env.MapboxAccessToken;
1✔
700
  } catch {
1✔
701
    // ignore
1✔
702
  }
1✔
703

1✔
704
  try {
1✔
705
    // eslint-disable-next-line no-process-env
1✔
706
    accessToken = accessToken || process.env.REACT_APP_MAPBOX_ACCESS_TOKEN;
1✔
707
  } catch {
1✔
708
    // ignore
1✔
709
  }
1✔
710

1✔
711
  return accessToken;
1✔
712
}
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