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

visgl / react-map-gl / 6152126117

11 Sep 2023 09:48PM UTC coverage: 81.438%. Remained the same
6152126117

Pull #2279

github

web-flow
Merge 306d15ce4 into f903e4459
Pull Request #2279: Fix resetting cursor style

270 of 335 branches covered (0.0%)

Branch coverage included in aggregate %.

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

2029 of 2488 relevant lines covered (81.55%)

12.48 hits per line

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

75.85
/src/mapbox/mapbox.ts
1
import {
1✔
2
  transformToViewState,
1✔
3
  applyViewStateToTransform,
1✔
4
  cloneTransform,
1✔
5
  syncProjection
1✔
6
} from '../utils/transform';
1✔
7
import {normalizeStyle} from '../utils/style-utils';
1✔
8
import {deepEqual} from '../utils/deep-equal';
1✔
9

1✔
10
import type {
1✔
11
  Transform,
1✔
12
  ViewState,
1✔
13
  ViewStateChangeEvent,
1✔
14
  Point,
1✔
15
  PointLike,
1✔
16
  PaddingOptions,
1✔
17
  MapStyle,
1✔
18
  ImmutableLike,
1✔
19
  LngLatBoundsLike,
1✔
20
  Callbacks,
1✔
21
  MapEvent,
1✔
22
  ErrorEvent,
1✔
23
  MapMouseEvent,
1✔
24
  MapGeoJSONFeature,
1✔
25
  MapInstance,
1✔
26
  MapInstanceInternal
1✔
27
} from '../types';
1✔
28

1✔
29
export type MapboxProps<
1✔
30
  StyleT extends MapStyle = MapStyle,
1✔
31
  CallbacksT extends Callbacks = {}
1✔
32
> = Partial<ViewState> &
1✔
33
  CallbacksT & {
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 | StyleT | ImmutableLike<StyleT>;
1✔
63
    /** Enable diffing when the map style changes
1✔
64
     * @default true
1✔
65
     */
1✔
66
    styleDiffing?: boolean;
1✔
67
    /** The fog property of the style. Must conform to the Fog Style Specification .
1✔
68
     * If `undefined` is provided, removes the fog from the map. */
1✔
69
    fog?: StyleT['fog'];
1✔
70
    /** Light properties of the map. */
1✔
71
    light?: StyleT['light'];
1✔
72
    /** Terrain property of the style. Must conform to the Terrain Style Specification .
1✔
73
     * If `undefined` is provided, removes terrain from the map. */
1✔
74
    terrain?: StyleT['terrain'];
1✔
75

1✔
76
    /** Default layers to query on pointer events */
1✔
77
    interactiveLayerIds?: string[];
1✔
78
    /** CSS cursor */
1✔
79
    cursor?: string;
1✔
80
  };
1✔
81

1✔
82
const DEFAULT_STYLE = {version: 8, sources: {}, layers: []} as MapStyle;
1✔
83

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

1✔
152
/**
1✔
153
 * A wrapper for mapbox-gl's Map class
1✔
154
 */
1✔
155
export default class Mapbox<
1✔
156
  StyleT extends MapStyle = MapStyle,
1✔
157
  CallbacksT extends Callbacks = {},
1✔
158
  MapT extends MapInstance = MapInstance
