Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: custom location provider & native user location styling #3134

Closed
wants to merge 24 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f3cdd2b
[android] add custom location support for user location
g4rb4g3 Oct 12, 2023
02ed04e
[ios] add custom location support for user location
g4rb4g3 Oct 13, 2023
4475d67
add native user location images & scaling
g4rb4g3 Oct 13, 2023
e065131
Merge branch 'main' of https://github.com/rnmapbox/maps
g4rb4g3 Oct 13, 2023
68cf843
use list of listeners
g4rb4g3 Oct 13, 2023
f9c9cf4
add custom location provider cleanup
g4rb4g3 Oct 17, 2023
5936b02
Merge branch 'main' of https://github.com/rnmapbox/maps
g4rb4g3 Oct 17, 2023
135d2ba
fix: missing removeCustomLocationProvider
g4rb4g3 Oct 17, 2023
e1f5d58
remove custom location provider properly
g4rb4g3 Oct 18, 2023
83bca06
Merge branch 'main' of https://github.com/rnmapbox/maps
g4rb4g3 Oct 24, 2023
26001e6
fix: multiple custom location provider issue
g4rb4g3 Oct 24, 2023
f8de9e5
clean up
g4rb4g3 Oct 24, 2023
a2383d9
fix: v10 compile error
g4rb4g3 Oct 24, 2023
bdbf142
add example
g4rb4g3 Oct 24, 2023
e59545b
Update UserLocationNativeAnimated.tsx
g4rb4g3 Oct 24, 2023
d3be1ea
Merge branch 'main' of https://github.com/rnmapbox/maps
g4rb4g3 Oct 25, 2023
2910524
export more types
g4rb4g3 Oct 25, 2023
02ff8a1
export more types
g4rb4g3 Oct 25, 2023
8db3a30
Revert "export more types"
g4rb4g3 Oct 25, 2023
cf88292
Merge branch 'main' of https://github.com/rnmapbox/maps
g4rb4g3 Oct 25, 2023
f477910
Merge branch 'main' of https://github.com/rnmapbox/maps
g4rb4g3 Oct 27, 2023
0d28768
rename props, export NativeUserLocation
g4rb4g3 Oct 27, 2023
33a54bf
Merge branch 'main' of https://github.com/rnmapbox/maps
g4rb4g3 Oct 31, 2023
98bb0da
add mapbox-10 implementation for ios
g4rb4g3 Oct 31, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions __tests__/interface.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ describe('Public Interface', () => {
'Callout',
'Camera',
'UserLocation',
'NativeUserLocation',
'StyleImport',

// modules
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
package com.rnmapbox.rnmbx.components.location

import android.content.Context
import android.graphics.BitmapFactory
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.graphics.drawable.VectorDrawable
import androidx.appcompat.content.res.AppCompatResources
import android.graphics.drawable.BitmapDrawable
import androidx.lifecycle.Lifecycle
import com.mapbox.maps.plugin.locationcomponent.location
import com.rnmapbox.rnmbx.components.mapview.RNMBXMapView
import com.mapbox.maps.extension.style.expressions.dsl.generated.interpolate
import com.mapbox.maps.plugin.LocationPuck2D
import com.mapbox.maps.plugin.lifecycle.lifecycle
import com.mapbox.maps.plugin.locationcomponent.location
import com.rnmapbox.rnmbx.R
import com.rnmapbox.rnmbx.components.mapview.RNMBXMapView
import com.rnmapbox.rnmbx.location.LocationManager

import com.rnmapbox.rnmbx.v11compat.location.PuckBearingSource;
import com.rnmapbox.rnmbx.v11compat.image.*;
import com.rnmapbox.rnmbx.v11compat.image.AppCompatResourcesV11
import com.rnmapbox.rnmbx.v11compat.image.ImageHolder
import com.rnmapbox.rnmbx.v11compat.image.toBitmapImageHolder
import com.rnmapbox.rnmbx.v11compat.image.toByteArray
import com.rnmapbox.rnmbx.v11compat.image.toImageData
import com.rnmapbox.rnmbx.v11compat.location.PuckBearingSource

/**
* The LocationComponent on android implements display of user's current location.
Expand All @@ -23,9 +25,20 @@ import com.rnmapbox.rnmbx.v11compat.image.*;
* And NativeUserLocation can ask for display of user's current location - independent of Camera's user tracking.
*/
class LocationComponentManager(mapView: RNMBXMapView, context: Context) {
private val MAPBOX_BLUE_COLOR = Color.parseColor("#4A90E2")

var mMapView = mapView
var mContext = context
private var mState = State(showUserLocation=false, followUserLocation=false, tintColor= null, bearingImage = null, puckBearingSource =null)
private var mState = State(
showUserLocation = false,
followUserLocation = false,
tintColor = null,
bearingImage = null,
puckBearingSource = null,
topImage = null,
shadowImage = null,
scale = 1.0,
)

private var mLocationManager: LocationManager = LocationManager.getInstance(context)

Expand All @@ -37,8 +50,12 @@ class LocationComponentManager(mapView: RNMBXMapView, context: Context) {
val showUserLocation: Boolean,
val followUserLocation: Boolean,
val tintColor: Int?, // tint of location puck
var bearingImage: ImageHolder?, // bearing image (background)
var puckBearingSource: PuckBearingSource? // bearing source
var bearingImage: ImageHolder?, // The image used as the middle of the location indicator.
var topImage: ImageHolder?, // The image to use as the top layer for the location indicator.
var shadowImage: ImageHolder?, // The image that acts as a background of the location indicator.
var puckBearingSource: PuckBearingSource?, // bearing source
var pulsing: Boolean = true,
var scale: Double = 1.0,
) {
val enabled: Boolean
get() = showUserLocation || followUserLocation
Expand Down Expand Up @@ -78,42 +95,52 @@ class LocationComponentManager(mapView: RNMBXMapView, context: Context) {
mapView.location.updateSettings {
enabled = newState.enabled

if (fullUpdate || (newState.hidden != oldState.hidden) || (newState.tintColor != oldState.tintColor) || (newState.bearingImage != oldState.bearingImage)) {
if (fullUpdate ||
newState.hidden != oldState.hidden ||
newState.tintColor != oldState.tintColor ||
newState.bearingImage != oldState.bearingImage ||
newState.topImage != oldState.topImage ||
newState.shadowImage != oldState.shadowImage ||
newState.scale != oldState.scale
) {
if (newState.hidden) {
var emptyLocationPuck = LocationPuck2D()
val empty = AppCompatResourcesV11.getDrawableImageHolder(mContext, R.drawable.empty)
val empty =
AppCompatResourcesV11.getDrawableImageHolder(mContext, R.drawable.empty)
emptyLocationPuck.bearingImage = empty
emptyLocationPuck.shadowImage = empty
emptyLocationPuck.topImage = empty
//emptyLocationPuck.opacity = 0.0
locationPuck = emptyLocationPuck
pulsingEnabled = false
} else {
val mapboxBlueColor = Color.parseColor("#4A90E2")
val tintColor = newState.tintColor
val defaultLocationPuck = LocationPuck2D()
var topImage = AppCompatResourcesV11.getDrawableImageHolder(mContext, R.drawable.mapbox_user_icon)
if (tintColor != null) {
val drawable = AppCompatResources.getDrawable(mContext, R.drawable.mapbox_user_icon) as VectorDrawable?
drawable!!.setTint(tintColor)
var topImage = newState.topImage
if (tintColor != null && topImage != null) {
val imageData = (topImage as ByteArray).toImageData().toByteArray()
val drawable = BitmapDrawable(mContext.resources, BitmapFactory.decodeByteArray(imageData, 0, imageData.size))
drawable.setTint(tintColor)
topImage = drawable.toBitmapImageHolder()
}
defaultLocationPuck.topImage = topImage
val defaultBearingImage = AppCompatResourcesV11.getDrawableImageHolder(
mContext, R.drawable.mapbox_user_stroke_icon
val scaleExpression = if (newState.scale != 1.0) {
interpolate {
linear()
zoom()
stop {
literal(0)
literal(newState.scale)
}
}.toJson()
} else null

locationPuck = LocationPuck2D(
topImage = topImage,
bearingImage = newState.bearingImage,
shadowImage = newState.shadowImage,
scaleExpression = scaleExpression,
)
defaultLocationPuck.bearingImage = newState.bearingImage ?: defaultBearingImage
val shadowImage = AppCompatResourcesV11.getDrawableImageHolder(
mContext, R.drawable.mapbox_user_icon_shadow
)
defaultLocationPuck.shadowImage = shadowImage
locationPuck = defaultLocationPuck
pulsingEnabled = true
if (tintColor != null) {
pulsingColor = tintColor
} else {
pulsingColor = mapboxBlueColor
}
pulsingEnabled = newState.pulsing
pulsingColor = tintColor ?: MAPBOX_BLUE_COLOR
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,16 @@ package com.rnmapbox.rnmbx.components.location

import android.annotation.SuppressLint
import android.content.Context
import androidx.appcompat.content.res.AppCompatResources
import com.rnmapbox.rnmbx.components.mapview.OnMapReadyCallback
import com.mapbox.maps.MapboxMap
import com.mapbox.android.core.permissions.PermissionsManager
import com.mapbox.maps.MapboxMap
import com.mapbox.maps.Style
import com.rnmapbox.rnmbx.R
import com.rnmapbox.rnmbx.components.AbstractMapFeature
import com.rnmapbox.rnmbx.components.RemovalReason
import com.rnmapbox.rnmbx.components.mapview.OnMapReadyCallback
import com.rnmapbox.rnmbx.components.mapview.RNMBXMapView

import com.rnmapbox.rnmbx.v11compat.location.PuckBearingSource
import com.rnmapbox.rnmbx.v11compat.image.AppCompatResourcesV11
import com.rnmapbox.rnmbx.v11compat.location.PuckBearingSource

enum class RenderMode {
GPS, COMPASS, NORMAL
Expand All @@ -24,6 +22,26 @@ class RNMBXNativeUserLocation(context: Context) : AbstractMapFeature(context), O
private var mMap: MapboxMap? = null
private var mRenderMode : RenderMode = RenderMode.NORMAL;
private var mContext : Context = context
var mTopImage: String? = null
set(value) {
field = value
applyChanges()
}
var mBearingImage: String? = null
set(value) {
field = value
applyChanges()
}
var mShadowImage: String? = null
set(value) {
field = value
applyChanges()
}
var mScale: Double = 1.0
set(value) {
field = value
applyChanges()
}

override fun addToMap(mapView: RNMBXMapView) {
super.addToMap(mapView)
Expand Down Expand Up @@ -64,19 +82,78 @@ class RNMBXNativeUserLocation(context: Context) : AbstractMapFeature(context), O
mMapView?.locationComponentManager?.showNativeUserLocation(mEnabled)
}

@SuppressLint("DiscouragedApi")
fun applyChanges() {
mMapView?.locationComponentManager?.let {
val useCustomImages = mTopImage != null || mBearingImage != null || mShadowImage != null

val bearingImageResourceId = if (mBearingImage != null) {
context.resources.getIdentifier(
mBearingImage,
"drawable",
context.packageName
)
} else if (useCustomImages) {
null
} else when (mRenderMode) {
RenderMode.GPS -> R.drawable.mapbox_user_bearing_icon
RenderMode.COMPASS -> R.drawable.mapbox_user_puck_icon
RenderMode.NORMAL -> R.drawable.mapbox_user_stroke_icon
}

val topImageResourceId = if (mTopImage != null) {
context.resources.getIdentifier(
mTopImage,
"drawable",
context.packageName
)
} else if (useCustomImages) {
null
} else R.drawable.mapbox_user_icon

val shadowImageResourceId = if (mShadowImage != null) {
context.resources.getIdentifier(
mShadowImage,
"drawable",
context.packageName
)
} else if (useCustomImages) {
null
} else R.drawable.mapbox_user_icon_shadow

val puckBearingSource = when (mRenderMode) {
RenderMode.GPS -> PuckBearingSource.COURSE
RenderMode.COMPASS -> PuckBearingSource.HEADING
RenderMode.NORMAL -> null
}
val pulsing = mRenderMode == RenderMode.NORMAL


mMapView?.locationComponentManager?.let { locationComponentManager ->
// emulate https://docs.mapbox.com/android/legacy/maps/guides/location-component/
when (mRenderMode) {
RenderMode.NORMAL ->
it.update { it.copy(bearingImage = null, puckBearingSource = null)}
RenderMode.GPS -> it.update {
it.copy(bearingImage = AppCompatResourcesV11.getDrawableImageHolder(
mContext, R.drawable.mapbox_user_bearing_icon
), puckBearingSource = PuckBearingSource.COURSE) }
RenderMode.COMPASS -> it.update{ it.copy(bearingImage= AppCompatResourcesV11.getDrawableImageHolder(
mContext, R.drawable.mapbox_user_puck_icon
), puckBearingSource = PuckBearingSource.HEADING) }
locationComponentManager.update { state ->
state.copy(
bearingImage = bearingImageResourceId?.let { bearingImageResourceId ->
AppCompatResourcesV11.getDrawableImageHolder(
mContext,
bearingImageResourceId
)
},
topImage = topImageResourceId?.let { topImageResourceId ->
AppCompatResourcesV11.getDrawableImageHolder(
mContext,
topImageResourceId
)
},
shadowImage = shadowImageResourceId?.let { shadowImageResourceId ->
AppCompatResourcesV11.getDrawableImageHolder(
mContext,
shadowImageResourceId
)
},
puckBearingSource = puckBearingSource,
pulsing = pulsing,
scale = mScale
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,32 @@ class RNMBXNativeUserLocationManager : ViewGroupManager<RNMBXNativeUserLocation>
@ReactProp(name = "androidRenderMode")
override fun setAndroidRenderMode(userLocation: RNMBXNativeUserLocation, mode: Dynamic) {
when (mode.asString()) {
"compass" -> userLocation.setAndroidRenderMode(RenderMode.COMPASS);
"gps" -> userLocation.setAndroidRenderMode(RenderMode.GPS);
"normal" -> userLocation.setAndroidRenderMode(RenderMode.NORMAL);
"compass" -> userLocation.setAndroidRenderMode(RenderMode.COMPASS)
"gps" -> userLocation.setAndroidRenderMode(RenderMode.GPS)
"normal" -> userLocation.setAndroidRenderMode(RenderMode.NORMAL)
}
}

@ReactProp(name = "topImage")
override fun setTopImage(view: RNMBXNativeUserLocation, value: Dynamic?) {
view.mTopImage = value?.asString()
}

@ReactProp(name = "bearingImage")
override fun setBearingImage(view: RNMBXNativeUserLocation, value: Dynamic?) {
view.mBearingImage = value?.asString()
}

@ReactProp(name = "shadowImage")
override fun setShadowImage(view: RNMBXNativeUserLocation, value: Dynamic?) {
view.mShadowImage = value?.asString()
}

@ReactProp(name = "scale", defaultDouble = 1.0)
override fun setScale(view: RNMBXNativeUserLocation, value: Dynamic?) {
view.mScale = value?.asDouble() ?: 1.0
}

@ReactProp(name = "iosShowsUserHeadingIndicator")
override fun setIosShowsUserHeadingIndicator(view: RNMBXNativeUserLocation, value: Dynamic) {
// iOS only
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,13 @@
package com.rnmapbox.rnmbx.components.mapview

import android.util.Log
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.UIManager
import com.facebook.react.bridge.WritableMap
import com.facebook.react.bridge.WritableNativeMap
import com.facebook.react.uimanager.IllegalViewOperationException
import com.facebook.react.uimanager.UIManagerHelper
import com.facebook.react.uimanager.common.UIManagerType
import com.rnmapbox.rnmbx.BuildConfig
import com.rnmapbox.rnmbx.NativeMapViewModuleSpec
import com.rnmapbox.rnmbx.utils.ConvertUtils
import com.rnmapbox.rnmbx.utils.ExpressionParser
import com.rnmapbox.rnmbx.utils.Logger
import com.rnmapbox.rnmbx.utils.ViewTagResolver
import com.rnmapbox.rnmbx.utils.extensions.toCoordinate
import com.rnmapbox.rnmbx.utils.extensions.toScreenCoordinate
Expand Down Expand Up @@ -183,6 +176,24 @@ class NativeMapViewModule(context: ReactApplicationContext, val viewTagResolver:
}
}

override fun setCustomLocation(
viewRef: Double?,
latitude: Double,
longitude: Double,
heading: Double?,
promise: Promise
) {
withMapViewOnUIThread(viewRef, promise) {
it.setCustomLocation(latitude, longitude, heading, createCommandResponse(promise))
}
}

override fun removeCustomLocationProvider(viewRef: Double?, promise: Promise) {
withMapViewOnUIThread(viewRef, promise) {
it.removeCustomLocationProvider(createCommandResponse(promise))
}
}

companion object {
const val NAME = "RNMBXMapViewModule"
}
Expand Down
Loading
Loading