Mapbox Implementation
Mapbox
Mapbox Version
10.16.2
React Native Version
0.73.6
Platform
Android
@rnmapbox/maps version
10.1.36
Standalone component to reproduce
import React , { forwardRef } from 'react'
import useTheme from 'hooks/useTheme'
import { setAccessToken , MapView , VectorSource , FillExtrusionLayer } from '@rnmapbox/maps'
import { MAPBOX_API_TOKEN } from 'constants/env'
type Props = typeof MapView . defaultProps
setAccessToken ( MAPBOX_API_TOKEN )
const Mapbox = forwardRef < MapView , Props > ( ( props , ref ) => {
const { isDarkMode, colors } = useTheme ( )
return (
< MapView
ref = { ref }
style = { [ { flex : 1 } , props . style ] }
{ ...props }
styleURL = { isDarkMode ? 'mapbox://styles/mapbox/dark-v11' : undefined } >
< VectorSource id = "composite" >
< FillExtrusionLayer
id = "3d-buildings"
sourceLayerID = "building"
style = { {
fillExtrusionColor : colors . surface ,
fillExtrusionHeight : [ 'get' , 'height' ] ,
fillExtrusionOpacity : 1 ,
} }
minZoomLevel = { 0 }
maxZoomLevel = { 200 }
/>
</ VectorSource >
{ props . children }
</ MapView >
)
} )
export default Mapbox
import React , { useEffect , useMemo , useState } from 'react'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { useTripCoordinates } from 'services/gps/api/trips/coordinates'
import useTheme from 'hooks/useTheme'
import moment from 'moment'
import { StyleSheet , View } from 'react-native'
import { Text } from 'react-native-paper'
import Mapbox from 'components/Mapbox'
import { Camera , LineLayer , MarkerView , ShapeSource , SkyLayer } from '@rnmapbox/maps'
import FA5Icon from 'react-native-vector-icons/FontAwesome5'
import IconButton from 'components/Buttons/IconButton'
import PlayerControl from 'components/PlayerControl'
import { Padding } from 'styles'
import { SignedInGpsScreenPropsFor } from 'navigation/types'
import { MapState } from '@rnmapbox/maps/src/components/MapView'
const toRadians = ( deg : number ) => ( deg * Math . PI ) / 180
const toDegrees = ( rad : number ) => ( rad * 180 ) / Math . PI
const GpsReplayTripScreen = ( { navigation, route } : SignedInGpsScreenPropsFor < 'GpsReplayTripScreen' > ) => {
const { id } = route . params
const { colors } = useTheme ( )
const [ hasFollowingStarted , setHasFollowingStarted ] = useState ( false )
const [ isPlaying , setIsPlaying ] = useState ( false )
const [ progress , setProgress ] = useState ( 0 )
const [ mapState , setMapState ] = useState < MapState > ( {
gestures : { isGestureActive : false } ,
properties : { bounds : { ne : [ ] , sw : [ ] } , center : [ ] , heading : 0 , pitch : 0 , zoom : 0 } ,
timestamp : 0 ,
} )
const { top } = useSafeAreaInsets ( )
const { data, isLoading } = useTripCoordinates ( { trip_id : id } )
const coordinates = useMemo ( ( ) => data ?. map ( item => [ + item . lng , + item . lat ] ) || [ ] , [ data ] )
const [ drivingSpeed , setDrivingSpeed ] = useState ( 0 )
const [ timestamp , setTimestamp ] = useState ( '' )
const timeRange = useMemo < number > ( ( ) => {
if ( ! data || data ?. length < 2 ) return 0
const start = data ?. [ 0 ] ?. timestamp
const end = data ?. [ data ?. length - 1 ] ?. timestamp
return moment ( end ) . diff ( start , 'seconds' ) || 0
} , [ data ] )
const position = useMemo ( ( ) => {
const total = coordinates . length
if ( total === 0 ) return { coordinate : [ 0 , 0 ] , heading : 0 }
const index = Math . floor ( progress * ( total - 1 ) )
const nextIndex = Math . min ( index + 1 , total - 1 )
const t = ( progress * ( total - 1 ) ) % 1
const currentData = data [ index ]
if ( currentData ) {
const speed = Number ( currentData . average_speed )
if ( speed !== drivingSpeed ) setDrivingSpeed ( speed )
if ( currentData . timestamp !== timestamp ) setTimestamp ( currentData . timestamp )
}
const [ lon1 , lat1 ] = coordinates [ index ]
const [ lon2 , lat2 ] = coordinates [ nextIndex ]
// Interpolated coordinate
const lon = lon1 + ( lon2 - lon1 ) * t
const lat = lat1 + ( lat2 - lat1 ) * t
// Heading calculation
const dLng = toRadians ( lon2 - lon1 )
const lat1Rad = toRadians ( lat1 )
const lat2Rad = toRadians ( lat2 )
const y = Math . sin ( dLng ) * Math . cos ( lat2Rad )
const x = Math . cos ( lat1Rad ) * Math . sin ( lat2Rad ) - Math . sin ( lat1Rad ) * Math . cos ( lat2Rad ) * Math . cos ( dLng )
const absoluteHeading = ( toDegrees ( Math . atan2 ( y , x ) ) + 360 ) % 360
const heading = absoluteHeading - 45 - mapState . properties . heading
return { coordinate : [ lon , lat ] , heading, absoluteHeading }
} , [ coordinates , data , drivingSpeed , mapState . properties . heading , progress , timestamp ] )
useEffect ( ( ) => {
const timeout = setTimeout ( ( ) => {
setHasFollowingStarted ( true )
} , 2000 )
return ( ) => clearTimeout ( timeout )
} , [ ] )
const markerSize = useMemo ( ( ) => mapState . properties . zoom * 3 , [ mapState . properties . zoom ] )
const lineWidth = useMemo ( ( ) => mapState . properties . zoom * 0.7 , [ mapState . properties . zoom ] )
return (
< View style = { styles . page } >
< Mapbox style = { styles . map } onCameraChanged = { setMapState } >
< Camera
heading = { position . absoluteHeading }
zoomLevel = { ! hasFollowingStarted ? 16 : undefined }
centerCoordinate = { position . coordinate }
animationDuration = { isPlaying ? 300 : 1000 }
animationMode = { hasFollowingStarted ? 'easeTo' : 'flyTo' }
pitch = { 60 }
/>
< SkyLayer id = "sky" style = { { skyType : 'atmosphere' } } />
< ShapeSource
id = "lineSource"
shape = { {
type : 'Feature' ,
properties : { } ,
geometry : {
type : 'LineString' ,
coordinates,
} ,
} } >
< LineLayer
id = "lineLayer"
style = { {
lineColor : colors . primary ,
lineWidth,
lineCap : 'round' ,
lineJoin : 'round' ,
} }
/>
</ ShapeSource >
< MarkerView id = "moving-object" coordinate = { position . coordinate } >
< FA5Icon
name = "location-arrow"
color = { colors . blue }
size = { markerSize }
style = { { transform : [ { rotate : `${ position . heading } deg` } ] } }
/>
</ MarkerView >
</ Mapbox >
< IconButton
style = { { top : top + 10 , position : 'absolute' , left : Padding } }
containerColor = { colors . onPrimary }
icon = "arrow-left"
onPress = { ( ) => navigation . goBack ( ) }
/>
< PlayerControl
timeRange = { timeRange }
Top = { < Text style = { { textAlign : 'center' } } > { `${ moment ( timestamp ) . format ( 'HH:mm' ) } ${ drivingSpeed } km/h` } </ Text > }
disabled = { isLoading }
isPlaying = { isPlaying }
setIsPlaying = { setIsPlaying }
progress = { progress }
setProgress = { setProgress }
/>
</ View >
)
}
const styles = StyleSheet . create ( {
page : { flex : 1 , justifyContent : 'center' , alignItems : 'center' } ,
map : { flex : 1 , width : '100%' } ,
} )
export default GpsReplayTripScreen
Mapbox.txt
GpsReplayTripScreen.txt
Observed behavior and steps to reproduce
I navigate to the map screen 2 or 3 times, sometime even more and the app crashes
Expected behavior
The app should not crash
Notes / preliminary analysis
I think the app has a memory leaks and it fails to clean up the map after the user navigates away from the map screen.
freeAllBuffers: 1 buffers were freed while being dequeued! getSlotFromBufferLocked: unknown buffer: 0x0 onDropViewInstance: view [1639] has a context that is not a ThemedReactContext: com.app.name.MainActivity@f1b4b5
Additional links and references
Screen.Recording.2025-07-02.at.6.12.56.PM.mov
Mapbox Implementation
Mapbox
Mapbox Version
10.16.2
React Native Version
0.73.6
Platform
Android
@rnmapbox/mapsversion10.1.36
Standalone component to reproduce
Mapbox.txt
GpsReplayTripScreen.txt
Observed behavior and steps to reproduce
I navigate to the map screen 2 or 3 times, sometime even more and the app crashes
Expected behavior
The app should not crash
Notes / preliminary analysis
I think the app has a memory leaks and it fails to clean up the map after the user navigates away from the map screen.
freeAllBuffers: 1 buffers were freed while being dequeued! getSlotFromBufferLocked: unknown buffer: 0x0 onDropViewInstance: view [1639] has a context that is not a ThemedReactContext: com.app.name.MainActivity@f1b4b5Additional links and references
Screen.Recording.2025-07-02.at.6.12.56.PM.mov