1✔
159
> {
1✔
160
  private _MapClass: {new (options: any): MapInstance};
1✔
161
  // mapboxgl.Map instance
1✔
162
  private _map: MapInstanceInternal<MapT> = null;
1✔
163
  // User-supplied props
1✔
164
  props: MapboxProps<StyleT, CallbacksT>;
1✔
165

1✔
166
  // Mapbox map is stateful.
1✔
167
  // During method calls/user interactions, map.transform is mutated and
1✔
168
  // deviate from user-supplied props.
1✔
169
  // In order to control the map reactively, we shadow the transform
1✔
170
  // with the one below, which reflects the view state resolved from
1✔
171
  // both user-supplied props and the underlying state
1✔
172
  private _renderTransform: Transform;
1✔
173

1✔
174
  // Internal states
1✔
175
  private _internalUpdate: boolean = false;
1✔
176
  private _inRender: boolean = false;
1✔
177
  private _hoveredFeatures: MapGeoJSONFeature[] = null;
1✔
178
  private _deferredEvents: {
1✔
179
    move: boolean;
1✔
180
    zoom: boolean;
1✔
181
    pitch: boolean;
1✔
182
    rotate: boolean;
1✔
183
  } = {
1✔
184
    move: false,
1✔
185
    zoom: false,
1✔
186
    pitch: false,
1✔
187
    rotate: false
1✔
188
  };
1✔
189

1✔
190
  static savedMaps: Mapbox[] = [];
1✔
191

1✔
192
  constructor(
1✔
193
    MapClass: {new (options: any): MapInstance},
11✔
194
    props: MapboxProps<StyleT, CallbacksT>,
11✔
195
    container: HTMLDivElement
11✔
196
  ) {
11✔
197
    this._MapClass = MapClass;
11✔
198
    this.props = props;
11✔
199
    this._initialize(container);
11✔
200
  }
11✔
201

1✔
202
  get map(): MapT {
1✔
203
    return this._map;
11✔
204
  }
11✔
205

1✔
206
  get transform(): Transform {
1✔
207
    return this._renderTransform;
22✔
208
  }
22✔
209

1✔
210
  setProps(props: MapboxProps<StyleT, CallbacksT>) {
1✔
211
    const oldProps = this.props;
40✔
212
    this.props = props;
40✔
213

40✔
214
    const settingsChanged = this._updateSettings(props, oldProps);
40✔
215
    if (settingsChanged) {
40!
216
      this._createShadowTransform(this._map);
×
217
    }
×
218
    const sizeChanged = this._updateSize(props);
40✔
219
    const viewStateChanged = this._updateViewState(props, true);
40✔
220
    this._updateStyle(props, oldProps);
40✔
221
    this._updateStyleComponents(props, oldProps);
40✔
222
    this._updateHandlers(props, oldProps);
40✔
223

40✔
224
    // If 1) view state has changed to match props and
40✔
225
    //    2) the props change is not triggered by map events,
40✔
226
    // it's driven by an external state change. Redraw immediately
40✔
227
    if (settingsChanged || sizeChanged || (viewStateChanged && !this._map.isMoving())) {
40✔
228
      this.redraw();
2✔
229
    }
2✔
230
  }
40✔
231

1✔
232
  static reuse<StyleT extends MapStyle, CallbacksT extends Callbacks, MapT extends MapInstance>(
1✔
233
    props: MapboxProps<StyleT, CallbacksT>,
×
234
    container: HTMLDivElement
×
235
  ): Mapbox<StyleT, CallbacksT, MapT> {
×
236
    const that = Mapbox.savedMaps.pop() as Mapbox<StyleT, CallbacksT, MapT>;
×
237
    if (!that) {
×
238
      return null;
×
239
    }
×
240

×
241
    const map = that.map;
×
242
    // When reusing the saved map, we need to reparent the map(canvas) and other child nodes
×
243
    // intoto the new container from the props.
×
244
    // Step 1: reparenting child nodes from old container to new container
×
245
    const oldContainer = map.getContainer();
×
246
    container.className = oldContainer.className;
×
247
    while (oldContainer.childNodes.length > 0) {
×
248
      container.appendChild(oldContainer.childNodes[0]);
×
249
    }
×
250
    // Step 2: replace the internal container with new container from the react component
×
251
    // @ts-ignore
×
252
    map._container = container;
×
253

×
254
    // With maplibre-gl as mapLib, map uses ResizeObserver to observe when its container resizes.
×
255
    // When reusing the saved map, we need to disconnect the observer and observe the new container.
×
256
    // Step 3: telling the ResizeObserver to disconnect and observe the new container
×
257
    // @ts-ignore
×
258
    const resizeObserver = map._resizeObserver;
×
259
    if (resizeObserver) {
×
260
      resizeObserver.disconnect();
×
261
      resizeObserver.observe(container);
×
262
    }
×
263

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

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

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

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

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

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

11✔
322
    const map = new this._MapClass(mapOptions) as MapInstanceInternal<MapT>;
11✔
323
    // Props that are not part of constructor options
11✔
324
    if (viewState.padding) {
11!
325
      map.setPadding(viewState.padding);
×
326
    }
×
327
    if (props.cursor) {
11!
328
      map.getCanvas().style.cursor = props.cursor;
×
329
    }
×
330
    this._createShadowTransform(map);
11✔
331

11✔
332
    // Hack
11✔
333
    // Insert code into map's render cycle
11✔
334
    const renderMap = map._render;
11✔
335
    map._render = (arg: number) => {
11✔
336
      this._inRender = true;
22✔
337
      renderMap.call(map, arg);
22✔
338
      this._inRender = false;
22✔
339
    };
11✔
340
    const runRenderTaskQueue = map._renderTaskQueue.run;
11✔
341
    map._renderTaskQueue.run = (arg: number) => {
11✔
342
      runRenderTaskQueue.call(map._renderTaskQueue, arg);
22✔
343
      this._onBeforeRepaint();
22✔
344
    };
11✔
345
    map.on('render', () => this._onAfterRepaint());
11✔
346
    // Insert code into map's event pipeline
11✔
347
    // eslint-disable-next-line @typescript-eslint/unbound-method
11✔
348
    const fireEvent = map.fire;
11✔
349
    map.fire = this._fireEvent.bind(this, fireEvent);
11✔
350

11✔
351
    // add listeners
11✔
352
    map.on('resize', () => {
11✔
353
      this._renderTransform.resize(map.transform.width, map.transform.height);
×
354
    });
11✔
355
    map.on('styledata', () => {
11✔
356
      this._updateStyleComponents(this.props, {});
13✔
357
      // Projection can be set in stylesheet
13✔
358
      syncProjection(map.transform, this._renderTransform);
13✔
359
    });
11✔
360
    map.on('sourcedata', () => this._updateStyleComponents(this.props, {}));
11✔
361
    for (const eventName in pointerEvents) {
11✔
362
      map.on(eventName, this._onPointerEvent);
154✔
363
    }
154✔
364
    for (const eventName in cameraEvents) {
11✔
365
      map.on(eventName, this._onCameraEvent);
165✔
366
    }
165✔
367
    for (const eventName in otherEvents) {
11✔
368
      map.on(eventName, this._onEvent);
143✔
369
    }
143✔
370
    this._map = map;
11✔
371
  }
11✔
372
  /* eslint-enable complexity,max-statements */
1✔
373

1✔
374
  recycle() {
1✔
375
    // Clean up unnecessary elements before storing for reuse.
×
376
    const container = this.map.getContainer();
×
377
    const children = container.querySelector('[mapboxgl-children]');
×
378
    children?.remove();
×
379

×
380
    Mapbox.savedMaps.push(this);
×
381
  }
×
382

1✔
383
  destroy() {
1✔
384
    this._map.remove();
6✔
385
  }
6✔
386

1✔
387
  // Force redraw the map now. Typically resize() and jumpTo() is reflected in the next
1✔
388
  // render cycle, which is managed by Mapbox's animation loop.
1✔
389
  // This removes the synchronization issue caused by requestAnimationFrame.
1✔
390
  redraw() {
1✔
391
    const map = this._map as any;
2✔
392
    // map._render will throw error if style does not exist
2✔
393
    // https://github.com/mapbox/mapbox-gl-js/blob/fb9fc316da14e99ff4368f3e4faa3888fb43c513
2✔
394
    //   /src/ui/map.js#L1834
2✔
395
    if (!this._inRender && map.style) {
2✔
396
      // cancel the scheduled update
2✔
397
      if (map._frame) {
2!
398
        map._frame.cancel();
×
399
        map._frame = null;
×
400
      }
×
401
      // the order is important - render() may schedule another update
2✔
402
      map._render();
2✔
403
    }
2✔
404
  }
2✔
405

1✔
406
  _createShadowTransform(map: any) {
1✔
407
    const renderTransform = cloneTransform(map.transform);
11✔
408
    map.painter.transform = renderTransform;
11✔
409

11✔
410
    this._renderTransform = renderTransform;
11✔
411
  }
11✔
412

1✔
413
  /* Trigger map resize if size is controlled
1✔
414
     @param {object} nextProps
1✔
415
     @returns {bool} true if size has changed
1✔
416
   */
1✔
417
  _updateSize(nextProps: MapboxProps<StyleT>): boolean {
1✔
418
    // Check if size is controlled
40✔
419
    const {viewState} = nextProps;
40✔
420
    if (viewState) {
40!
421
      const map = this._map;
×
422
      if (viewState.width !== map.transform.width || viewState.height !== map.transform.height) {
×
423
        map.resize();
×
424
        return true;
×
425
      }
×
426
    }
×
427
    return false;
40✔
428
  }
40✔
429

1✔
430
  // Adapted from map.jumpTo
1✔
431
  /* Update camera to match props
1✔
432
     @param {object} nextProps
1✔
433
     @param {bool} triggerEvents - should fire camera events
1✔
434
     @returns {bool} true if anything is changed
1✔
435
   */
1✔
436
  _updateViewState(nextProps: MapboxProps<StyleT>, triggerEvents: boolean): boolean {
1✔
437
    if (this._internalUpdate) {
62✔
438
      return false;
2✔
439
    }
2✔
440
    const map = this._map;
60✔
441

60✔
442
    const tr = this._renderTransform;
60✔
443
    // Take a snapshot of the transform before mutation
60✔
444
    const {zoom, pitch, bearing} = tr;
60✔
445
    const isMoving = map.isMoving();
60✔
446

60✔
447
    if (isMoving) {
62✔
448
      // All movement of the camera is done relative to the sea level
29✔
449
      tr.cameraElevationReference = 'sea';
29✔
450
    }
29✔
451
    const changed = applyViewStateToTransform(tr, {
60✔
452
      ...transformToViewState(map.transform),
60✔
453
      ...nextProps
60✔
454
    });
60✔
455
    if (isMoving) {
62✔
456
      // Reset camera reference
29✔
457
      tr.cameraElevationReference = 'ground';
29✔
458
    }
29✔
459

60✔
460
    if (changed && triggerEvents) {
62✔
461
      const deferredEvents = this._deferredEvents;
11✔
462
      // Delay DOM control updates to the next render cycle
11✔
463
      deferredEvents.move = true;
11✔
464
      deferredEvents.zoom ||= zoom !== tr.zoom;
11✔
465
      deferredEvents.rotate ||= bearing !== tr.bearing;
11✔
466
      deferredEvents.pitch ||= pitch !== tr.pitch;
11✔
467
    }
11✔
468

60✔
469
    // Avoid manipulating the real transform when interaction/animation is ongoing
60✔
470
    // as it would interfere with Mapbox's handlers
60✔
471
    if (!isMoving) {
62✔
472
      applyViewStateToTransform(map.transform, nextProps);
31✔
473
    }
31✔
474

60✔
475
    return changed;
60✔
476
  }
60✔
477

1✔
478
  /* Update camera constraints and projection settings to match props
1✔
479
     @param {object} nextProps
1✔
480
     @param {object} currProps
1✔
481
     @returns {bool} true if anything is changed
1✔
482
   */
1✔
483
  _updateSettings(nextProps: MapboxProps<StyleT>, currProps: MapboxProps<StyleT>): boolean {
1✔
484
    const map = this._map;
40✔
485
    let changed = false;
40✔
486
    for (const propName of settingNames) {
40✔
487
      if (propName in nextProps && !deepEqual(nextProps[propName], currProps[propName])) {
280!
488
        changed = true;
×
489
        const setter = map[`set${propName[0].toUpperCase()}${propName.slice(1)}`];
×
490
        setter?.call(map, nextProps[propName]);
×
491
      }
×
492
    }
280✔
493
    return changed;
40✔
494
  }
40✔
495

1✔
496
  /* Update map style to match props
1✔
497
     @param {object} nextProps
1✔
498
     @param {object} currProps
1✔
499
     @returns {bool} true if style is changed
1✔
500
   */
1✔
501
  _updateStyle(nextProps: MapboxProps<StyleT>, currProps: MapboxProps<StyleT>): boolean {
1✔
502
    if (nextProps.cursor !== currProps.cursor) {
40!
503
      this._map.getCanvas().style.cursor = nextProps.cursor || '';
×
504
    }
×
505
    if (nextProps.mapStyle !== currProps.mapStyle) {
40✔
506
      const {mapStyle = DEFAULT_STYLE, styleDiffing = true} = nextProps;
2✔
507
      const options: any = {
2✔
508
        diff: styleDiffing
2✔
509
      };
2✔
510
      if ('localIdeographFontFamily' in nextProps) {
2!
511
        // @ts-ignore Mapbox specific prop
×
512
        options.localIdeographFontFamily = nextProps.localIdeographFontFamily;
×
513
      }
×
514
      this._map.setStyle(normalizeStyle(mapStyle), options);
2✔
515
      return true;
2✔
516
    }
2✔
517
    return false;
38✔
518
  }
38✔
519

1✔
520
  /* Update fog, light and terrain to match props
1✔
521
     @param {object} nextProps
1✔
522
     @param {object} currProps
1✔
523
     @returns {bool} true if anything is changed
1✔
524
   */
1✔
525
  _updateStyleComponents(nextProps: MapboxProps<StyleT>, currProps: MapboxProps<StyleT>): boolean {
1✔
526
    const map = this._map;
53✔
527
    let changed = false;
53✔
528
    if (map.isStyleLoaded()) {
53✔
529
      if ('light' in nextProps && map.setLight && !deepEqual(nextProps.light, currProps.light)) {
37!
530
        changed = true;
×
531
        map.setLight(nextProps.light);
×
532
      }
×
533
      if ('fog' in nextProps && map.setFog && !deepEqual(nextProps.fog, currProps.fog)) {
37!
534
        changed = true;
×
535
        map.setFog(nextProps.fog);
×
536
      }
×
537
      if (
37✔
538
        'terrain' in nextProps &&
37!
539
        map.setTerrain &&
37!
540
        !deepEqual(nextProps.terrain, currProps.terrain)
×
541
      ) {
37!
542
        if (!nextProps.terrain || map.getSource(nextProps.terrain.source)) {
×
543
          changed = true;
×
544
          map.setTerrain(nextProps.terrain);
×
545
        }
×
546
      }
×
547
    }
37✔
548
    return changed;
53✔
549
  }
53✔
550

1✔
551
  /* Update interaction handlers to match props
1✔
552
     @param {object} nextProps
1✔
553
     @param {object} currProps
1✔
554
     @returns {bool} true if anything is changed
1✔
555
   */
1✔
556
  _updateHandlers(nextProps: MapboxProps<StyleT>, currProps: MapboxProps<StyleT>): boolean {
1✔
557
    const map = this._map;
40✔
558
    let changed = false;
40✔
559
    for (const propName of handlerNames) {
40✔
560
      const newValue = nextProps[propName] ?? true;
320!
561
      const oldValue = currProps[propName] ?? true;
320!
562
      if (!deepEqual(newValue, oldValue)) {
320!
563
        changed = true;
×
564
        if (newValue) {
×
565
          map[propName].enable(newValue);
×
566
        } else {
×
567
          map[propName].disable();
×
568
        }
×
569
      }
×
570
    }
320✔
571
    return changed;
40✔
572
  }
40✔
573

1✔
574
  _onEvent = (e: MapEvent<MapT>) => {
1✔
575
    // @ts-ignore
51✔
576
    const cb = this.props[otherEvents[e.type]];
51✔
577
    if (cb) {
51✔
578
      cb(e);
26✔
579
    } else if (e.type === 'error') {
51!
580
      console.error((e as ErrorEvent<MapT>).error); // eslint-disable-line
×
581
    }
×
582
  };
1✔
583

1✔
584
  private _queryRenderedFeatures(point: Point) {
1✔
585
    const map = this._map;
×
586
    const {interactiveLayerIds = []} = this.props;
×
587
    try {
×
588
      return map.queryRenderedFeatures(point, {
×
589
        layers: interactiveLayerIds.filter(map.getLayer.bind(map))
×
590
      });
×
591
    } catch {
×
592
      // May fail if style is not loaded
×
593
      return [];
×
594
    }
×
595
  }
×
596

1✔
597
  _updateHover(e: MapMouseEvent<MapT>) {
1✔
598
    const {props} = this;
×
599
    const shouldTrackHoveredFeatures =
×
600
      props.interactiveLayerIds && (props.onMouseMove || props.onMouseEnter || props.onMouseLeave);
×
601

×
602
    if (shouldTrackHoveredFeatures) {
×
603
      const eventType = e.type;
×
604
      const wasHovering = this._hoveredFeatures?.length > 0;
×
605
      const features = this._queryRenderedFeatures(e.point);
×
606
      const isHovering = features.length > 0;
×
607

×
608
      if (!isHovering && wasHovering) {
×
609
        e.type = 'mouseleave';
×
610
        this._onPointerEvent(e);
×
611
      }
×
612
      this._hoveredFeatures = features;
×
613
      if (isHovering && !wasHovering) {
×
614
        e.type = 'mouseenter';
×
615
        this._onPointerEvent(e);
×
616
      }
×
617
      e.type = eventType;
×
618
    } else {
×
619
      this._hoveredFeatures = null;
×
620
    }
×
621
  }
×
622

1✔
623
  _onPointerEvent = (e: MapMouseEvent<MapT> | MapMouseEvent<MapT>) => {
1✔
624
    if (e.type === 'mousemove' || e.type === 'mouseout') {
×
625
      this._updateHover(e);
×
626
    }
×
627

×
628
    // @ts-ignore
×
629
    const cb = this.props[pointerEvents[e.type]];
×
630
    if (cb) {
×
631
      if (this.props.interactiveLayerIds && e.type !== 'mouseover' && e.type !== 'mouseout') {
×
632
        e.features = this._hoveredFeatures || this._queryRenderedFeatures(e.point);
×
633
      }
×
634
      cb(e);
×
635
      delete e.features;
×
636
    }
×
637
  };
1✔
638

1✔
639
  _onCameraEvent = (e: ViewStateChangeEvent<MapT>) => {
1✔
640
    if (!this._internalUpdate) {
41✔
641
      // @ts-ignore
28✔
642
      const cb = this.props[cameraEvents[e.type]];
28✔
643
      if (cb) {
28✔
644
        cb(e);
10✔
645
      }
10✔
646
    }
28✔
647
    if (e.type in this._deferredEvents) {
41✔
648
      this._deferredEvents[e.type] = false;
33✔
649
    }
33✔
650
  };
1✔
651

1✔
652
  _fireEvent(baseFire: Function, event: string | MapEvent<MapT>, properties?: object) {
1✔
653
    const map = this._map;
92✔
654
    const tr = map.transform;
92✔
655

92✔
656
    const eventType = typeof event === 'string' ? event : event.type;
92✔
657
    if (eventType === 'move') {
92✔
658
      this._updateViewState(this.props, false);
22✔
659
    }
22✔
660
    if (eventType in cameraEvents) {
92✔
661
      if (typeof event === 'object') {
41✔
662
        (event as unknown as ViewStateChangeEvent<MapT>).viewState = transformToViewState(tr);
28✔
663
      }
28✔
664
      if (this._map.isMoving()) {
41✔
665
        // Replace map.transform with ours during the callbacks
35✔
666
        map.transform = this._renderTransform;
35✔
667
        baseFire.call(map, event, properties);
35✔
668
        map.transform = tr;
35✔
669

35✔
670
        return map;
35✔
671
      }
35✔
672
    }
41✔
673
    baseFire.call(map, event, properties);
57✔
674

57✔
675
    return map;
57✔
676
  }
57✔
677

1✔
678
  // All camera manipulations are complete, ready to repaint
1✔
679
  _onBeforeRepaint() {
1✔
680
    const map = this._map;
22✔
681

22✔
682
    // If there are camera changes driven by props, invoke camera events so that DOM controls are synced
22✔
683
    this._internalUpdate = true;
22✔
684
    for (const eventType in this._deferredEvents) {
22✔
685
      if (this._deferredEvents[eventType]) {
88✔
686
        map.fire(eventType);
13✔
687
      }
13✔
688
    }
88✔
689
    this._internalUpdate = false;
22✔
690

22✔
691
    const tr = this._map.transform;
22✔
692
    // Make sure camera matches the current props
22✔
693
    map.transform = this._renderTransform;
22✔
694

22✔
695
    this._onAfterRepaint = () => {
22✔
696
      // Mapbox transitions between non-mercator projection and mercator during render time
22✔
697
      // Copy it back to the other
22✔
698
      syncProjection(this._renderTransform, tr);
22✔
699
      // Restores camera state before render/load events are fired
22✔
700
      map.transform = tr;
22✔
701
    };
22✔
702
  }
22✔
703

1✔
704
  _onAfterRepaint: () => void;
1✔
705
}
1✔
706

