UNPKG

@mapbox/react-native-mapbox-gl

Version:

A Mapbox GL react native module for creating custom maps

643 lines (552 loc) 17.4 kB
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, IS_ANDROID, viewPropTypes, } from '../utils'; import { getFilter } from '../utils/filterUtils'; const MapboxGL = NativeModules.MGLModule; export const NATIVE_MODULE_NAME = 'RCTMGLMapView'; /** * MapView backed by Mapbox Native GL */ class MapView extends React.Component { static propTypes = { ...viewPropTypes, /** * 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, /** * The distance from the edges of the map view’s frame to the edges of the map view’s logical viewport. */ contentInset: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.number), 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 zoom on the map */ zoomEnabled: PropTypes.bool, /** * 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, /** * Enable/Disable the compass from appearing on the map */ compassEnabled: 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(); } /** * The coordinate bounds(ne, sw) visible in the users’s viewport. * * @example * const visibleBounds = await this._map.getVisibleBounds(); * * @return {Array} */ async getVisibleBounds () { const res = await this._runNativeCommand('getVisibleBounds'); return res.visibleBounds; } /** * 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))); } _getContentInset () { if (!this.props.contentInset) { return; } if (!Array.isArray(this.props.contentInset)) { return [this.props.contentInset]; } return this.props.contentInset; } render () { let props = { ...this.props, centerCoordinate: this._getCenterCoordinate(), contentInset: this._getContentInset(), }; 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> ); } } const RCTMGLMapView = requireNativeComponent(NATIVE_MODULE_NAME, MapView, { nativeOnly: { onMapChange: true, onAndroidCallback: true }, }); export default MapView;