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

visgl / react-map-gl / 14266442401

04 Apr 2025 01:27PM UTC coverage: 85.014% (-0.02%) from 85.035%
14266442401

Pull #2521

github

web-flow
Merge ca4c3ba8a into 129848b62
Pull Request #2521: fix(geocoder): add styles for example

939 of 1164 branches covered (80.67%)

Branch coverage included in aggregate %.

5562 of 6483 relevant lines covered (85.79%)

58.18 hits per line

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

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

1✔
156
/**
1✔
157
 * A wrapper for mapbox-gl's Map class
1✔
158
 */
1✔
159
export default class Mapbox {
1✔
160
  private _MapClass: {new (options: any): MapInstance};
1✔
161
  /** mapboxgl.Map instance */
1✔
162
  private _map: MapInstance = null;
1✔
163
  /** User-supplied props */
1✔
164
  props: MapboxProps;
1✔
165

1✔
166
  /** The transform that replaces native map.transform to resolve changes vs. React props
1✔
167
   * See proxy-transform.ts
1✔
168
   */
1✔
169
  private _proxyTransform: ProxyTransform;
1✔
170

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

1✔
194
  static savedMaps: Mapbox[] = [];
1✔
195

1✔
196
  constructor(
1✔
197
    MapClass: {new (options: any): MapInstance},
13✔
198
    props: MapboxProps,
13✔
199
    container: HTMLDivElement
13✔
200
  ) {
13✔
201
    this._MapClass = MapClass;
13✔
202
    this.props = props;
13✔
203
    this._initialize(container);
13✔
204
  }
13✔
205

1✔
206
  get map(): MapInstance {
1✔
207
    return this._map;
13✔
208
  }
13✔
209

1✔
210
  get transform(): Transform {
1✔
211
    return this._map.transform;
36✔
212
  }
36✔
213

1✔
214
  setProps(props: MapboxProps) {
1✔
215
    const oldProps = this.props;
44✔
216
    this.props = props;
44✔
217

44✔
218
    const settingsChanged = this._updateSettings(props, oldProps);
44✔
219
    if (settingsChanged) {
44!
220
      this._createProxyTransform(this._map);
×
221
    }
×
222
    const sizeChanged = this._updateSize(props);
44✔
223
    const viewStateChanged = this._updateViewState(props, true);
44✔
224
    this._updateStyle(props, oldProps);
44✔
225
    this._updateStyleComponents(props, oldProps);
44✔
226
    this._updateHandlers(props, oldProps);
44✔
227

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

1✔
236
  static reuse(props: MapboxProps, container: HTMLDivElement): Mapbox {
1✔
237
    const that = Mapbox.savedMaps.pop();
×
238
    if (!that) {
×
239
      return null;
×
240
    }
×
241

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

×
255
    // Step 4: apply new props
×
256
    that.setProps({...props, styleDiffing: false});
×
257
    map.resize();
×
258
    const {initialViewState} = props;
×
259
    if (initialViewState) {
×
260
      if (initialViewState.bounds) {
×
261
        map.fitBounds(initialViewState.bounds, {...initialViewState.fitBoundsOptions, duration: 0});
×
262
      } else {
×
263
        that._updateViewState(initialViewState, false);
×
264
      }
×
265
    }
×
266

×
267
    // Simulate load event
×
268
    if (map.isStyleLoaded()) {
×
269
      map.fire('load');
×
270
    } else {
×
271
      map.once('styledata', () => map.fire('load'));
×
272
    }
×
273

×
274
    // Force reload
×
275
    // @ts-ignore
×
276
    map._update();
×
277
    return that;
×
278
  }
×
279

1✔
280
  /* eslint-disable complexity,max-statements */
1✔
281
  _initialize(container: HTMLDivElement) {
1✔
282
    const {props} = this;
13✔
283
    const {mapStyle = DEFAULT_STYLE} = props;
13✔
284
    const mapOptions = {
13✔
285
      ...props,
13✔
286
      ...props.initialViewState,
13✔
287
      accessToken: props.mapboxAccessToken || getAccessTokenFromEnv() || null,
13✔
288
      container,
13✔
289
      style: normalizeStyle(mapStyle)
13✔
290
    };
13✔
291

13✔
292
    const viewState = mapOptions.initialViewState || mapOptions.viewState || mapOptions;
13✔
293
    Object.assign(mapOptions, {
13✔
294
      center: [viewState.longitude || 0, viewState.latitude || 0],
13✔
295
      zoom: viewState.zoom || 0,
13✔
296
      pitch: viewState.pitch || 0,
13✔
297
      bearing: viewState.bearing || 0
13✔
298
    });
13✔
299

13✔
300
    if (props.gl) {
13!
301
      // eslint-disable-next-line
×
302
      const getContext = HTMLCanvasElement.prototype.getContext;
×
303
      // Hijack canvas.getContext to return our own WebGLContext
×
304
      // This will be called inside the mapboxgl.Map constructor
×
305
      // @ts-expect-error
×
306
      HTMLCanvasElement.prototype.getContext = () => {
×
307
        // Unhijack immediately
×
308
        HTMLCanvasElement.prototype.getContext = getContext;
×
309
        return props.gl;
×
310
      };
×
311
    }
×
312

13✔
313
    const map = new this._MapClass(mapOptions);
13✔
314
    // Props that are not part of constructor options
13✔
315
    if (viewState.padding) {
13!
316
      map.setPadding(viewState.padding);
×
317
    }
×
318
    if (props.cursor) {
13!
319
      map.getCanvas().style.cursor = props.cursor;
×
320
    }
×
321
    this._createProxyTransform(map);
13✔
322

13✔
323
    // Hack
13✔
324
    // Insert code into map's render cycle
13✔
325
    // eslint-disable-next-line @typescript-eslint/unbound-method
13✔
326
    const renderMap = map._render;
13✔
327
    map._render = (arg: number) => {
13✔
328
      // Hijacked to set this state flag
65✔
329
      this._inRender = true;
65✔
330
      renderMap.call(map, arg);
65✔
331
      this._inRender = false;
65✔
332
    };
65✔
333
    // eslint-disable-next-line @typescript-eslint/unbound-method
13✔
334
    const runRenderTaskQueue = map._renderTaskQueue.run;
13✔
335
    map._renderTaskQueue.run = (arg: number) => {
13✔
336
      // This is where camera updates from input handler/animation happens
65✔
337
      // And where all view state change events are fired
65✔
338
      this._proxyTransform.$internalUpdate = true;
65✔
339
      runRenderTaskQueue.call(map._renderTaskQueue, arg);
65✔
340
      this._proxyTransform.$internalUpdate = false;
65✔
341
      this._fireDefferedEvents();
65✔
342
    };
65✔
343
    // Insert code into map's event pipeline
13✔
344
    // eslint-disable-next-line @typescript-eslint/unbound-method
13✔
345
    const fireEvent = map.fire;
13✔
346
    map.fire = this._fireEvent.bind(this, fireEvent);
13✔
347

13✔
348
    // add listeners
13✔
349
    map.on('styledata', () => {
13✔
350
      this._updateStyleComponents(this.props, {});
32✔
351
    });
13✔
352
    map.on('sourcedata', () => {
13✔
353
      this._updateStyleComponents(this.props, {});
14✔
354
    });
13✔
355
    for (const eventName in pointerEvents) {
13✔
356
      map.on(eventName, this._onPointerEvent);
182✔
357
    }
182✔
358
    for (const eventName in cameraEvents) {
13✔
359
      map.on(eventName, this._onCameraEvent);
195✔
360
    }
195✔
361
    for (const eventName in otherEvents) {
13✔
362
      map.on(eventName, this._onEvent);
169✔
363
    }
169✔
364
    this._map = map;
13✔
365
  }
13✔
366
  /* eslint-enable complexity,max-statements */
1✔
367

1✔
368
  recycle() {
1✔
369
    // Clean up unnecessary elements before storing for reuse.
×
370
    const container = this.map.getContainer();
×
371
    const children = container.querySelector('[mapboxgl-children]');
×
372
    children?.remove();
×
373

×
374
    Mapbox.savedMaps.push(this);
×
375
  }
×
376

1✔
377
  destroy() {
1✔
378
    this._map.remove();
12✔
379
  }
12✔
380

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

1✔
400
  _createProxyTransform(map: any) {
1✔
401
    const proxyTransform = createProxyTransform(map.transform);
13✔
402
    map.transform = proxyTransform;
13✔
403
    map.painter.transform = proxyTransform;
13✔
404
    this._proxyTransform = proxyTransform;
13✔
405
  }
13✔
406

1✔
407
  /* Trigger map resize if size is controlled
1✔
408
     @param {object} nextProps
1✔
409
     @returns {bool} true if size has changed
1✔
410
   */
1✔
411
  _updateSize(nextProps: MapboxProps): boolean {
1✔
412
    // Check if size is controlled
44✔
413
    const {viewState} = nextProps;
44✔
414
    if (viewState) {
44!
415
      const map = this._map;
×
416
      if (viewState.width !== map.transform.width || viewState.height !== map.transform.height) {
×
417
        map.resize();
×
418
        return true;
×
419
      }
×
420
    }
×
421
    return false;
44✔
422
  }
44✔
423

1✔
424
  // Adapted from map.jumpTo
1✔
425
  /* Update camera to match props
1✔
426
     @param {object} nextProps
1✔
427
     @param {bool} triggerEvents - should fire camera events
1✔
428
     @returns {bool} true if anything is changed
1✔
429
   */
1✔
430
  _updateViewState(nextProps: MapboxProps, triggerEvents: boolean): boolean {
1✔
431
    const viewState: Partial<ViewState> = nextProps.viewState || nextProps;
44✔
432
    const tr = this._proxyTransform;
44✔
433
    const {zoom, pitch, bearing} = tr;
44✔
434
    const changed = compareViewStateWithTransform(this._proxyTransform, viewState);
44✔
435
    tr.$reactViewState = viewState;
44✔
436

44✔
437
    if (changed && triggerEvents) {
44✔
438
      const deferredEvents = this._deferredEvents;
13✔
439
      // Delay DOM control updates to the next render cycle
13✔
440
      deferredEvents.move = true;
13✔
441
      deferredEvents.zoom ||= zoom !== tr.zoom;
13✔
442
      deferredEvents.rotate ||= bearing !== tr.bearing;
13✔
443
      deferredEvents.pitch ||= pitch !== tr.pitch;
13✔
444
    }
13✔
445

44✔
446
    return changed;
44✔
447
  }
44✔
448

1✔
449
  /* Update camera constraints and projection settings to match props
1✔
450
     @param {object} nextProps
1✔
451
     @param {object} currProps
1✔
452
     @returns {bool} true if anything is changed
1✔
453
   */
1✔
454
  _updateSettings(nextProps: MapboxProps, currProps: MapboxProps): boolean {
1✔
455
    const map = this._map;
44✔
456
    let changed = false;
44✔
457
    for (const propName of settingNames) {
44✔
458
      if (propName in nextProps && !deepEqual(nextProps[propName], currProps[propName])) {
308!
459
        changed = true;
×
460
        const setter = map[`set${propName[0].toUpperCase()}${propName.slice(1)}`];
×
461
        setter?.call(map, nextProps[propName]);
×
462
      }
×
463
    }
308✔
464
    return changed;
44✔
465
  }
44✔
466

1✔
467
  /* Update map style to match props
1✔
468
     @param {object} nextProps
1✔
469
     @param {object} currProps
1✔
470
     @returns {bool} true if style is changed
1✔
471
   */
1✔
472
  _updateStyle(nextProps: MapboxProps, currProps: MapboxProps): boolean {
1✔
473
    if (nextProps.cursor !== currProps.cursor) {
44!
474
      this._map.getCanvas().style.cursor = nextProps.cursor || '';
×
475
    }
×
476
    if (nextProps.mapStyle !== currProps.mapStyle) {
44✔
477
      const {mapStyle = DEFAULT_STYLE, styleDiffing = true} = nextProps;
2✔
478
      const options: any = {
2✔
479
        diff: styleDiffing
2✔
480
      };
2✔
481
      if ('localIdeographFontFamily' in nextProps) {
2!
482
        // @ts-ignore Mapbox specific prop
×
483
        options.localIdeographFontFamily = nextProps.localIdeographFontFamily;
×
484
      }
×
485
      this._map.setStyle(normalizeStyle(mapStyle), options);
2✔
486
      return true;
2✔
487
    }
2✔
488
    return false;
42✔
489
  }
44✔
490

1✔
491
  /* Update fog, light and terrain to match props
1✔
492
     @param {object} nextProps
1✔
493
     @param {object} currProps
1✔
494
     @returns {bool} true if anything is changed
1✔
495
   */
1✔
496
  _updateStyleComponents(nextProps: MapboxProps, currProps: MapboxProps): boolean {
1✔
497
    const map = this._map;
90✔
498
    let changed = false;
90✔
499
    if (map.isStyleLoaded()) {
90✔
500
      if ('light' in nextProps && map.setLight && !deepEqual(nextProps.light, currProps.light)) {
77!
501
        changed = true;
×
502
        map.setLight(nextProps.light);
×
503
      }
×
504
      if ('fog' in nextProps && map.setFog && !deepEqual(nextProps.fog, currProps.fog)) {
77!
505
        changed = true;
×
506
        map.setFog(nextProps.fog);
×
507
      }
×
508
      if (
77✔
509
        'terrain' in nextProps &&
77!
510
        map.setTerrain &&
×
511
        !deepEqual(nextProps.terrain, currProps.terrain)
×
512
      ) {
77!
513
        if (!nextProps.terrain || map.getSource(nextProps.terrain.source)) {
×
514
          changed = true;
×
515
          map.setTerrain(nextProps.terrain);
×
516
        }
×
517
      }
×
518
    }
77✔
519
    return changed;
90✔
520
  }
90✔
521

1✔
522
  /* Update interaction handlers to match props
1✔
523
     @param {object} nextProps
1✔
524
     @param {object} currProps
1✔
525
     @returns {bool} true if anything is changed
1✔
526
   */
1✔
527
  _updateHandlers(nextProps: MapboxProps, currProps: MapboxProps): boolean {
1✔
528
    const map = this._map;
44✔
529
    let changed = false;
44✔
530
    for (const propName of handlerNames) {
44✔
531
      const newValue = nextProps[propName] ?? true;
352✔
532
      const oldValue = currProps[propName] ?? true;
352✔
533
      if (!deepEqual(newValue, oldValue)) {
352!
534
        changed = true;
×
535
        if (newValue) {
×
536
          map[propName].enable(newValue);
×
537
        } else {
×
538
          map[propName].disable();
×
539
        }
×
540
      }
×
541
    }
352✔
542
    return changed;
44✔
543
  }
44✔
544

1✔
545
  _onEvent = (e: MapEvent) => {
1✔
546
    // @ts-ignore
200✔
547
    const cb = this.props[otherEvents[e.type]];
200✔
548
    if (cb) {
200✔
549
      cb(e);
38✔
550
    } else if (e.type === 'error') {
200✔
551
      console.error((e as ErrorEvent).error); // eslint-disable-line
12✔
552
    }
12✔
553
  };
200✔
554

1✔
555
  private _queryRenderedFeatures(point: Point) {
1✔
556
    const map = this._map;
×
557
    const {interactiveLayerIds = []} = this.props;
×
558
    try {
×
559
      return map.queryRenderedFeatures(point, {
×
560
        layers: interactiveLayerIds.filter(map.getLayer.bind(map))
×
561
      });
×
562
    } catch {
×
563
      // May fail if style is not loaded
×
564
      return [];
×
565
    }
×
566
  }
×
567

1✔
568
  _updateHover(e: MapMouseEvent) {
1✔
569
    const {props} = this;
×
570
    const shouldTrackHoveredFeatures =
×
571
      props.interactiveLayerIds && (props.onMouseMove || props.onMouseEnter || props.onMouseLeave);
×
572

×
573
    if (shouldTrackHoveredFeatures) {
×
574
      const eventType = e.type;
×
575
      const wasHovering = this._hoveredFeatures?.length > 0;
×
576
      const features = this._queryRenderedFeatures(e.point);
×
577
      const isHovering = features.length > 0;
×
578

×
579
      if (!isHovering && wasHovering) {
×
580
        e.type = 'mouseleave';
×
581
        this._onPointerEvent(e);
×
582
      }
×
583
      this._hoveredFeatures = features;
×
584
      if (isHovering && !wasHovering) {
×
585
        e.type = 'mouseenter';
×
586
        this._onPointerEvent(e);
×
587
      }
×
588
      e.type = eventType;
×
589
    } else {
×
590
      this._hoveredFeatures = null;
×
591
    }
×
592
  }
×
593

1✔
594
  _onPointerEvent = (e: MapMouseEvent) => {
1✔
595
    if (e.type === 'mousemove' || e.type === 'mouseout') {
×
596
      this._updateHover(e);
×
597
    }
×
598

×
599
    // @ts-ignore
×
600
    const cb = this.props[pointerEvents[e.type]];
×
601
    if (cb) {
×
602
      if (this.props.interactiveLayerIds && e.type !== 'mouseover' && e.type !== 'mouseout') {
×
603
        e.features = this._hoveredFeatures || this._queryRenderedFeatures(e.point);
×
604
      }
×
605
      cb(e);
×
606
      delete e.features;
×
607
    }
×
608
  };
×
609

1✔
610
  _onCameraEvent = (e: ViewStateChangeEvent) => {
1✔
611
    if (!this._internalUpdate) {
74✔
612
      // @ts-ignore
72✔
613
      const cb = this.props[cameraEvents[e.type]];
72✔
614
      const tr = this._proxyTransform;
72✔
615
      if (cb) {
72✔
616
        e.viewState = transformToViewState(tr.$proposedTransform ?? tr);
18✔
617
        cb(e);
18✔
618
      }
18✔
619
      if (e.type === 'moveend') {
72✔
620
        tr.$proposedTransform = null;
4✔
621
      }
4✔
622
    }
72✔
623
    if (e.type in this._deferredEvents) {
74✔
624
      this._deferredEvents[e.type] = false;
58✔
625
    }
58✔
626
  };
74✔
627

1✔
628
  _fireEvent(baseFire: Function, event: string | MapEvent, properties?: object) {
1✔
629
    const map = this._map;
364✔
630
    const tr = this._proxyTransform;
364✔
631

364✔
632
    // Always expose the controlled transform to controls/end user
364✔
633
    const internal = tr.$internalUpdate;
364✔
634
    try {
364✔
635
      tr.$internalUpdate = false;
364✔
636
      baseFire.call(map, event, properties);
364✔
637
    } finally {
364✔
638
      tr.$internalUpdate = internal;
364✔
639
    }
364✔
640

364✔
641
    return map;
364✔
642
  }
364✔
643

1✔
644
  // If there are camera changes driven by props, invoke camera events so that DOM controls are synced
1✔
645
  _fireDefferedEvents() {
1✔
646
    const map = this._map;
65✔
647
    this._internalUpdate = true;
65✔
648
    for (const eventType in this._deferredEvents) {
65✔
649
      if (this._deferredEvents[eventType]) {
260✔
650
        map.fire(eventType);
2✔
651
      }
2✔
652
    }
260✔
653
    this._internalUpdate = false;
65✔
654
  }
65✔
655
}
1✔
656

1✔
657
/**
1✔
658
 * Access token can be provided via one of:
1✔
659
 *   mapboxAccessToken prop
1✔
660
 *   access_token query parameter
1✔
661
 *   MapboxAccessToken environment variable
1✔
662
 *   REACT_APP_MAPBOX_ACCESS_TOKEN environment variable
1✔
663
 * @returns access token
1✔
664
 */
1✔
665
function getAccessTokenFromEnv(): string {
13✔
666
  let accessToken = null;
13✔
667

13✔
668
  /* global location, process */
13✔
669
  if (typeof location !== 'undefined') {
13✔
670
    const match = /access_token=([^&\/]*)/.exec(location.search);
13✔
671
    accessToken = match && match[1];
13!
672
  }
13✔
673

13✔
674
  // Note: This depends on bundler plugins (e.g. webpack) importing environment correctly
13✔
675
  try {
13✔
676
    // eslint-disable-next-line no-process-env
13✔
677
    accessToken = accessToken || process.env.MapboxAccessToken;
13✔
678
  } catch {
13✔
679
    // ignore
13✔
680
  }
13✔
681

13✔
682
  try {
13✔
683
    // eslint-disable-next-line no-process-env
13✔
684
    accessToken = accessToken || process.env.REACT_APP_MAPBOX_ACCESS_TOKEN;
13✔
685
  } catch {
13✔
686
    // ignore
13✔
687
  }
13✔
688

13✔
689
  return accessToken;
13✔
690
}
13✔
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