Created
August 13, 2021 10:48
-
-
Save nesjett/99388f73f1b4d82735bde89b90e24202 to your computer and use it in GitHub Desktop.
React native mapbox custom UserLocation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* | |
* Detailed explanation (how to use) at https://nsabater.com/react-native-mapboxgl-custom-userlocation | |
* Code writteng by Nestor Sabater extending React Native mapboxgl base code. | |
* | |
* NOTE: The file should be split away in different smaller files, but the original source code for the | |
* component UserLocation.js is written in a single file, so I preserved that pattern for this example. | |
* | |
***/ | |
import React from 'react'; | |
import PropTypes from 'prop-types'; | |
import locationManager from '@react-native-mapbox-gl/maps/javascript/modules/location/locationManager'; | |
import Annotation from '@react-native-mapbox-gl/maps/javascript/components/annotations/Annotation'; // eslint-disable-line import/no-cycle | |
import MapboxGL from '@react-native-mapbox-gl/maps'; | |
import CircleLayer from '@react-native-mapbox-gl/maps/javascript/components/CircleLayer'; | |
import HeadingIndicator from '@react-native-mapbox-gl/maps/javascript/components/HeadingIndicator'; | |
import NativeUserLocation from '@react-native-mapbox-gl/maps/javascript/components/NativeUserLocation'; | |
import circle from '@turf/circle'; | |
import { interactionRadius } from '../../../constants'; | |
const mapboxBlue = 'rgba(255, 181, 229, 100)'; | |
const layerStyles = { | |
normal: { | |
pluse: { | |
circleRadius: 15, | |
circleColor: mapboxBlue, | |
circleOpacity: 0.2, | |
circlePitchAlignment: 'map', | |
}, | |
background: { | |
circleRadius: 9, | |
circleColor: '#fff', | |
circlePitchAlignment: 'map', | |
}, | |
foreground: { | |
circleRadius: 6, | |
circleColor: mapboxBlue, | |
circlePitchAlignment: 'map', | |
}, | |
}, | |
}; | |
const boundsStyle = { | |
fillAntialias: true, | |
fillColor: 'rgba(255, 181, 229, 0.3)', | |
fillOutlineColor: 'rgba(255, 181, 229, 0.7)', | |
}; | |
export const normalIcon = (showsUserHeadingIndicator, heading) => [ | |
<CircleLayer | |
key="mapboxUserLocationPluseCircle" | |
id="mapboxUserLocationPluseCircle" | |
style={layerStyles.normal.pluse} | |
/>, | |
<CircleLayer | |
key="mapboxUserLocationPluseCircle2" | |
id="mapboxUserLocationPluseCircle2" | |
// eslint-disable-next-line react-native/no-inline-styles | |
style={{ ...layerStyles.normal.background, circleRadius: 30 }} | |
/>, | |
<CircleLayer | |
key="mapboxUserLocationWhiteCircle" | |
id="mapboxUserLocationWhiteCircle" | |
style={layerStyles.normal.background} | |
/>, | |
<CircleLayer | |
key="mapboxUserLocationBlueCicle" | |
id="mapboxUserLocationBlueCicle" | |
aboveLayerID="mapboxUserLocationWhiteCircle" | |
style={layerStyles.normal.foreground} | |
/>, | |
...(showsUserHeadingIndicator && heading !== null | |
? [HeadingIndicator(heading)] | |
: []), | |
]; | |
type props = { | |
minDisplacement: number, | |
renderMode: any, | |
onUpdate: any, | |
visible: boolean, | |
androidRenderMode: any, | |
showsUserHeadingIndicator: boolean, | |
onPress: any, | |
animated: boolean | |
} | |
type state = { | |
interactionArea: any | |
coordinates: number[][], | |
heading: any, | |
lastCoordinates: number[][], | |
shouldShowUserLocation: boolean | |
} | |
class UserLocationCustom extends React.Component<props, state> { | |
static propTypes = { | |
/** | |
* Whether location icon is animated between updates | |
*/ | |
animated: PropTypes.bool, | |
/** | |
* Which render mode to use. | |
* Can either be `normal` or `native` | |
*/ | |
renderMode: PropTypes.oneOf(['normal', 'native']), | |
/** | |
* native/android only render mode | |
* | |
* - normal: just a circle | |
* - compass: triangle with heading | |
* - gps: large arrow | |
* | |
* @platform android | |
*/ | |
androidRenderMode: PropTypes.oneOf(['normal', 'compass', 'gps']), | |
/** | |
* Whether location icon is visible | |
*/ | |
visible: PropTypes.bool, | |
/** | |
* Callback that is triggered on location icon press | |
*/ | |
onPress: PropTypes.func, | |
/** | |
* Callback that is triggered on location update | |
*/ | |
onUpdate: PropTypes.func, | |
/** | |
* Show or hide small arrow which indicates direction the device is pointing relative to north. | |
*/ | |
showsUserHeadingIndicator: PropTypes.bool, | |
/** | |
* Minimum amount of movement before GPS location is updated in meters | |
*/ | |
minDisplacement: PropTypes.number, | |
/** | |
* Custom location icon of type mapbox-gl-native components | |
*/ | |
children: PropTypes.any, | |
}; | |
static defaultProps = { | |
animated: true, | |
visible: true, | |
showsUserHeadingIndicator: false, | |
minDisplacement: 0, | |
renderMode: 'normal', | |
}; | |
static RenderMode = { | |
Native: 'native', | |
Normal: 'normal', | |
}; | |
constructor(props) { | |
super(props); | |
this.state = { | |
shouldShowUserLocation: false, | |
coordinates: null, | |
heading: null, | |
lastCoordinates: null, | |
interactionArea: null | |
}; | |
this._onLocationUpdate = this._onLocationUpdate.bind(this); | |
} | |
// required as #setLocationManager attempts to setState | |
// after component unmount | |
_isMounted = null; | |
locationManagerRunning = false; | |
async componentDidMount() { | |
this._isMounted = true; | |
await this.setLocationManager({ | |
running: this.needsLocationManagerRunning(), | |
}); | |
if (this.props.renderMode === UserLocationCustom.RenderMode.Native) { | |
return; | |
} | |
locationManager.setMinDisplacement(this.props.minDisplacement); | |
} | |
async componentDidUpdate(prevProps) { | |
await this.setLocationManager({ | |
running: this.needsLocationManagerRunning(), | |
}); | |
if (this.props.minDisplacement !== prevProps.minDisplacement) { | |
locationManager.setMinDisplacement(this.props.minDisplacement); | |
} | |
} | |
async componentWillUnmount() { | |
this._isMounted = false; | |
await this.setLocationManager({running: false}); | |
} | |
/** | |
* Whether to start or stop listening to the locationManager | |
* | |
* Notice, that listening will start automatically when | |
* either `onUpdate` or `visible` are set | |
* | |
* @async | |
* @param {Object} running - Object with key `running` and `boolean` value | |
* @return {Promise<void>} | |
*/ | |
async setLocationManager({running}) { | |
if (this.locationManagerRunning !== running) { | |
this.locationManagerRunning = running; | |
if (running) { | |
locationManager.addListener(this._onLocationUpdate); | |
const location = await locationManager.getLastKnownLocation(); | |
this._onLocationUpdate(location); | |
} else { | |
locationManager.removeListener(this._onLocationUpdate); | |
} | |
} | |
} | |
/** | |
* | |
* If locationManager should be running | |
* | |
* @return {boolean} | |
*/ | |
needsLocationManagerRunning() { | |
return ( | |
!!this.props.onUpdate || | |
(this.props.renderMode === UserLocationCustom.RenderMode.Normal && | |
this.props.visible) | |
); | |
} | |
_onLocationUpdate(location) { | |
if (!this._isMounted || !location) { | |
return; | |
} | |
let coordinates = null; | |
let heading = null; | |
let interactionArea = this.state.interactionArea; | |
let lastCoordinates = this.state.lastCoordinates; | |
if (location && location.coords) { | |
const {longitude, latitude} = location.coords; | |
({heading} = location.coords); | |
coordinates = [longitude, latitude]; | |
if (lastCoordinates !== null && location.coords !== null) { | |
if (lastCoordinates[0] !== longitude || lastCoordinates[1] !== latitude) { | |
interactionArea = this._generateInteractionArea(coordinates); | |
} | |
} else { | |
interactionArea = this._generateInteractionArea(coordinates); | |
} | |
lastCoordinates = coordinates; | |
} | |
this.setState({ | |
coordinates, | |
heading, | |
interactionArea, | |
lastCoordinates | |
}); | |
if (this.props.onUpdate) { | |
this.props.onUpdate(location); | |
} | |
} | |
_generateInteractionArea(coords) { | |
return { | |
"type": "FeatureCollection", | |
"features": [ | |
circle(coords, interactionRadius, {units: 'meters', steps: 35}) | |
] | |
} | |
} | |
_renderNative() { | |
const {androidRenderMode, showsUserHeadingIndicator} = this.props; | |
const props = { | |
androidRenderMode, | |
iosShowsUserHeadingIndicator: showsUserHeadingIndicator, | |
}; | |
return <NativeUserLocation {...props} />; | |
} | |
render() { | |
const {heading, coordinates, interactionArea} = this.state; | |
const { | |
children, | |
visible, | |
showsUserHeadingIndicator, | |
onPress, | |
animated, | |
} = this.props; | |
if (!visible) { | |
return null; | |
} | |
if (this.props.renderMode === UserLocationCustom.RenderMode.Native) { | |
return this._renderNative(); | |
} | |
if (!coordinates) { | |
return null; | |
} | |
return ( | |
<> | |
<MapboxGL.ShapeSource | |
id="asdfasdfasdf" shape={interactionArea} | |
> | |
<MapboxGL.FillLayer | |
id="boundsFillaaaa" | |
belowLayerID="pinsLayer" | |
style={boundsStyle} | |
/> | |
</MapboxGL.ShapeSource> | |
<Annotation | |
animated={animated} | |
id="mapboxUserLocation" | |
onPress={onPress} | |
coordinates={coordinates} | |
style={{ | |
iconRotate: heading, | |
}} | |
> | |
{children || normalIcon(showsUserHeadingIndicator, heading)} | |
</Annotation> | |
</> | |
); | |
} | |
} | |
export default UserLocationCustom; | |
/** | |
* | |
* How to use and explanation at https://nsabater.com/react-native-mapboxgl-custom-userlocation | |
* Writteng by Nestor Sabater | |
* | |
* NOTE: The file should be split away in different smaller files, but the original source code for the | |
* react native mapboxgl dependency is written in a single file, so I preserved that pattern for this example. | |
* | |
***/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thanks for your work! helped a lot.