mirror of
https://github.com/status-im/react-native-mapbox-gl.git
synced 2026-08-29 12:01:12 +00:00
609 lines
16 KiB
JavaScript
609 lines
16 KiB
JavaScript
import React from 'react';
|
||
import PropTypes from 'prop-types';
|
||
import { NativeModules, requireNativeComponent } from 'react-native';
|
||
import { makePoint, makeLatLngBounds } from '../utils/geoUtils';
|
||
|
||
import {
|
||
isFunction,
|
||
isNumber,
|
||
runNativeCommand,
|
||
toJSONString,
|
||
getFilter,
|
||
IS_ANDROID,
|
||
} from '../utils';
|
||
|
||
const MapboxGL = NativeModules.MGLModule;
|
||
|
||
export const NATIVE_MODULE_NAME = 'RCTMGLMapView';
|
||
|
||
const RCTMGLMapView = requireNativeComponent(NATIVE_MODULE_NAME, MapView, {
|
||
nativeOnly: { onMapChange: true, onAndroidCallback: true },
|
||
});
|
||
|
||
/**
|
||
* MapView backed by Mapbox Native GL
|
||
*/
|
||
class MapView extends React.Component {
|
||
static propTypes = {
|
||
/**
|
||
* Animates changes between pitch and bearing
|
||
*/
|
||
animated: PropTypes.bool,
|
||
|
||
/**
|
||
* Initial center coordinate on map [lng, lat]
|
||
*/
|
||
centerCoordinate: PropTypes.arrayOf(PropTypes.number),
|
||
|
||
/**
|
||
* Shows the users location on the map
|
||
*/
|
||
showUserLocation: PropTypes.bool,
|
||
|
||
/**
|
||
* The mode used to track the user location on the map
|
||
*/
|
||
userTrackingMode: PropTypes.number,
|
||
|
||
/**
|
||
* Initial heading on map
|
||
*/
|
||
heading: PropTypes.number,
|
||
|
||
/**
|
||
* Initial pitch on map
|
||
*/
|
||
pitch: PropTypes.number,
|
||
|
||
/**
|
||
* Style for wrapping React Native View
|
||
*/
|
||
style: PropTypes.any,
|
||
|
||
/**
|
||
* Style URL for map
|
||
*/
|
||
styleURL: PropTypes.string,
|
||
|
||
/**
|
||
* Initial zoom level of map
|
||
*/
|
||
zoomLevel: PropTypes.number,
|
||
|
||
/**
|
||
* Min zoom level of map
|
||
*/
|
||
minZoomLevel: PropTypes.number,
|
||
|
||
/**
|
||
* Max zoom level of map
|
||
*/
|
||
maxZoomLevel: PropTypes.number,
|
||
|
||
/**
|
||
* Enable/Disable scroll on the map
|
||
*/
|
||
scrollEnabled: PropTypes.bool,
|
||
|
||
/**
|
||
* Enable/Disable pitch on map
|
||
*/
|
||
pitchEnabled: PropTypes.bool,
|
||
|
||
/**
|
||
* Enable/Disable rotation on map
|
||
*/
|
||
rotateEnabled: PropTypes.bool,
|
||
|
||
/**
|
||
* Enable/Disable attribution on map. For iOS you need to add MGLMapboxMetricsEnabledSettingShownInApp=YES
|
||
* to your Info.plist
|
||
*/
|
||
attributionEnabled: PropTypes.bool,
|
||
|
||
/**
|
||
* Enable/Disable the logo on the map.
|
||
*/
|
||
logoEnabled: PropTypes.bool,
|
||
|
||
/**
|
||
* Map press listener, gets called when a user presses the map
|
||
*/
|
||
onPress: PropTypes.func,
|
||
|
||
/**
|
||
* Map long press listener, gets called when a user long presses the map
|
||
*/
|
||
onLongPress: PropTypes.func,
|
||
|
||
/**
|
||
* This event is triggered whenever the currently displayed map region is about to change.
|
||
*/
|
||
onRegionWillChange: PropTypes.func,
|
||
|
||
/**
|
||
* This event is triggered whenever the currently displayed map region is changing.
|
||
*/
|
||
onRegionIsChanging: PropTypes.func,
|
||
|
||
/**
|
||
* This event is triggered whenever the currently displayed map region finished changing
|
||
*/
|
||
onRegionDidChange: PropTypes.func,
|
||
|
||
/**
|
||
* This event is triggered when the map is about to start loading a new map style.
|
||
*/
|
||
onWillStartLoadingMap: PropTypes.func,
|
||
|
||
/**
|
||
* This is triggered when the map has successfully loaded a new map style.
|
||
*/
|
||
onDidFinishLoadingMap: PropTypes.func,
|
||
|
||
/**
|
||
* This event is triggered when the map has failed to load a new map style.
|
||
*/
|
||
onDidFailLoadingMap: PropTypes.func,
|
||
|
||
/**
|
||
* This event is triggered when the map will start rendering a frame.
|
||
*/
|
||
onWillStartRenderingFrame: PropTypes.func,
|
||
|
||
/**
|
||
* This event is triggered when the map finished rendering a frame.
|
||
*/
|
||
onDidFinishRenderingFrame: PropTypes.func,
|
||
|
||
/**
|
||
* This event is triggered when the map fully finished rendering a frame.
|
||
*/
|
||
onDidFinishRenderingFrameFully: PropTypes.func,
|
||
|
||
/**
|
||
* This event is triggered when the map will start rendering the map.
|
||
*/
|
||
onWillStartRenderingMap: PropTypes.func,
|
||
|
||
/**
|
||
* This event is triggered when the map finished rendering the map.
|
||
*/
|
||
onDidFinishRenderingMap: PropTypes.func,
|
||
|
||
/**
|
||
* This event is triggered when the map fully finished rendering the map.
|
||
*/
|
||
onDidFinishRenderingMapFully: PropTypes.func,
|
||
|
||
/**
|
||
* This event is triggered when a style has finished loading.
|
||
*/
|
||
onDidFinishLoadingStyle: PropTypes.func,
|
||
|
||
/**
|
||
* This event is triggered when a fly to animation is cancelled or completed after calling flyTo
|
||
*/
|
||
onFlyToComplete: PropTypes.func,
|
||
|
||
/**
|
||
* This event is triggered once the camera is finished after calling setCamera
|
||
*/
|
||
onSetCameraComplete: PropTypes.func,
|
||
};
|
||
|
||
static defaultProps = {
|
||
animated: false,
|
||
heading: 0,
|
||
pitch: 0,
|
||
scrollEnabled: true,
|
||
pitchEnabled: true,
|
||
rotateEnabled: true,
|
||
attributionEnabled: true,
|
||
logoEnabled: true,
|
||
zoomLevel: 16,
|
||
userTrackingMode: MapboxGL.UserTrackingModes.None,
|
||
styleURL: MapboxGL.StyleURL.Street,
|
||
};
|
||
|
||
constructor (props) {
|
||
super(props);
|
||
|
||
this._onPress = this._onPress.bind(this);
|
||
this._onLongPress = this._onLongPress.bind(this);
|
||
this._onChange = this._onChange.bind(this);
|
||
this._onAndroidCallback = this._onAndroidCallback.bind(this);
|
||
|
||
this._callbackMap = new Map();
|
||
}
|
||
|
||
/**
|
||
* Returns an array of rendered map features that intersect with a given point.
|
||
*
|
||
* @example
|
||
* this._map.queryRenderedFeaturesAtPoint([30, 40], ['==', 'type', 'Point'], ['id1', 'id2'])
|
||
*
|
||
* @param {Array<Number>} coordinate - A point expressed in the map view’s coordinate system.
|
||
* @param {Array=} filter - A set of strings that correspond to the names of layers defined in the current style. Only the features contained in these layers are included in the returned array.
|
||
* @param {Array=} layerIDs - A array of layer id's to filter the features by
|
||
* @return {FeatureCollection}
|
||
*/
|
||
async queryRenderedFeaturesAtPoint (coordinate, filter = [], layerIDs = []) {
|
||
if (!coordinate || coordinate.length < 2) {
|
||
throw new Error('Must pass in valid coordinate[lng, lat]');
|
||
}
|
||
|
||
const res = await this._runNativeCommand('queryRenderedFeaturesAtPoint', [
|
||
coordinate,
|
||
getFilter(filter),
|
||
layerIDs,
|
||
]);
|
||
|
||
if (IS_ANDROID) {
|
||
return JSON.parse(res.data);
|
||
}
|
||
|
||
return res.data;
|
||
}
|
||
|
||
/**
|
||
* Returns an array of rendered map features that intersect with the given rectangle,
|
||
* restricted to the given style layers and filtered by the given predicate.
|
||
*
|
||
* @example
|
||
* this._map.queryRenderedFeaturesInRect([30, 40, 20, 10], ['==', 'type', 'Point'], ['id1', 'id2'])
|
||
*
|
||
* @param {Array<Number>} bbox - A rectangle expressed in the map view’s coordinate system.
|
||
* @param {Array=} filter - A set of strings that correspond to the names of layers defined in the current style. Only the features contained in these layers are included in the returned array.
|
||
* @param {Array=} layerIDs - A array of layer id's to filter the features by
|
||
* @return {FeatureCollection}
|
||
*/
|
||
async queryRenderedFeaturesInRect (bbox, filter = [], layerIDs = []) {
|
||
if (!bbox || bbox.length !== 4) {
|
||
throw new Error('Must pass in a valid bounding box[top, right, bottom, left]');
|
||
}
|
||
const res = await this._runNativeCommand('queryRenderedFeaturesInRect', [
|
||
bbox,
|
||
getFilter(filter),
|
||
layerIDs,
|
||
]);
|
||
|
||
if (IS_ANDROID) {
|
||
return JSON.parse(res.data);
|
||
}
|
||
|
||
return res.data;
|
||
}
|
||
|
||
/**
|
||
* Map camera transitions to fit provided bounds
|
||
*
|
||
* @example
|
||
* this.map.fitBounds([lng, lat], [lng, lat])
|
||
* this.map.fitBounds([lng, lat], [lng, lat], 20, 1000) // padding for all sides
|
||
* this.map.fitBounds([lng, lat], [lng, lat], [verticalPadding, horizontalPadding], 1000)
|
||
* this.map.fitBounds([lng, lat], [lng, lat], [top, right, bottom, left], 1000)
|
||
*
|
||
* @param {Array<Number>} northEastCoordinates - North east coordinate of bound
|
||
* @param {Array<Number>} southWestCoordinates - South west coordinate of bound
|
||
* @param {Number=} padding - Camera padding for bound
|
||
* @param {Number=} duration - Duration of camera animation
|
||
* @return {void}
|
||
*/
|
||
fitBounds (northEastCoordinates, southWestCoordinates, padding = 0, duration = 2000) {
|
||
if (!this._nativeRef) {
|
||
return;
|
||
}
|
||
|
||
let pad = {
|
||
paddingLeft: 0,
|
||
paddingRight: 0,
|
||
paddingTop: 0,
|
||
paddingBottom: 0,
|
||
};
|
||
|
||
if (Array.isArray(padding)) {
|
||
if (padding.length === 2) {
|
||
pad.paddingTop = padding[0];
|
||
pad.paddingBottom = padding[0];
|
||
pad.paddingLeft = padding[1];
|
||
pad.paddingRight = padding[1];
|
||
} else if (padding.length === 4) {
|
||
pad.paddingTop = padding[0];
|
||
pad.paddingRight = padding[1];
|
||
pad.paddingBottom = padding[2];
|
||
pad.paddingLeft = padding[3];
|
||
}
|
||
} else {
|
||
pad.paddingLeft = padding;
|
||
pad.paddingRight = padding;
|
||
pad.paddingTop = padding;
|
||
pad.paddingBottom = padding;
|
||
}
|
||
|
||
return this.setCamera({
|
||
bounds: {
|
||
ne: northEastCoordinates,
|
||
sw: southWestCoordinates,
|
||
...pad,
|
||
},
|
||
duration: duration,
|
||
mode: MapboxGL.CameraModes.Flight,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Map camera will fly to new coordinate
|
||
*
|
||
* @example
|
||
* this.map.flyTo([lng, lat])
|
||
* this.map.flyTo([lng, lat], 12000)
|
||
*
|
||
* @param {Array<Number>} coordinates - Coordinates that map camera will jump too
|
||
* @param {Number=} duration - Duration of camera animation
|
||
* @return {void}
|
||
*/
|
||
flyTo (coordinates, duration = 2000) {
|
||
if (!this._nativeRef) {
|
||
return Promise.reject('No native reference found');
|
||
}
|
||
return this.setCamera({
|
||
centerCoordinate: coordinates,
|
||
duration: duration,
|
||
mode: MapboxGL.CameraModes.Flight,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Map camera will move to new coordinate at the same zoom level
|
||
*
|
||
* @example
|
||
* this.map.moveTo([lng, lat], 200) // eases camera to new location based on duration
|
||
* this.map.moveTo([lng, lat]) // snaps camera to new location without any easing
|
||
*
|
||
* @param {Array<Number>} coordinates - Coordinates that map camera will move too
|
||
* @param {Number=} duration - Duration of camera animation
|
||
* @return {void}
|
||
*/
|
||
moveTo (coordinates, duration = 0) {
|
||
if (!this._nativeRef) {
|
||
return Promise.reject('No native reference found');
|
||
}
|
||
return this.setCamera({
|
||
centerCoordinate: coordinates,
|
||
duration: duration,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Map camera will zoom to specified level
|
||
*
|
||
* @example
|
||
* this.map.zoomTo(16)
|
||
* this.map.zoomTo(16, 100)
|
||
*
|
||
* @param {Number} zoomLevel - Zoom level that the map camera will animate too
|
||
* @param {Number=} duration - Duration of camera animation
|
||
* @return {void}
|
||
*/
|
||
zoomTo (zoomLevel, duration = 2000) {
|
||
if (!this._nativeRef) {
|
||
return Promise.reject('No native reference found');
|
||
}
|
||
return this.setCamera({
|
||
zoom: zoomLevel,
|
||
duration: duration,
|
||
mode: MapboxGL.CameraModes.Flight,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Map camera will perform updates based on provided config. Advanced use only!
|
||
*
|
||
* @example
|
||
* this.map.setCamera({
|
||
* centerCoordinate: [lng, lat],
|
||
* zoomLevel: 16,
|
||
* duration: 2000,
|
||
* })
|
||
*
|
||
* this.map.setCamera({
|
||
* stops: [
|
||
* { pitch: 45, duration: 200 },
|
||
* { heading: 180, duration: 300 },
|
||
* ]
|
||
* })
|
||
*
|
||
* @param {Object} config - Camera configuration
|
||
*/
|
||
setCamera (config = {}) {
|
||
if (!this._nativeRef) {
|
||
return;
|
||
}
|
||
|
||
let cameraConfig = {};
|
||
|
||
if (config.stops) {
|
||
cameraConfig.stops = [];
|
||
|
||
for (let stop of config.stops) {
|
||
cameraConfig.stops.push(this._createStopConfig(stop));
|
||
}
|
||
} else {
|
||
cameraConfig = this._createStopConfig(config);
|
||
}
|
||
|
||
return this._runNativeCommand('setCamera', [cameraConfig]);
|
||
}
|
||
|
||
_runNativeCommand (methodName, args) {
|
||
if (IS_ANDROID) {
|
||
return new Promise ((resolve) => {
|
||
const callbackID = '' + Date.now();
|
||
this._addAddAndroidCallback(callbackID, resolve);
|
||
args.unshift(callbackID);
|
||
runNativeCommand(NATIVE_MODULE_NAME, methodName, this._nativeRef, args);
|
||
});
|
||
}
|
||
return runNativeCommand(NATIVE_MODULE_NAME, methodName, this._nativeRef, args);
|
||
}
|
||
|
||
_createStopConfig (config = {}) {
|
||
let stopConfig = {
|
||
mode: isNumber(config.mode) ? config.mode : MapboxGL.CameraModes.Ease,
|
||
pitch: config.pitch,
|
||
heading: config.heading,
|
||
duration: config.duration || 2000,
|
||
zoom: config.zoom,
|
||
};
|
||
|
||
if (config.centerCoordinate) {
|
||
stopConfig.centerCoordinate = toJSONString(makePoint(config.centerCoordinate));
|
||
}
|
||
|
||
if (config.bounds && config.bounds.ne && config.bounds.sw) {
|
||
const { ne, sw, paddingLeft, paddingRight, paddingTop, paddingBottom } = config.bounds;
|
||
stopConfig.bounds = toJSONString(makeLatLngBounds(ne, sw));
|
||
stopConfig.boundsPaddingTop = paddingTop || 0;
|
||
stopConfig.boundsPaddingRight = paddingRight || 0;
|
||
stopConfig.boundsPaddingBottom = paddingBottom || 0;
|
||
stopConfig.boundsPaddingLeft = paddingLeft || 0;
|
||
}
|
||
|
||
return stopConfig;
|
||
}
|
||
|
||
_addAddAndroidCallback (id, callback) {
|
||
this._callbackMap.set(id, callback);
|
||
}
|
||
|
||
_removeAndroidCallback (id) {
|
||
this._callbackMap.remove(id);
|
||
}
|
||
|
||
_onAndroidCallback (e) {
|
||
const callbackID = e.nativeEvent.type;
|
||
const callback = this._callbackMap.get(callbackID);
|
||
|
||
if (!callback) {
|
||
return;
|
||
}
|
||
|
||
this._callbackMap.delete(callbackID);
|
||
callback.call(null, e.nativeEvent.payload);
|
||
}
|
||
|
||
_onPress (e) {
|
||
if (isFunction(this.props.onPress)) {
|
||
this.props.onPress(e.nativeEvent.payload);
|
||
}
|
||
}
|
||
|
||
_onLongPress (e) {
|
||
if (isFunction(this.props.onLongPress)) {
|
||
this.props.onLongPress(e.nativeEvent.payload);
|
||
}
|
||
}
|
||
|
||
_onChange (e) {
|
||
const { type, payload } = e.nativeEvent;
|
||
let propName = '';
|
||
|
||
switch (type) {
|
||
case MapboxGL.EventTypes.RegionWillChange:
|
||
propName = 'onRegionWillChange';
|
||
break;
|
||
case MapboxGL.EventTypes.RegionIsChanging:
|
||
propName = 'onRegionIsChanging';
|
||
break;
|
||
case MapboxGL.EventTypes.RegionDidChange:
|
||
propName = 'onRegionDidChange';
|
||
break;
|
||
case MapboxGL.EventTypes.WillStartLoadinMap:
|
||
propName = 'onWillStartLoadingMap';
|
||
break;
|
||
case MapboxGL.EventTypes.DidFinishLoadingMap:
|
||
propName = 'onDidFinishLoadingMap';
|
||
break;
|
||
case MapboxGL.EventTypes.DidFailLoadingMap:
|
||
propName = 'onDidFailLoadingMap';
|
||
break;
|
||
case MapboxGL.EventTypes.WillStartRenderingFrame:
|
||
propName = 'onWillStartRenderingFrame';
|
||
break;
|
||
case MapboxGL.EventTypes.DidFinishRenderingFrame:
|
||
propName = 'onDidFinishRenderingFrame';
|
||
break;
|
||
case MapboxGL.EventTypes.DidFinishRenderingFrameFully:
|
||
propName = 'onDidFinishRenderingFrameFully';
|
||
break;
|
||
case MapboxGL.EventTypes.WillStartRenderingMap:
|
||
propName = 'onWillStartRenderingMap';
|
||
break;
|
||
case MapboxGL.EventTypes.DidFinishRenderingMap:
|
||
propName = 'onDidFinishRenderingMap';
|
||
break;
|
||
case MapboxGL.EventTypes.DidFinishRenderingMapFully:
|
||
propName = 'onDidFinishRenderingMapFully';
|
||
break;
|
||
case MapboxGL.EventTypes.DidFinishLoadingStyle:
|
||
propName = 'onDidFinishLoadingStyle';
|
||
break;
|
||
}
|
||
|
||
if (propName.length) {
|
||
this._handleOnChange(propName, payload);
|
||
}
|
||
}
|
||
|
||
_handleOnChange (propName, payload) {
|
||
if (isFunction(this.props[propName])) {
|
||
this.props[propName](payload);
|
||
}
|
||
}
|
||
|
||
_getCenterCoordinate () {
|
||
if (!this.props.centerCoordinate) {
|
||
return;
|
||
}
|
||
return toJSONString((makePoint(this.props.centerCoordinate)));
|
||
}
|
||
|
||
render () {
|
||
let props = {
|
||
animated: this.props.animated,
|
||
centerCoordinate: this._getCenterCoordinate(),
|
||
showUserLocation: this.props.showUserLocation,
|
||
userTrackingMode: this.props.userTrackingMode,
|
||
heading: this.props.heading,
|
||
pitch: this.props.pitch,
|
||
style: this.props.style,
|
||
styleURL: this.props.styleURL,
|
||
zoomLevel: this.props.zoomLevel,
|
||
minZoomLevel: this.props.minZoomLevel,
|
||
maxZoomLevel: this.props.maxZoomLevel,
|
||
scrollEnabled: this.props.scrollEnabled,
|
||
pitchEnabled: this.props.pitchEnabled,
|
||
rotateEnabled: this.props.rotateEnabled,
|
||
attributionEnabled: this.props.attributionEnabled,
|
||
logoEnabled: this.props.logoEnabled,
|
||
};
|
||
|
||
const callbacks = {
|
||
ref: (nativeRef) => this._nativeRef = nativeRef,
|
||
onPress: this._onPress,
|
||
onLongPress: this._onLongPress,
|
||
onMapChange: this._onChange,
|
||
onAndroidCallback: IS_ANDROID ? this._onAndroidCallback : undefined,
|
||
};
|
||
|
||
return (
|
||
<RCTMGLMapView {...props} {...callbacks}>
|
||
{this.props.children}
|
||
</RCTMGLMapView>
|
||
);
|
||
}
|
||
}
|
||
|
||
export default MapView;
|