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

visgl / react-map-gl / 5138709139

pending completion
5138709139

Pull #2183

github

web-flow
Merge 888eead76 into 69426a919
Pull Request #2183: Bump http-cache-semantics from 4.1.0 to 4.1.1 in /website

272 of 336 branches covered (80.95%)

Branch coverage included in aggregate %.

2519 of 2972 relevant lines covered (84.76%)

10.57 hits per line

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

76.91
/src/mapbox/mapbox.ts
1
import {transformToViewState, applyViewStateToTransform, cloneTransform} from '../utils/transform';
2
import {normalizeStyle} from '../utils/style-utils';
3
import {deepEqual} from '../utils/deep-equal';
4

5
import type {
6
  Transform,
7
  ViewState,
8
  ViewStateChangeEvent,
9
  Point,
10
  PointLike,
11
  PaddingOptions,
12
  Light,
13
  Fog,
14
  Terrain,
15
  MapboxStyle,
16
  ImmutableLike,
17
  LngLatBoundsLike,
18
  MapMouseEvent,
19
  MapLayerMouseEvent,
20
  MapLayerTouchEvent,
21
  MapWheelEvent,
22
  MapBoxZoomEvent,
23
  MapStyleDataEvent,
24
  MapSourceDataEvent,
25
  MapEvent,
26
  ErrorEvent,
27
  MapGeoJSONFeature,
28
  MapInstance,
29
  MapInstanceInternal
30
} from '../types';
31

32
export type MapboxProps<MapT extends MapInstance = MapInstance> = Partial<ViewState> & {
33
  // Init options
34
  mapboxAccessToken?: string;
35

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

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

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

58
  // Styling
59

60
  /** Mapbox style */
61
  mapStyle?: string | MapboxStyle | ImmutableLike<MapboxStyle>;
62
  /** Enable diffing when the map style changes
63
   * @default true
64
   */
65
  styleDiffing?: boolean;
66

67
  /** Default layers to query on pointer events */
68
  interactiveLayerIds?: string[];
69
  /** CSS cursor */
70
  cursor?: string;
71

72
  // Callbacks
73
  onMouseDown?: (e: MapLayerMouseEvent<MapT>) => void;
74
  onMouseUp?: (e: MapLayerMouseEvent<MapT>) => void;
75
  onMouseOver?: (e: MapLayerMouseEvent<MapT>) => void;
76
  onMouseMove?: (e: MapLayerMouseEvent<MapT>) => void;
77
  onClick?: (e: MapLayerMouseEvent<MapT>) => void;
78
  onDblClick?: (e: MapLayerMouseEvent<MapT>) => void;
79
  onMouseEnter?: (e: MapLayerMouseEvent<MapT>) => void;
80
  onMouseLeave?: (e: MapLayerMouseEvent<MapT>) => void;
81
  onMouseOut?: (e: MapLayerMouseEvent<MapT>) => void;
82
  onContextMenu?: (e: MapLayerMouseEvent<MapT>) => void;
83
  onTouchStart?: (e: MapLayerTouchEvent<MapT>) => void;
84
  onTouchEnd?: (e: MapLayerTouchEvent<MapT>) => void;
85
  onTouchMove?: (e: MapLayerTouchEvent<MapT>) => void;
86
  onTouchCancel?: (e: MapLayerTouchEvent<MapT>) => void;
87

88
  onMoveStart?: (e: ViewStateChangeEvent<MapT>) => void;
89
  onMove?: (e: ViewStateChangeEvent<MapT>) => void;
90
  onMoveEnd?: (e: ViewStateChangeEvent<MapT>) => void;
91
  onDragStart?: (e: ViewStateChangeEvent<MapT>) => void;
92
  onDrag?: (e: ViewStateChangeEvent<MapT>) => void;
93
  onDragEnd?: (e: ViewStateChangeEvent<MapT>) => void;
94
  onZoomStart?: (e: ViewStateChangeEvent<MapT>) => void;
95
  onZoom?: (e: ViewStateChangeEvent<MapT>) => void;
96
  onZoomEnd?: (e: ViewStateChangeEvent<MapT>) => void;
97
  onRotateStart?: (e: ViewStateChangeEvent<MapT>) => void;
98
  onRotate?: (e: ViewStateChangeEvent<MapT>) => void;
99
  onRotateEnd?: (e: ViewStateChangeEvent<MapT>) => void;
100
  onPitchStart?: (e: ViewStateChangeEvent<MapT>) => void;
101
  onPitch?: (e: ViewStateChangeEvent<MapT>) => void;
102
  onPitchEnd?: (e: ViewStateChangeEvent<MapT>) => void;
103

104
  onWheel?: (e: MapWheelEvent<MapT>) => void;
105
  onBoxZoomStart?: (e: MapBoxZoomEvent<MapT>) => void;
106
  onBoxZoomEnd?: (e: MapBoxZoomEvent<MapT>) => void;
107
  onBoxZoomCancel?: (e: MapBoxZoomEvent<MapT>) => void;
108

109
  onResize?: (e: MapEvent<MapT>) => void;
110
  onLoad?: (e: MapEvent<MapT>) => void;
111
  onRender?: (e: MapEvent<MapT>) => void;
112
  onIdle?: (e: MapEvent<MapT>) => void;
113
  onError?: (e: ErrorEvent<MapT>) => void;
114
  onRemove?: (e: MapEvent<MapT>) => void;
115
  onData?: (e: MapStyleDataEvent<MapT> | MapSourceDataEvent<MapT>) => void;
116
  onStyleData?: (e: MapStyleDataEvent<MapT>) => void;
117
  onSourceData?: (e: MapSourceDataEvent<MapT>) => void;
118
};
119

