mirror of
https://github.com/status-im/react-native-mapbox-gl.git
synced 2026-08-29 12:01:12 +00:00
577 lines
15 KiB
JavaScript
577 lines
15 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();
|
|
}
|
|
|
|
/**
|
|
* [queryRenderedFeaturesAtPoint description]
|
|
* @param {Array<Number>} coordinate - coordinate
|
|
* @param {Array<String>} filter - filter
|
|
* @param {Array<String>} layerIDs - layerIDs
|
|
* @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;
|
|
}
|
|
|
|
/**
|
|
* [queryRenderedFeaturesInRect description]
|
|
* @param {Array<Number>} bbox [description]
|
|
* @param {Array<String>} filters [description]
|
|
* @param {Array<String>} layerIDs [description]
|
|
* @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)
|
|
*
|
|
* @param {Array<Number>} northEastCoordinates - North east coordinate of bound
|
|
* @param {Array<Number>} southWestCoordinates - South west coordinate of bound
|
|
* @param {(Number|Array<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 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;
|