1✔
707
/**
1✔
708
 * Access token can be provided via one of:
1✔
709
 *   mapboxAccessToken prop
1✔
710
 *   access_token query parameter
1✔
711
 *   MapboxAccessToken environment variable
1✔
712
 *   REACT_APP_MAPBOX_ACCESS_TOKEN environment variable
1✔
713
 * @returns access token
1✔
714
 */
1✔
715
function getAccessTokenFromEnv(): string {
11✔
716
  let accessToken = null;
11✔
717

11✔
718
  /* global location, process */
11✔
719
  if (typeof location !== 'undefined') {
11!
720
    const match = /access_token=([^&\/]*)/.exec(location.search);
×
721
    accessToken = match && match[1];
×
722
  }
×
723

11✔
724
  // Note: This depends on bundler plugins (e.g. webpack) importing environment correctly
11✔
725
  try {
11✔
726
    accessToken = accessToken || process.env.MapboxAccessToken;
11✔
727
  } catch {
11!
728
    // ignore
×
729
  }
×
730

11✔
731
  try {
11✔
732
    accessToken = accessToken || process.env.REACT_APP_MAPBOX_ACCESS_TOKEN;
11✔
733
  } catch {
11!
734
    // ignore
×
735
  }
×
736

11✔
737
  return accessToken;
11✔
738
}
11✔
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