120
const DEFAULT_STYLE = {version: 8, sources: {}, layers: []} as MapboxStyle;
121

122
const pointerEvents = {
123
  mousedown: 'onMouseDown',
124
  mouseup: 'onMouseUp',
125
  mouseover: 'onMouseOver',
126
  mousemove: 'onMouseMove',
127
  click: 'onClick',
128
  dblclick: 'onDblClick',
129
  mouseenter: 'onMouseEnter',
130
  mouseleave: 'onMouseLeave',
131
  mouseout: 'onMouseOut',
132
  contextmenu: 'onContextMenu',
133
  touchstart: 'onTouchStart',
134
  touchend: 'onTouchEnd',
135
  touchmove: 'onTouchMove',
136
  touchcancel: 'onTouchCancel'
137
};
138
const cameraEvents = {
139
  movestart: 'onMoveStart',
140
  move: 'onMove',
141
  moveend: 'onMoveEnd',
142
  dragstart: 'onDragStart',
143
  drag: 'onDrag',
144
  dragend: 'onDragEnd',
145
  zoomstart: 'onZoomStart',
146
  zoom: 'onZoom',
147
  zoomend: 'onZoomEnd',
148
  rotatestart: 'onRotateStart',
149
  rotate: 'onRotate',
150
  rotateend: 'onRotateEnd',
151
  pitchstart: 'onPitchStart',
152
  pitch: 'onPitch',
153
  pitchend: 'onPitchEnd'
154
};
155
const otherEvents = {
156
  wheel: 'onWheel',
157
  boxzoomstart: 'onBoxZoomStart',
158
  boxzoomend: 'onBoxZoomEnd',
159
  boxzoomcancel: 'onBoxZoomCancel',
160
  resize: 'onResize',
161
  load: 'onLoad',
162
  render: 'onRender',
163
  idle: 'onIdle',
164
  remove: 'onRemove',
165
  data: 'onData',
166
  styledata: 'onStyleData',
167
  sourcedata: 'onSourceData',
168
  error: 'onError'
169
};
170
const settingNames = [
171
  'minZoom',
172
  'maxZoom',
173
  'minPitch',
174
  'maxPitch',
175
  'maxBounds',
176
  'projection',
177
  'renderWorldCopies'
178
];
179
const handlerNames = [
180
  'scrollZoom',
181
  'boxZoom',
182
  'dragRotate',
183
  'dragPan',
184
  'keyboard',
185
  'doubleClickZoom',
186
  'touchZoomRotate',
187
  'touchPitch'
188
];
189

190
/**
191
 * A wrapper for mapbox-gl's Map class
192
 */
