Skip to content

Instantly share code, notes, and snippets.

@nesjett
Created August 13, 2021 10:48
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nesjett/99388f73f1b4d82735bde89b90e24202 to your computer and use it in GitHub Desktop.
Save nesjett/99388f73f1b4d82735bde89b90e24202 to your computer and use it in GitHub Desktop.
React native mapbox custom UserLocation
/**
*
* 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.
*
***/
@joseortiz9
Copy link

Thanks for your work! helped a lot.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment