react-map-gl/src/mapbox/mapbox.ts
2022-04-21 11:49:13 -07:00

923 lines
29 KiB
TypeScript

import {transformToViewState, applyViewStateToTransform, cloneTransform} from '../utils/transform';
import {normalizeStyle} from '../utils/style-utils';
import {deepEqual} from '../utils/deep-equal';
import type {
Transform,
ProjectionSpecification,
ViewState,
ViewStateChangeEvent,
DragPanOptions,
ZoomRotateOptions,
TransformRequestFunction,
Light,
Fog,
TerrainSpecification,
MapboxStyle,
ImmutableLike,
LngLatBoundsLike,
FitBoundsOptions,
MapMouseEvent,
MapLayerMouseEvent,
MapLayerTouchEvent,
MapWheelEvent,
MapBoxZoomEvent,
MapStyleDataEvent,
MapSourceDataEvent,
MapboxEvent,
ErrorEvent,
MapboxGeoJSONFeature,
MapboxMap
} from '../types';
export type MapboxProps = Partial<ViewState> & {
// Init options
mapboxAccessToken?: string;
/** Camera options used when constructing the Map instance */
initialViewState?: Partial<ViewState> & {
/** The initial bounds of the map. If bounds is specified, it overrides longitude, latitude and zoom options. */
bounds?: LngLatBoundsLike;
/** A fitBounds options object to use only when setting the bounds option. */
fitBoundsOptions?: FitBoundsOptions;
};
/** If provided, render into an external WebGL context */
gl?: WebGLRenderingContext;
/**
* If true, the gl context will be created with MSA antialiasing, which can be useful for antialiasing custom layers.
* This is false by default as a performance optimization.
* @default false
*/
antialias?: boolean;
/**
* If true, an attribution control will be added to the map.
* @default true
*/
attributionControl?: boolean;
/**
* Snap to north threshold in degrees.
* @default 7
*/
bearingSnap?: number;
/**
* The max number of pixels a user can shift the mouse pointer during a click for it to be
* considered a valid click (as opposed to a mouse drag).
* @default 3
*/
clickTolerance?: number;
/**
* If `true`, Resource Timing API information will be collected for requests made by GeoJSON
* and Vector Tile web workers (this information is normally inaccessible from the main
* Javascript thread). Information will be returned in a `resourceTiming` property of
* relevant `data` events.
* @default false
*/
collectResourceTiming?: boolean;
/**
* If `true` , scroll zoom will require pressing the ctrl or ⌘ key while scrolling to zoom map,
* and touch pan will require using two fingers while panning to move the map.
* Touch pitch will require three fingers to activate if enabled.
*/
cooperativeGestures?: boolean;
/**
* If `true`, symbols from multiple sources can collide with each other during collision
* detection. If `false`, collision detection is run separately for the symbols in each source.
* @default true
*/
crossSourceCollisions?: boolean;
/** String or strings to show in an AttributionControl.
* Only applicable if options.attributionControl is `true`. */
customAttribution?: string | string[];
/**
* Controls the duration of the fade-in/fade-out animation for label collisions, in milliseconds.
* This setting affects all symbol layers. This setting does not affect the duration of runtime
* styling transitions or raster tile cross-fading.
* @default 300
*/
fadeDuration?: number;
/** If true, map creation will fail if the implementation determines that the performance of the created WebGL context would be dramatically lower than expected.
* @default false
*/
failIfMajorPerformanceCaveat?: boolean;
/** If `true`, the map's position (zoom, center latitude, center longitude, bearing, and pitch) will be synced with the hash fragment of the page's URL.
* For example, `http://path/to/my/page.html#2.59/39.26/53.07/-24.1/60`.
* An additional string may optionally be provided to indicate a parameter-styled hash,
* e.g. http://path/to/my/page.html#map=2.59/39.26/53.07/-24.1/60&foo=bar, where foo
* is a custom parameter and bar is an arbitrary hash distinct from the map hash.
*/
hash?: boolean | string;
/** If false, no mouse, touch, or keyboard listeners are attached to the map, so it will not respond to input
* @default true
*/
interactive?: boolean;
/** A patch to apply to the default localization table for UI strings, e.g. control tooltips.
* The `locale` object maps namespaced UI string IDs to translated strings in the target language;
* see `src/ui/default_locale.js` for an example with all supported string IDs.
* The object may specify all UI strings (thereby adding support for a new translation) or
* only a subset of strings (thereby patching the default translation table).
*/
locale?: {[key: string]: string};
/**
* Overrides the generation of all glyphs and font settings except font-weight keywords
* Also overrides localIdeographFontFamily
* @default null
*/
localFontFamily?: string;
/**
* If specified, defines a CSS font-family for locally overriding generation of glyphs in the
* 'CJK Unified Ideographs' and 'Hangul Syllables' ranges. In these ranges, font settings from
* the map's style will be ignored, except for font-weight keywords (light/regular/medium/bold).
* The purpose of this option is to avoid bandwidth-intensive glyph server requests.
* @default "sans-serif"
*/
localIdeographFontFamily?: string;
/**
* A string representing the position of the Mapbox wordmark on the map.
* @default "bottom-left"
*/
logoPosition?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
/**
* The maximum number of tiles stored in the tile cache for a given source. If omitted, the
* cache will be dynamically sized based on the current viewport.
* @default null
*/
maxTileCacheSize?: number;
/**
* If true, map will prioritize rendering for performance by reordering layers
* If false, layers will always be drawn in the specified order
* @default true
*/
optimizeForTerrain?: boolean;
/**
* If `false`, the map's pitch (tilt) control with "drag to rotate" interaction will be disabled.
* @default true
*/
pitchWithRotate?: boolean;
/** If true, The maps canvas can be exported to a PNG using map.getCanvas().toDataURL();. This is false by default as a performance optimization.
* @default false
*/
preserveDrawingBuffer?: boolean;
/**
* If `false`, the map won't attempt to re-request tiles once they expire per their HTTP
* `cacheControl`/`expires` headers.
* @default true
*/
refreshExpiredTiles?: boolean;
/**
* Allows for the usage of the map in automated tests without an accessToken with custom self-hosted test fixtures.
* @default null
*/
testMode?: boolean;
/**
* If true, the map will automatically resize when the browser window resizes
* @default true
*/
trackResize?: boolean;
/**
* A callback run before the Map makes a request for an external URL. The callback can be
* used to modify the url, set headers, or set the credentials property for cross-origin requests.
* @default null
*/
transformRequest?: TransformRequestFunction;
// Handlers
/**
* If true, enable the "box zoom" interaction (see BoxZoomHandler)
* @default true
*/
boxZoom?: boolean;
/**
* If true, enable the "double click to zoom" interaction (see DoubleClickZoomHandler).
* @default true
*/
doubleClickZoom?: boolean;
/**
* If `true`, the "drag to pan" interaction is enabled.
* An `Object` value is passed as options to {@link DragPanHandler#enable}.
* @default true
*/
dragPan?: boolean | DragPanOptions;
/**
* If true, enable the "drag to rotate" interaction (see DragRotateHandler).
* @default true
*/
dragRotate?: boolean;
/**
* If true, enable keyboard shortcuts (see KeyboardHandler).
* @default true
*/
keyboard?: boolean;
/**
* If `true`, the "scroll to zoom" interaction is enabled.
* An `Object` value is passed as options to {@link ScrollZoomHandler#enable}.
* @default true
*/
scrollZoom?: boolean | ZoomRotateOptions;
/**
* If `true`, the "drag to pitch" interaction is enabled.
* An `Object` value is passed as options to {@link TouchPitchHandler#enable}.
* @default true
*/
touchPitch?: boolean;
/**
* If `true`, the "pinch to rotate and zoom" interaction is enabled.
* An `Object` value is passed as options to {@link TouchZoomRotateHandler#enable}.
* @default true
*/
touchZoomRotate?: boolean | ZoomRotateOptions;
// Constraints
/** If set, the map is constrained to the given bounds. */
maxBounds?: LngLatBoundsLike;
/** Maximum pitch of the map. */
maxPitch?: number;
/** Maximum zoom of the map. */
maxZoom?: number;
/** Minimum pitch of the map. */
minPitch?: number;
/** Minimum zoom of the map. */
minZoom?: number;
/** For external controller to override the camera state */
viewState?: ViewState & {
width: number;
height: number;
};
// Styling
/** Mapbox style */
mapStyle?: string | MapboxStyle | ImmutableLike;
/** Enable diffing when the map style changes
* @default true
*/
styleDiffing?: boolean;
/** The fog property of the style. Must conform to the Fog Style Specification .
* If `null` is provided, removes the fog from the map. */
fog?: Fog | null;
/** Light properties of the map. */
light?: Light;
/** Terrain property of the style. Must conform to the Terrain Style Specification .
* If `null` is provided, removes terrain from the map. */
terrain?: TerrainSpecification | null;
/** Default layers to query on pointer events */
interactiveLayerIds?: string[];
/** The projection the map should be rendered in
* @default "mercator"
*/
projection?: ProjectionSpecification | string;
/**
* If `true`, multiple copies of the world will be rendered, when zoomed out.
* @default true
*/
renderWorldCopies?: boolean;
/** CSS cursor */
cursor?: string;
// Callbacks
onMouseDown?: (e: MapLayerMouseEvent) => void;
onMouseUp?: (e: MapLayerMouseEvent) => void;
onMouseOver?: (e: MapLayerMouseEvent) => void;
onMouseMove?: (e: MapLayerMouseEvent) => void;
onClick?: (e: MapLayerMouseEvent) => void;
onDblClick?: (e: MapLayerMouseEvent) => void;
onMouseEnter?: (e: MapLayerMouseEvent) => void;
onMouseLeave?: (e: MapLayerMouseEvent) => void;
onMouseOut?: (e: MapLayerMouseEvent) => void;
onContextMenu?: (e: MapLayerMouseEvent) => void;
onTouchStart?: (e: MapLayerTouchEvent) => void;
onTouchEnd?: (e: MapLayerTouchEvent) => void;
onTouchMove?: (e: MapLayerTouchEvent) => void;
onTouchCancel?: (e: MapLayerTouchEvent) => void;
onMoveStart?: (e: ViewStateChangeEvent) => void;
onMove?: (e: ViewStateChangeEvent) => void;
onMoveEnd?: (e: ViewStateChangeEvent) => void;
onDragStart?: (e: ViewStateChangeEvent) => void;
onDrag?: (e: ViewStateChangeEvent) => void;
onDragEnd?: (e: ViewStateChangeEvent) => void;
onZoomStart?: (e: ViewStateChangeEvent) => void;
onZoom?: (e: ViewStateChangeEvent) => void;
onZoomEnd?: (e: ViewStateChangeEvent) => void;
onRotateStart?: (e: ViewStateChangeEvent) => void;
onRotate?: (e: ViewStateChangeEvent) => void;
onRotateEnd?: (e: ViewStateChangeEvent) => void;
onPitchStart?: (e: ViewStateChangeEvent) => void;
onPitch?: (e: ViewStateChangeEvent) => void;
onPitchEnd?: (e: ViewStateChangeEvent) => void;
onWheel?: (e: MapWheelEvent) => void;
onBoxZoomStart?: (e: MapBoxZoomEvent) => void;
onBoxZoomEnd?: (e: MapBoxZoomEvent) => void;
onBoxZoomCancel?: (e: MapBoxZoomEvent) => void;
onResize?: (e: MapboxEvent) => void;
onLoad?: (e: MapboxEvent) => void;
onRender?: (e: MapboxEvent) => void;
onIdle?: (e: MapboxEvent) => void;
onError?: (e: ErrorEvent) => void;
onRemove?: (e: MapboxEvent) => void;
onData?: (e: MapStyleDataEvent | MapSourceDataEvent) => void;
onStyleData?: (e: MapStyleDataEvent) => void;
onSourceData?: (e: MapSourceDataEvent) => void;
};
const pointerEvents = {
mousedown: 'onMouseDown',
mouseup: 'onMouseUp',
mouseover: 'onMouseOver',
mousemove: 'onMouseMove',
click: 'onClick',
dblclick: 'onDblClick',
mouseenter: 'onMouseEnter',
mouseleave: 'onMouseLeave',
mouseout: 'onMouseOut',
contextmenu: 'onContextMenu',
touchstart: 'onTouchStart',
touchend: 'onTouchEnd',
touchmove: 'onTouchMove',
touchcancel: 'onTouchCancel'
};
const cameraEvents = {
movestart: 'onMoveStart',
move: 'onMove',
moveend: 'onMoveEnd',
dragstart: 'onDragStart',
drag: 'onDrag',
dragend: 'onDragEnd',
zoomstart: 'onZoomStart',
zoom: 'onZoom',
zoomend: 'onZoomEnd',
rotatestart: 'onRotateStart',
rotate: 'onRotate',
rotateend: 'onRotateEnd',
pitchstart: 'onPitchStart',
pitch: 'onPitch',
pitchend: 'onPitchEnd'
};
const otherEvents = {
wheel: 'onWheel',
boxzoomstart: 'onBoxZoomStart',
boxzoomend: 'onBoxZoomEnd',
boxzoomcancel: 'onBoxZoomCancel',
resize: 'onResize',
load: 'onLoad',
render: 'onRender',
idle: 'onIdle',
remove: 'onRemove',
data: 'onData',
styledata: 'onStyleData',
sourcedata: 'onSourceData',
error: 'onError'
};
const settingNames: (keyof MapboxProps)[] = [
'minZoom',
'maxZoom',
'minPitch',
'maxPitch',
'maxBounds',
'projection',
'renderWorldCopies'
];
const handlerNames: (keyof MapboxProps)[] = [
'scrollZoom',
'boxZoom',
'dragRotate',
'dragPan',
'keyboard',
'doubleClickZoom',
'touchZoomRotate',
'touchPitch'
];
/**
* A wrapper for mapbox-gl's Map class
*/
export default class Mapbox {
private _MapClass: typeof MapboxMap;
// mapboxgl.Map instance. Not using type here because we are accessing
// private members and methods
private _map: any = null;
// User-supplied props
props: MapboxProps;
// Mapbox map is stateful.
// During method calls/user interactions, map.transform is mutated and
// deviate from user-supplied props.
// In order to control the map reactively, we shadow the transform
// with the one below, which reflects the view state resolved from
// both user-supplied props and the underlying state
private _renderTransform: Transform;
// Internal states
private _internalUpdate: boolean = false;
private _inRender: boolean = false;
private _hoveredFeatures: MapboxGeoJSONFeature[] = null;
private _deferredEvents: {
move: boolean;
zoom: boolean;
pitch: boolean;
rotate: boolean;
} = {
move: false,
zoom: false,
pitch: false,
rotate: false
};
static savedMaps: Mapbox[] = [];
constructor(MapClass: typeof MapboxMap, props: MapboxProps, container: HTMLDivElement) {
this._MapClass = MapClass;
this.props = props;
this._initialize(container);
}
get map(): MapboxMap {
return this._map as MapboxMap;
}
get transform(): Transform {
return this._renderTransform;
}
setProps(props: MapboxProps) {
const oldProps = this.props;
this.props = props;
const settingsChanged = this._updateSettings(props, oldProps);
if (settingsChanged) {
this._renderTransform = cloneTransform(this._map.transform);
}
const sizeChanged = this._updateSize(props);
const viewStateChanged = this._updateViewState(props, true);
this._updateStyle(props, oldProps);
this._updateStyleComponents(props, oldProps);
this._updateHandlers(props, oldProps);
// If 1) view state has changed to match props and
// 2) the props change is not triggered by map events,
// it's driven by an external state change. Redraw immediately
if (settingsChanged || sizeChanged || (viewStateChanged && !this._map.isMoving())) {
this.redraw();
}
}
static reuse(props: MapboxProps, container: HTMLDivElement) {
const that = Mapbox.savedMaps.pop();
if (!that) {
return null;
}
const map = that.map;
// When reusing the saved map, we need to reparent the map(canvas) and other child nodes
// intoto the new container from the props.
// Step1: reparenting child nodes from old container to new container
const oldContainer = map.getContainer();
container.className = oldContainer.className;
while (oldContainer.childNodes.length > 0) {
container.appendChild(oldContainer.childNodes[0]);
}
// Step2: replace the internal container with new container from the react component
// @ts-ignore
map._container = container;
// Step 3: apply new props
if (props.initialViewState) {
that._updateViewState(props.initialViewState, false);
}
map.resize();
that.setProps({...props, styleDiffing: false});
// Simulate load event
if (map.isStyleLoaded()) {
map.fire('load');
} else {
map.once('styledata', () => map.fire('load'));
}
return that;
}
/* eslint-disable complexity,max-statements */
_initialize(container: HTMLDivElement) {
const {props} = this;
const mapOptions = {
...props,
...props.initialViewState,
accessToken: props.mapboxAccessToken || getAccessTokenFromEnv() || null,
container,
style: normalizeStyle(props.mapStyle)
};
const viewState = mapOptions.initialViewState || mapOptions.viewState || mapOptions;
Object.assign(mapOptions, {
center: [viewState.longitude || 0, viewState.latitude || 0],
zoom: viewState.zoom || 0,
pitch: viewState.pitch || 0,
bearing: viewState.bearing || 0
});
if (props.gl) {
// eslint-disable-next-line
const getContext = HTMLCanvasElement.prototype.getContext;
// Hijack canvas.getContext to return our own WebGLContext
// This will be called inside the mapboxgl.Map constructor
// @ts-expect-error
HTMLCanvasElement.prototype.getContext = () => {
// Unhijack immediately
HTMLCanvasElement.prototype.getContext = getContext;
return props.gl;
};
}
const map: any = new this._MapClass(mapOptions);
// Props that are not part of constructor options
if (viewState.padding) {
map.setPadding(viewState.padding);
}
if (props.cursor) {
map.getCanvas().style.cursor = props.cursor;
}
this._renderTransform = cloneTransform(map.transform);
// Hack
// Insert code into map's render cycle
const renderMap = map._render;
map._render = (arg: number) => {
this._inRender = true;
renderMap.call(map, arg);
this._inRender = false;
};
const runRenderTaskQueue = map._renderTaskQueue.run;
map._renderTaskQueue.run = (arg: number) => {
runRenderTaskQueue.call(map._renderTaskQueue, arg);
this._onBeforeRepaint();
};
map.on('render', () => this._onAfterRepaint());
// Insert code into map's event pipeline
const fireEvent = map.fire;
map.fire = this._fireEvent.bind(this, fireEvent);
// add listeners
map.on('resize', () => {
this._renderTransform.resize(map.transform.width, map.transform.height);
});
map.on('styledata', () => this._updateStyleComponents(this.props, {}));
map.on('sourcedata', () => this._updateStyleComponents(this.props, {}));
for (const eventName in pointerEvents) {
map.on(eventName, this._onPointerEvent);
}
for (const eventName in cameraEvents) {
map.on(eventName, this._onCameraEvent);
}
for (const eventName in otherEvents) {
map.on(eventName, this._onEvent);
}
this._map = map;
}
/* eslint-enable complexity,max-statements */
recycle() {
Mapbox.savedMaps.push(this);
}
destroy() {
this._map.remove();
}
// Force redraw the map now. Typically resize() and jumpTo() is reflected in the next
// render cycle, which is managed by Mapbox's animation loop.
// This removes the synchronization issue caused by requestAnimationFrame.
redraw() {
const map = this._map;
// map._render will throw error if style does not exist
// https://github.com/mapbox/mapbox-gl-js/blob/fb9fc316da14e99ff4368f3e4faa3888fb43c513
// /src/ui/map.js#L1834
if (!this._inRender && map.style) {
// cancel the scheduled update
if (map._frame) {
map._frame.cancel();
map._frame = null;
}
// the order is important - render() may schedule another update
map._render();
}
}
/* Trigger map resize if size is controlled
@param {object} nextProps
@returns {bool} true if size has changed
*/
_updateSize(nextProps: MapboxProps): boolean {
// Check if size is controlled
const {viewState} = nextProps;
if (viewState) {
const map = this._map;
if (viewState.width !== map.transform.width || viewState.height !== map.transform.height) {
map.resize();
return true;
}
}
return false;
}
// Adapted from map.jumpTo
/* Update camera to match props
@param {object} nextProps
@param {bool} triggerEvents - should fire camera events
@returns {bool} true if anything is changed
*/
_updateViewState(nextProps: MapboxProps, triggerEvents: boolean): boolean {
if (this._internalUpdate) {
return false;
}
const map = this._map;
const tr = this._renderTransform;
// Take a snapshot of the transform before mutation
const {zoom, pitch, bearing} = tr;
const changed = applyViewStateToTransform(tr, {
...transformToViewState(map.transform),
...nextProps
});
if (changed && triggerEvents) {
const deferredEvents = this._deferredEvents;
// Delay DOM control updates to the next render cycle
deferredEvents.move = true;
deferredEvents.zoom ||= zoom !== tr.zoom;
deferredEvents.rotate ||= bearing !== tr.bearing;
deferredEvents.pitch ||= pitch !== tr.pitch;
}
// Avoid manipulating the real transform when interaction/animation is ongoing
// as it would interfere with Mapbox's handlers
if (!map.isMoving()) {
applyViewStateToTransform(map.transform, nextProps);
}
return changed;
}
/* Update camera constraints and projection settings to match props
@param {object} nextProps
@param {object} currProps
@returns {bool} true if anything is changed
*/
_updateSettings(nextProps: MapboxProps, currProps: MapboxProps): boolean {
const map = this._map;
let changed = false;
for (const propName of settingNames) {
if (propName in nextProps && !deepEqual(nextProps[propName], currProps[propName])) {
changed = true;
map[`set${propName[0].toUpperCase()}${propName.slice(1)}`](nextProps[propName]);
}
}
return changed;
}
/* Update map style to match props
@param {object} nextProps
@param {object} currProps
@returns {bool} true if style is changed
*/
_updateStyle(nextProps: MapboxProps, currProps: MapboxProps): boolean {
if (nextProps.cursor !== currProps.cursor) {
this._map.getCanvas().style.cursor = nextProps.cursor;
}
if (nextProps.mapStyle !== currProps.mapStyle) {
const options: any = {
diff: nextProps.styleDiffing
};
if ('localIdeographFontFamily' in nextProps) {
options.localIdeographFontFamily = nextProps.localIdeographFontFamily;
}
this._map.setStyle(normalizeStyle(nextProps.mapStyle), options);
return true;
}
return false;
}
/* Update fog, light and terrain to match props
@param {object} nextProps
@param {object} currProps
@returns {bool} true if anything is changed
*/
_updateStyleComponents(nextProps: MapboxProps, currProps: MapboxProps): boolean {
const map = this._map;
let changed = false;
if (map.style.loaded()) {
if ('light' in nextProps && !deepEqual(nextProps.light, currProps.light)) {
changed = true;
map.setLight(nextProps.light);
}
if ('fog' in nextProps && !deepEqual(nextProps.fog, currProps.fog)) {
changed = true;
map.setFog(nextProps.fog);
}
if ('terrain' in nextProps && !deepEqual(nextProps.terrain, currProps.terrain)) {
if (!nextProps.terrain || map.getSource(nextProps.terrain.source)) {
changed = true;
map.setTerrain(nextProps.terrain);
}
}
// Copy changes to the transform
this._renderTransform.elevation = map.transform.elevation;
}
return changed;
}
/* Update interaction handlers to match props
@param {object} nextProps
@param {object} currProps
@returns {bool} true if anything is changed
*/
_updateHandlers(nextProps: MapboxProps, currProps: MapboxProps): boolean {
const map = this._map;
let changed = false;
for (const propName of handlerNames) {
const newValue = nextProps[propName];
if (!deepEqual(newValue, currProps[propName])) {
changed = true;
if (newValue) {
map[propName].enable(newValue);
} else {
map[propName].disable();
}
}
}
return changed;
}
_onEvent = (e: MapboxEvent) => {
// @ts-ignore
const cb = this.props[otherEvents[e.type]];
if (cb) {
cb(e);
}
};
_updateHover(e: MapMouseEvent) {
const {props} = this;
const shouldTrackHoveredFeatures =
props.interactiveLayerIds && (props.onMouseMove || props.onMouseEnter || props.onMouseLeave);
if (shouldTrackHoveredFeatures) {
const eventType = e.type;
const wasHovering = this._hoveredFeatures?.length > 0;
let features;
if (eventType === 'mousemove') {
try {
features = this._map.queryRenderedFeatures(e.point, {
layers: props.interactiveLayerIds
});
} catch {
features = [];
}
} else {
features = [];
}
const isHovering = features.length > 0;
if (!isHovering && wasHovering) {
e.type = 'mouseleave';
this._onPointerEvent(e);
}
this._hoveredFeatures = features;
if (isHovering && !wasHovering) {
e.type = 'mouseenter';
this._onPointerEvent(e);
}
e.type = eventType;
} else {
this._hoveredFeatures = null;
}
}
_onPointerEvent = (e: MapLayerMouseEvent | MapLayerTouchEvent) => {
if (e.type === 'mousemove' || e.type === 'mouseout') {
this._updateHover(e);
}
// @ts-ignore
const cb = this.props[pointerEvents[e.type]];
if (cb) {
if (this.props.interactiveLayerIds && e.type !== 'mouseover' && e.type !== 'mouseout') {
const features =
this._hoveredFeatures ||
this._map.queryRenderedFeatures(e.point, {
layers: this.props.interactiveLayerIds
});
if (!features.length) {
return;
}
e.features = features;
}
cb(e);
delete e.features;
}
};
_onCameraEvent = (e: ViewStateChangeEvent) => {
if (!this._internalUpdate) {
// @ts-ignore
const cb = this.props[cameraEvents[e.type]];
if (cb) {
cb(e);
}
}
if (e.type in this._deferredEvents) {
this._deferredEvents[e.type] = false;
}
};
_fireEvent(baseFire: Function, event: string | MapboxEvent, properties?: object) {
const map = this._map;
const tr = map.transform;
const eventType = typeof event === 'string' ? event : event.type;
if (eventType === 'move') {
this._updateViewState(this.props, false);
}
if (eventType in cameraEvents) {
if (typeof event === 'object') {
(event as ViewStateChangeEvent).viewState = transformToViewState(tr);
}
if (this._map.isMoving()) {
// Replace map.transform with ours during the callbacks
map.transform = this._renderTransform;
baseFire.call(map, event, properties);
map.transform = tr;
return map;
}
}
baseFire.call(map, event, properties);
return map;
}
// All camera manipulations are complete, ready to repaint
_onBeforeRepaint() {
const map = this._map;
// If there are camera changes driven by props, invoke camera events so that DOM controls are synced
this._internalUpdate = true;
for (const eventType in this._deferredEvents) {
if (this._deferredEvents[eventType]) {
map.fire(eventType);
}
}
this._internalUpdate = false;
const tr = this._map.transform;
// Make sure camera matches the current props
this._map.transform = this._renderTransform;
this._map.painter.transform = this._renderTransform;
this._onAfterRepaint = () => {
// Restores camera state before render/load events are fired
this._map.transform = tr;
};
}
_onAfterRepaint: () => void;
}
/**
* Access token can be provided via one of:
* mapboxAccessToken prop
* access_token query parameter
* MapboxAccessToken environment variable
* REACT_APP_MAPBOX_ACCESS_TOKEN environment variable
* @returns access token
*/
function getAccessTokenFromEnv(): string {
let accessToken = null;
/* global location, process */
if (typeof location !== 'undefined') {
const match = /access_token=([^&\/]*)/.exec(location.search);
accessToken = match && match[1];
}
// Note: This depends on bundler plugins (e.g. webpack) importing environment correctly
try {
accessToken = accessToken || process.env.MapboxAccessToken;
} catch {
// ignore
}
try {
accessToken = accessToken || process.env.REACT_APP_MAPBOX_ACCESS_TOKEN;
} catch {
// ignore
}
return accessToken;
}