193
export default class Mapbox<MapT extends MapInstance = MapInstance> {
194
  private _MapClass: {new (options: any): MapInstance};
195
  // mapboxgl.Map instance
196
  private _map: MapInstanceInternal<MapT> = null;
197
  // User-supplied props
198
  props: MapboxProps<MapT>;
199

200
  // Mapbox map is stateful.
201
  // During method calls/user interactions, map.transform is mutated and
202
  // deviate from user-supplied props.
203
  // In order to control the map reactively, we shadow the transform
204
  // with the one below, which reflects the view state resolved from
205
  // both user-supplied props and the underlying state
206
  private _renderTransform: Transform;
207

208
  // Internal states
209
  private _internalUpdate: boolean = false;
210
  private _inRender: boolean = false;
211
  private _hoveredFeatures: MapGeoJSONFeature[] = null;
212
  private _deferredEvents: {
213
    move: boolean;
214
    zoom: boolean;
215
    pitch: boolean;
216
    rotate: boolean;
217
  } = {
218
    move: false,
219
    zoom: false,
220
    pitch: false,
221
    rotate: false
222
  };
223

224
  static savedMaps: Mapbox[] = [];
225

226
  constructor(
11✔
227
    MapClass: {new (options: any): MapInstance},
228
    props: MapboxProps<MapT>,
229
    container: HTMLDivElement
230
  ) {
231
    this._MapClass = MapClass;
232
    this.props = props;
233
    this._initialize(container);
234
  }
235

236
  get map(): MapT {
11✔
237
    return this._map;
238
  }
239

240
  get transform(): Transform {
22✔
241
    return this._renderTransform;
242
  }
243

244
  setProps(props: MapboxProps<MapT>) {
40✔
245
    const oldProps = this.props;
246
    this.props = props;
247

248
    const settingsChanged = this._updateSettings(props, oldProps);
249
    if (settingsChanged) {
×
250
      this._createShadowTransform(this._map);
251
    }
252
    const sizeChanged = this._updateSize(props);
253
    const viewStateChanged = this._updateViewState(props, true);
254
    this._updateStyle(props, oldProps);
255
    this._updateStyleComponents(props, oldProps);
256
    this._updateHandlers(props, oldProps);
257

258
    // If 1) view state has changed to match props and
259
    //    2) the props change is not triggered by map events,
260
    // it's driven by an external state change. Redraw immediately
261
    if (settingsChanged || sizeChanged || (viewStateChanged && !this._map.isMoving())) {
13✔
262
      this.redraw();
263
    }
264
  }
265

266
  static reuse<MapT extends MapInstance>(
267
    props: MapboxProps<MapT>,
268
    container: HTMLDivElement
269
  ): Mapbox<MapT> {
270
    const that = Mapbox.savedMaps.pop() as Mapbox<MapT>;
271
    if (!that) {
272
      return null;
273
    }
274

275
    const map = that.map;
276
    // When reusing the saved map, we need to reparent the map(canvas) and other child nodes
277
    // intoto the new container from the props.
278
    // Step 1: reparenting child nodes from old container to new container
279
    const oldContainer = map.getContainer();
280
    container.className = oldContainer.className;
281
    while (oldContainer.childNodes.length > 0) {
282
      container.appendChild(oldContainer.childNodes[0]);
283
    }
284
    // Step 2: replace the internal container with new container from the react component
285
    // @ts-ignore
286
    map._container = container;
287

288
    // With maplibre-gl as mapLib, map uses ResizeObserver to observe when its container resizes.
289
    // When reusing the saved map, we need to disconnect the observer and observe the new container.
290
    // Step 3: telling the ResizeObserver to disconnect and observe the new container
291
    // @ts-ignore
292
    const resizeObserver = map._resizeObserver;
293
    if (resizeObserver) {
294
      resizeObserver.disconnect();
295
      resizeObserver.observe(container);
296
    }
297

298
    // Step 4: apply new props
299
    that.setProps({...props, styleDiffing: false});
300
    map.resize();
301
    const {initialViewState} = props;
302
    if (initialViewState) {
303
      if (initialViewState.bounds) {
304
        map.fitBounds(initialViewState.bounds, {...initialViewState.fitBoundsOptions, duration: 0});
305
      } else {
306
        that._updateViewState(initialViewState, false);
307
      }
308
    }
309

310
    // Simulate load event
311
    if (map.isStyleLoaded()) {
312
      map.fire('load');
313
    } else {
314
      map.once('styledata', () => map.fire('load'));
315
    }
316

317
    // Force reload
318
    // @ts-ignore
319
    map._update();
320
    return that;
321
  }
322

323
  /* eslint-disable complexity,max-statements */
324
  _initialize(container: HTMLDivElement) {
11✔
325
    const {props} = this;
326
    const {mapStyle = DEFAULT_STYLE} = props;
327
    const mapOptions = {
328
      ...props,
329
      ...props.initialViewState,
330
      accessToken: props.mapboxAccessToken || getAccessTokenFromEnv() || null,
331
      container,
332
      style: normalizeStyle(mapStyle)
333
    };
334

335
    const viewState = mapOptions.initialViewState || mapOptions.viewState || mapOptions;
18✔
336
    Object.assign(mapOptions, {
337
      center: [viewState.longitude || 0, viewState.latitude || 0],
12✔
338
      zoom: viewState.zoom || 0,
6✔
339
      pitch: viewState.pitch || 0,
340
      bearing: viewState.bearing || 0
341
    });
342

343
    if (props.gl) {
×
344
      // eslint-disable-next-line
345
      const getContext = HTMLCanvasElement.prototype.getContext;
346
      // Hijack canvas.getContext to return our own WebGLContext
347
      // This will be called inside the mapboxgl.Map constructor
348
      // @ts-expect-error
349
      HTMLCanvasElement.prototype.getContext = () => {
350
        // Unhijack immediately
351
        HTMLCanvasElement.prototype.getContext = getContext;
352
        return props.gl;
353
      };
354
    }
355

356
    const map = new this._MapClass(mapOptions) as MapInstanceInternal<MapT>;
357
    // Props that are not part of constructor options
358
    if (viewState.padding) {
×
359
      map.setPadding(viewState.padding);
360
    }
361
    if (props.cursor) {
×
362
      map.getCanvas().style.cursor = props.cursor;
363
    }
364
    this._createShadowTransform(map);
365

366
    // Hack
367
    // Insert code into map's render cycle
368
    const renderMap = map._render;
369
    map._render = (arg: number) => {
22✔
370
      this._inRender = true;
371
      renderMap.call(map, arg);
372
      this._inRender = false;
373
    };
374
    const runRenderTaskQueue = map._renderTaskQueue.run;
375
    map._renderTaskQueue.run = (arg: number) => {
22✔
376
      runRenderTaskQueue.call(map._renderTaskQueue, arg);
377
      this._onBeforeRepaint();
378
    };
379
    map.on('render', () => this._onAfterRepaint());
22✔
380
    // Insert code into map's event pipeline
381
    // eslint-disable-next-line @typescript-eslint/unbound-method
382
    const fireEvent = map.fire;
383
    map.fire = this._fireEvent.bind(this, fireEvent);
384

385
    // add listeners
386
    map.on('resize', () => {
387
      this._renderTransform.resize(map.transform.width, map.transform.height);
388
    });
389
    map.on('styledata', () => this._updateStyleComponents(this.props, {}));
13✔
390
    map.on('sourcedata', () => this._updateStyleComponents(this.props, {}));
391
    for (const eventName in pointerEvents) {
154✔
392
      map.on(eventName, this._onPointerEvent);
393
    }
394
    for (const eventName in cameraEvents) {
165✔
395
      map.on(eventName, this._onCameraEvent);
396
    }
397
    for (const eventName in otherEvents) {
143✔
398
      map.on(eventName, this._onEvent);
399
    }
400
    this._map = map;
401
  }
402
  /* eslint-enable complexity,max-statements */
403

404
  recycle() {
405
    // Clean up unnecessary elements before storing for reuse.
406
    const container = this.map.getContainer();
407
    const children = container.querySelector('[mapboxgl-children]');
408
    children?.remove();
409

410
    Mapbox.savedMaps.push(this);
411
  }
412

413
  destroy() {
6✔
414
    this._map.remove();
415
  }
416

417
  // Force redraw the map now. Typically resize() and jumpTo() is reflected in the next
418
  // render cycle, which is managed by Mapbox's animation loop.
419
  // This removes the synchronization issue caused by requestAnimationFrame.
420
  redraw() {
2✔
421
    const map = this._map as any;
422
    // map._render will throw error if style does not exist
423
    // https://github.com/mapbox/mapbox-gl-js/blob/fb9fc316da14e99ff4368f3e4faa3888fb43c513
424
    //   /src/ui/map.js#L1834
425
    if (!this._inRender && map.style) {
426
      // cancel the scheduled update
427
      if (map._frame) {
×
428
        map._frame.cancel();
429
        map._frame = null;
430
      }
431
      // the order is important - render() may schedule another update
432
      map._render();
433
    }
434
  }
435

436
  _createShadowTransform(map: any) {
11✔
437
    const renderTransform = cloneTransform(map.transform);
438
    map.painter.transform = renderTransform;
439

440
    this._renderTransform = renderTransform;
441
  }
442

443
  /* Trigger map resize if size is controlled
444
     @param {object} nextProps
445
     @returns {bool} true if size has changed
446
   */
447
  _updateSize(nextProps: MapboxProps<MapT>): boolean {
40✔
448
    // Check if size is controlled
449
    const {viewState} = nextProps;
450
    if (viewState) {
×
451
      const map = this._map;
452
      if (viewState.width !== map.transform.width || viewState.height !== map.transform.height) {
453
        map.resize();
454
        return true;
455
      }
456
    }
457
    return false;
458
  }
459

460
  // Adapted from map.jumpTo
461
  /* Update camera to match props
462
     @param {object} nextProps
463
     @param {bool} triggerEvents - should fire camera events
464
     @returns {bool} true if anything is changed
465
   */
466
  _updateViewState(nextProps: MapboxProps<MapT>, triggerEvents: boolean): boolean {
62✔
467
    if (this._internalUpdate) {
2✔
468
      return false;
469
    }
470
    const map = this._map;
60✔
471

472
    const tr = this._renderTransform;
473
    // Take a snapshot of the transform before mutation
474
    const {zoom, pitch, bearing} = tr;
475
    const isMoving = map.isMoving();
476

477
    if (isMoving) {
29✔
478
      // All movement of the camera is done relative to the sea level
479
      tr.cameraElevationReference = 'sea';
480
    }
481
    const changed = applyViewStateToTransform(tr, {
60✔
482
      ...transformToViewState(map.transform),
483
      ...nextProps
484
    });
485
    if (isMoving) {
29✔
486
      // Reset camera reference
487
      tr.cameraElevationReference = 'ground';
488
    }
489

60✔
490
    if (changed && triggerEvents) {
27✔
491
      const deferredEvents = this._deferredEvents;
492
      // Delay DOM control updates to the next render cycle
493
      deferredEvents.move = true;
494
      deferredEvents.zoom ||= zoom !== tr.zoom;
495
      deferredEvents.rotate ||= bearing !== tr.bearing;
496
      deferredEvents.pitch ||= pitch !== tr.pitch;
497
    }
498

60✔
499
    // Avoid manipulating the real transform when interaction/animation is ongoing
500
    // as it would interfere with Mapbox's handlers
501
    if (!isMoving) {
31✔
502
      applyViewStateToTransform(map.transform, nextProps);
503
    }
504

60✔
505
    return changed;
506
  }
507

508
  /* Update camera constraints and projection settings to match props
509
     @param {object} nextProps
510
     @param {object} currProps
511
     @returns {bool} true if anything is changed
512
   */
513
  _updateSettings(nextProps: MapboxProps<MapT>, currProps: MapboxProps<MapT>): boolean {
40✔
514
    const map = this._map;
515
    let changed = false;
516
    for (const propName of settingNames) {
280✔
517
      if (propName in nextProps && !deepEqual(nextProps[propName], currProps[propName])) {
×
518
        changed = true;
519
        const setter = map[`set${propName[0].toUpperCase()}${propName.slice(1)}`];
520
        setter?.(nextProps[propName]);
521
      }
522
    }
523
    return changed;
524
  }
525

526
  /* Update map style to match props
527
     @param {object} nextProps
528
     @param {object} currProps
529
     @returns {bool} true if style is changed
530
   */
531
  _updateStyle(nextProps: MapboxProps<MapT>, currProps: MapboxProps<MapT>): boolean {
40✔
532
    if (nextProps.cursor !== currProps.cursor) {
×
533
      this._map.getCanvas().style.cursor = nextProps.cursor;
534
    }
535
    if (nextProps.mapStyle !== currProps.mapStyle) {
2✔
536
      const {mapStyle = DEFAULT_STYLE, styleDiffing = true} = nextProps;
537
      const options: any = {
538
        diff: styleDiffing
539
      };
540
      if ('localIdeographFontFamily' in nextProps) {
×
541
        // @ts-ignore Mapbox specific prop
542
        options.localIdeographFontFamily = nextProps.localIdeographFontFamily;
543
      }
544
      this._map.setStyle(normalizeStyle(mapStyle), options);
545
      return true;
546
    }
547
    return false;
38✔
548
  }
549

550
  /* Update fog, light and terrain to match props
551
     @param {object} nextProps
552
     @param {object} currProps
553
     @returns {bool} true if anything is changed
554
   */
555
  _updateStyleComponents(
53✔
556
    nextProps: MapboxProps<MapT> & {
557
      light?: Light;
558
      fog?: Fog;
559
      terrain?: Terrain;
560
    },
561
    currProps: MapboxProps<MapT> & {
562
      light?: Light;
563
      fog?: Fog;
564
      terrain?: Terrain;
565
    }
566
  ): boolean {
567
    const map = this._map;
568
    let changed = false;
569
    if (map.isStyleLoaded()) {
37✔
570
      if ('light' in nextProps && map.setLight && !deepEqual(nextProps.light, currProps.light)) {
×
571
        changed = true;
572
        map.setLight(nextProps.light);
573
      }
574
      if ('fog' in nextProps && map.setFog && !deepEqual(nextProps.fog, currProps.fog)) {
×
575
        changed = true;
576
        map.setFog(nextProps.fog);
577
      }
578
      if (
579
        'terrain' in nextProps &&
×
580
        map.setTerrain &&
×
581
        !deepEqual(nextProps.terrain, currProps.terrain)
582
      ) {
×
583
        if (!nextProps.terrain || map.getSource(nextProps.terrain.source)) {
584
          changed = true;
585
          map.setTerrain(nextProps.terrain);
586
        }
587
      }
588
    }
589
    return changed;
590
  }
591

592
  /* Update interaction handlers to match props
593
     @param {object} nextProps
594
     @param {object} currProps
595
     @returns {bool} true if anything is changed
596
   */
597
  _updateHandlers(nextProps: MapboxProps<MapT>, currProps: MapboxProps<MapT>): boolean {
40✔
598
    const map = this._map;
599
    let changed = false;
600
    for (const propName of handlerNames) {
320✔
601
      const newValue = nextProps[propName] ?? true;
×
602
      const oldValue = currProps[propName] ?? true;
×
603
      if (!deepEqual(newValue, oldValue)) {
×
604
        changed = true;
605
        if (newValue) {
606
          map[propName].enable(newValue);
607
        } else {
608
          map[propName].disable();
609
        }
610
      }
611
    }
612
    return changed;
613
  }
614

615
  _onEvent = (e: MapEvent<MapT>) => {
51✔
616
    // @ts-ignore
617
    const cb = this.props[otherEvents[e.type]];
618
    if (cb) {
26✔
619
      cb(e);
620
    } else if (e.type === 'error') {
25!
621
      console.error((e as ErrorEvent<MapT>).error); // eslint-disable-line
622
    }
623
  };
624

625
  private _queryRenderedFeatures(point: Point) {
626
    const map = this._map;
627
    const {interactiveLayerIds = []} = this.props;
628
    try {
629
      return map.queryRenderedFeatures(point, {
630
        layers: interactiveLayerIds.filter(map.getLayer.bind(map))
631
      });
632
    } catch {
633
      // May fail if style is not loaded
634
      return [];
635
    }
636
  }
637

638
  _updateHover(e: MapMouseEvent<MapT>) {
639
    const {props} = this;
640
    const shouldTrackHoveredFeatures =
641
      props.interactiveLayerIds && (props.onMouseMove || props.onMouseEnter || props.onMouseLeave);
642

643
    if (shouldTrackHoveredFeatures) {
644
      const eventType = e.type;
645
      const wasHovering = this._hoveredFeatures?.length > 0;
646
      const features = this._queryRenderedFeatures(e.point);
647
      const isHovering = features.length > 0;
648

649
      if (!isHovering && wasHovering) {
650
        e.type = 'mouseleave';
651
        this._onPointerEvent(e);
652
      }
653
      this._hoveredFeatures = features;
654
      if (isHovering && !wasHovering) {
655
        e.type = 'mouseenter';
656
        this._onPointerEvent(e);
657
      }
658
      e.type = eventType;
659
    } else {
660
      this._hoveredFeatures = null;
661
    }
662
  }
663

664
  _onPointerEvent = (e: MapLayerMouseEvent<MapT> | MapLayerTouchEvent<MapT>) => {
665
    if (e.type === 'mousemove' || e.type === 'mouseout') {
666
      this._updateHover(e);
667
    }
668

669
    // @ts-ignore
670
    const cb = this.props[pointerEvents[e.type]];
671
    if (cb) {
672
      if (this.props.interactiveLayerIds && e.type !== 'mouseover' && e.type !== 'mouseout') {
673
        e.features = this._hoveredFeatures || this._queryRenderedFeatures(e.point);
674
      }
675
      cb(e);
676
      delete e.features;
677
    }
678
  };
679

680
  _onCameraEvent = (e: ViewStateChangeEvent<MapT>) => {
41✔
681
    if (!this._internalUpdate) {
28✔
682
      // @ts-ignore
683
      const cb = this.props[cameraEvents[e.type]];
684
      if (cb) {
10✔
685
        cb(e);
686
      }
687
    }
688
    if (e.type in this._deferredEvents) {
33✔
689
      this._deferredEvents[e.type] = false;
690
    }
691
  };
692

693
  _fireEvent(baseFire: Function, event: string | MapEvent<MapT>, properties?: object) {
92✔
694
    const map = this._map;
695
    const tr = map.transform;
696

697
    const eventType = typeof event === 'string' ? event : event.type;
92✔
698
    if (eventType === 'move') {
22✔
699
      this._updateViewState(this.props, false);
700
    }
701
    if (eventType in cameraEvents) {
41✔
702
      if (typeof event === 'object') {
28✔
703
        (event as unknown as ViewStateChangeEvent<MapT>).viewState = transformToViewState(tr);
704
      }
705
      if (this._map.isMoving()) {
35✔
706
        // Replace map.transform with ours during the callbacks
707
        map.transform = this._renderTransform;
708
        baseFire.call(map, event, properties);
709
        map.transform = tr;
710

711
        return map;
712
      }
713
    }
714
    baseFire.call(map, event, properties);
57✔
715

716
    return map;
717
  }
718

719
  // All camera manipulations are complete, ready to repaint
720
  _onBeforeRepaint() {
22✔
721
    const map = this._map;
722

723
    // If there are camera changes driven by props, invoke camera events so that DOM controls are synced
724
    this._internalUpdate = true;
725
    for (const eventType in this._deferredEvents) {
88✔
726
      if (this._deferredEvents[eventType]) {
13✔
727
        map.fire(eventType);
728
      }
729
    }
730
    this._internalUpdate = false;
731

732
    const tr = this._map.transform;
733
    // Make sure camera matches the current props
734
    this._map.transform = this._renderTransform;
735

736
    this._onAfterRepaint = () => {
22✔
737
      // Restores camera state before render/load events are fired
738
      this._map.transform = tr;
739
    };
740
  }
741

742
  _onAfterRepaint: () => void;
743
}
744

745
/**
746
 * Access token can be provided via one of:
747
 *   mapboxAccessToken prop
748
 *   access_token query parameter
749
 *   MapboxAccessToken environment variable
750
 *   REACT_APP_MAPBOX_ACCESS_TOKEN environment variable
751
 * @returns access token
752
 */
753
function getAccessTokenFromEnv(): string {
11✔
754
  let accessToken = null;
755

756
  /* global location, process */
757
  if (typeof location !== 'undefined') {
×
758
    const match = /access_token=([^&\/]*)/.exec(location.search);
759
    accessToken = match && match[1];
760
  }
761

762
  // Note: This depends on bundler plugins (e.g. webpack) importing environment correctly
763
  try {
764
    accessToken = accessToken || process.env.MapboxAccessToken;
765
  } catch {
×
766
    // ignore
767
  }
768

769
  try {
770
    accessToken = accessToken || process.env.REACT_APP_MAPBOX_ACCESS_TOKEN;
771
  } catch {
×
772
    // ignore
773
  }
774

775
  return accessToken;
776
}
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