diff --git a/__tests__/interface.test.js b/__tests__/interface.test.js index b61651ae2..41036593c 100644 --- a/__tests__/interface.test.js +++ b/__tests__/interface.test.js @@ -14,6 +14,7 @@ describe('Public Interface', () => { 'Callout', 'Camera', 'UserLocation', + 'NativeUserLocation', 'StyleImport', // modules diff --git a/android/src/main/java/com/rnmapbox/rnmbx/components/location/LocationComponentManager.kt b/android/src/main/java/com/rnmapbox/rnmbx/components/location/LocationComponentManager.kt index 1deba35c9..6e19c6b77 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/components/location/LocationComponentManager.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/components/location/LocationComponentManager.kt @@ -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. @@ -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) @@ -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 @@ -78,10 +95,18 @@ 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 @@ -89,31 +114,33 @@ class LocationComponentManager(mapView: RNMBXMapView, context: Context) { 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 } } diff --git a/android/src/main/java/com/rnmapbox/rnmbx/components/location/RNMBXNativeUserLocation.kt b/android/src/main/java/com/rnmapbox/rnmbx/components/location/RNMBXNativeUserLocation.kt index 5a5bcf3ff..eeb869349 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/components/location/RNMBXNativeUserLocation.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/components/location/RNMBXNativeUserLocation.kt @@ -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 @@ -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) @@ -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 + ) } } } diff --git a/android/src/main/java/com/rnmapbox/rnmbx/components/location/RNMBXNativeUserLocationManager.kt b/android/src/main/java/com/rnmapbox/rnmbx/components/location/RNMBXNativeUserLocationManager.kt index 35de87a6c..58801d1b4 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/components/location/RNMBXNativeUserLocationManager.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/components/location/RNMBXNativeUserLocationManager.kt @@ -17,12 +17,32 @@ class RNMBXNativeUserLocationManager : ViewGroupManager @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 diff --git a/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/NativeMapViewModule.kt b/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/NativeMapViewModule.kt index 4b6f11133..1062a4d62 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/NativeMapViewModule.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/NativeMapViewModule.kt @@ -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 @@ -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" } diff --git a/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/RNMBXMapView.kt b/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/RNMBXMapView.kt index 1a99e2e27..ce719a167 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/RNMBXMapView.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/RNMBXMapView.kt @@ -39,14 +39,14 @@ import com.mapbox.maps.plugin.annotation.AnnotationConfig import com.mapbox.maps.plugin.annotation.annotations import com.mapbox.maps.plugin.annotation.generated.* import com.mapbox.maps.plugin.attribution.attribution -import com.mapbox.maps.plugin.attribution.generated.AttributionSettings import com.mapbox.maps.plugin.compass.compass -import com.mapbox.maps.plugin.compass.generated.CompassSettings import com.mapbox.maps.plugin.delegates.listeners.* import com.mapbox.maps.plugin.gestures.* -import com.mapbox.maps.plugin.logo.generated.LogoSettings +import com.mapbox.maps.plugin.locationcomponent.DefaultLocationProvider +import com.mapbox.maps.plugin.locationcomponent.LocationConsumer +import com.mapbox.maps.plugin.locationcomponent.LocationProvider +import com.mapbox.maps.plugin.locationcomponent.location import com.mapbox.maps.plugin.logo.logo -import com.mapbox.maps.plugin.scalebar.generated.ScaleBarSettings import com.mapbox.maps.plugin.scalebar.scalebar import com.mapbox.maps.viewannotation.ViewAnnotationManager import com.rnmapbox.rnmbx.R @@ -65,26 +65,22 @@ import com.rnmapbox.rnmbx.components.styles.layers.RNMBXLayer import com.rnmapbox.rnmbx.components.styles.light.RNMBXLight import com.rnmapbox.rnmbx.components.styles.sources.RNMBXSource import com.rnmapbox.rnmbx.components.styles.terrain.RNMBXTerrain -import com.rnmapbox.rnmbx.events.AndroidCallbackEvent +import com.rnmapbox.rnmbx.events.CameraChangeEvent import com.rnmapbox.rnmbx.events.IEvent import com.rnmapbox.rnmbx.events.MapChangeEvent -import com.rnmapbox.rnmbx.events.CameraChangeEvent import com.rnmapbox.rnmbx.events.MapClickEvent import com.rnmapbox.rnmbx.events.constants.EventTypes import com.rnmapbox.rnmbx.utils.* import com.rnmapbox.rnmbx.utils.extensions.toReadableArray import com.rnmapbox.rnmbx.v11compat.annotation.AnnotationID import com.rnmapbox.rnmbx.v11compat.annotation.INVALID_ANNOTATION_ID -import org.json.JSONException -import org.json.JSONObject -import java.util.* - -import com.mapbox.maps.MapboxMap.*; - import com.rnmapbox.rnmbx.v11compat.event.* import com.rnmapbox.rnmbx.v11compat.feature.* import com.rnmapbox.rnmbx.v11compat.mapboxmap.* import com.rnmapbox.rnmbx.v11compat.ornamentsettings.* +import org.json.JSONException +import org.json.JSONObject +import java.util.* fun MutableList.removeIf21(predicate: (T) -> Boolean): Boolean { var removed = false @@ -220,6 +216,9 @@ open class RNMBXMapView(private val mContext: Context, var mManager: RNMBXMapVie private lateinit var mMap: MapboxMap private lateinit var mMapView: MapView + private var mLocationConsumers = mutableListOf() + private var mCustomLocationProvider: LocationProvider? = null + private var mDefaultLocationProvider: LocationProvider? = null val isInitialized: Boolean get() = this::mMapView.isInitialized @@ -1117,6 +1116,51 @@ open class RNMBXMapView(private val mContext: Context, var mManager: RNMBXMapVie } } + fun setCustomLocation( + latitude: Double, + longitude: Double, + heading: Double?, + response: CommandResponse + ) { + var customLocationProvider: LocationProvider? = null + if (mCustomLocationProvider == null) { + customLocationProvider = object : LocationProvider { + override fun registerLocationConsumer(locationConsumer: LocationConsumer) { + mLocationConsumers.add(locationConsumer) + } + + override fun unRegisterLocationConsumer(locationConsumer: LocationConsumer) { + mLocationConsumers.remove(locationConsumer) + } + } + } + if (customLocationProvider != null) { + mDefaultLocationProvider = mMapView.location.getLocationProvider() + mMapView.location.setLocationProvider(customLocationProvider) + mCustomLocationProvider = customLocationProvider + } + + val point = Point.fromLngLat(longitude, latitude) + mLocationConsumers.forEach { + it.onLocationUpdated(point) + if (heading != null) { + it.onBearingUpdated(heading) + } + } + + response.success { } + } + + fun removeCustomLocationProvider(response: CommandResponse) { + mMapView.location.setLocationProvider( + mDefaultLocationProvider ?: DefaultLocationProvider( + mContext + ) + ) + mCustomLocationProvider = null + response.success { } + } + fun getVisibleBounds(response: CommandResponse) { val bounds = mMap!!.coordinateBoundsForCamera(mMap!!.cameraState.toCameraOptions()) diff --git a/android/src/main/mapbox-v11-compat/v10/com/rnmapbox/rnmbx/v11compat/Image.kt b/android/src/main/mapbox-v11-compat/v10/com/rnmapbox/rnmbx/v11compat/Image.kt index 6c60b747e..f0b84878d 100644 --- a/android/src/main/mapbox-v11-compat/v10/com/rnmapbox/rnmbx/v11compat/Image.kt +++ b/android/src/main/mapbox-v11-compat/v10/com/rnmapbox/rnmbx/v11compat/Image.kt @@ -2,6 +2,7 @@ package com.rnmapbox.rnmbx.v11compat.image; import android.content.Context import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.graphics.drawable.VectorDrawable import androidx.annotation.DrawableRes @@ -9,7 +10,6 @@ import androidx.appcompat.content.res.AppCompatResources import com.mapbox.maps.Image import com.mapbox.maps.Style import com.rnmapbox.rnmbx.components.images.ImageInfo -import com.rnmapbox.rnmbx.components.images.addBitmapImage import java.nio.ByteBuffer typealias ImageHolder = Drawable; @@ -26,6 +26,10 @@ fun VectorDrawable.toBitmapImageHolder(): Drawable { return this } +fun BitmapDrawable.toBitmapImageHolder(): ImageHolder { + return this +} + fun emptyImage(width: Int, height: Int): Image { return Image(width, height, ByteArray(width * height * 4) /* ByteBuffer.allocateDirect(width * height * 4).array()*/) } diff --git a/android/src/main/mapbox-v11-compat/v11/com/rnmapbox/rnmbx/v11compat/Image.kt b/android/src/main/mapbox-v11-compat/v11/com/rnmapbox/rnmbx/v11compat/Image.kt index 322760d84..7fea16503 100644 --- a/android/src/main/mapbox-v11-compat/v11/com/rnmapbox/rnmbx/v11compat/Image.kt +++ b/android/src/main/mapbox-v11-compat/v11/com/rnmapbox/rnmbx/v11compat/Image.kt @@ -2,10 +2,9 @@ package com.rnmapbox.rnmbx.v11compat.image; import android.content.Context import android.graphics.Bitmap -import android.graphics.drawable.Drawable +import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.VectorDrawable import androidx.annotation.DrawableRes -import androidx.appcompat.content.res.AppCompatResources import androidx.core.graphics.drawable.toBitmap import com.mapbox.bindgen.DataRef import com.mapbox.maps.Image @@ -13,7 +12,6 @@ import com.mapbox.maps.ImageHolder import com.mapbox.maps.Style import com.mapbox.maps.toMapboxImage import com.rnmapbox.rnmbx.components.images.ImageInfo -import java.nio.ByteBuffer import com.mapbox.maps.toMapboxImage as _toMapboxImage typealias ImageHolder = com.mapbox.maps.ImageHolder @@ -37,6 +35,10 @@ fun VectorDrawable.toBitmapImageHolder(): ImageHolder { return ImageHolder.from(this.toBitmap()) } +fun BitmapDrawable.toBitmapImageHolder(): ImageHolder { + return ImageHolder.from(this.toBitmap()) +} + class AppCompatResourcesV11 { companion object { fun getDrawableImageHolder(context: Context, @DrawableRes resId: Int) : ImageHolder? { diff --git a/android/src/main/old-arch/com/facebook/react/viewmanagers/RNMBXNativeUserLocationManagerDelegate.java b/android/src/main/old-arch/com/facebook/react/viewmanagers/RNMBXNativeUserLocationManagerDelegate.java index 0ffcbc2a7..942733ca4 100644 --- a/android/src/main/old-arch/com/facebook/react/viewmanagers/RNMBXNativeUserLocationManagerDelegate.java +++ b/android/src/main/old-arch/com/facebook/react/viewmanagers/RNMBXNativeUserLocationManagerDelegate.java @@ -28,6 +28,18 @@ public void setProperty(T view, String propName, @Nullable Object value) { case "iosShowsUserHeadingIndicator": mViewManager.setIosShowsUserHeadingIndicator(view, new DynamicFromObject(value)); break; + case "bearingImage": + mViewManager.setBearingImage(view, new DynamicFromObject(value)); + break; + case "shadowImage": + mViewManager.setShadowImage(view, new DynamicFromObject(value)); + break; + case "topImage": + mViewManager.setTopImage(view, new DynamicFromObject(value)); + break; + case "scale": + mViewManager.setScale(view, new DynamicFromObject(value)); + break; default: super.setProperty(view, propName, value); } diff --git a/android/src/main/old-arch/com/facebook/react/viewmanagers/RNMBXNativeUserLocationManagerInterface.java b/android/src/main/old-arch/com/facebook/react/viewmanagers/RNMBXNativeUserLocationManagerInterface.java index c42498ed7..ca7065622 100644 --- a/android/src/main/old-arch/com/facebook/react/viewmanagers/RNMBXNativeUserLocationManagerInterface.java +++ b/android/src/main/old-arch/com/facebook/react/viewmanagers/RNMBXNativeUserLocationManagerInterface.java @@ -15,4 +15,8 @@ public interface RNMBXNativeUserLocationManagerInterface { void setAndroidRenderMode(T view, Dynamic value); void setIosShowsUserHeadingIndicator(T view, Dynamic value); + void setBearingImage(T view, Dynamic value); + void setShadowImage(T view, Dynamic value); + void setTopImage(T view, Dynamic value); + void setScale(T view, Dynamic value); } diff --git a/android/src/main/old-arch/com/rnmapbox/rnmbx/NativeMapViewModuleSpec.java b/android/src/main/old-arch/com/rnmapbox/rnmbx/NativeMapViewModuleSpec.java index 5014f4c73..5f53d5fe6 100644 --- a/android/src/main/old-arch/com/rnmapbox/rnmbx/NativeMapViewModuleSpec.java +++ b/android/src/main/old-arch/com/rnmapbox/rnmbx/NativeMapViewModuleSpec.java @@ -86,4 +86,12 @@ public NativeMapViewModuleSpec(ReactApplicationContext reactContext) { @ReactMethod @DoNotStrip public abstract void querySourceFeatures(@Nullable Double viewRef, String sourceId, ReadableArray withFilter, ReadableArray withSourceLayerIDs, Promise promise); + + @ReactMethod + @DoNotStrip + public abstract void setCustomLocation(@Nullable Double viewRef, double latitude, double longitude, @Nullable Double heading, Promise promise); + + @ReactMethod + @DoNotStrip + public abstract void removeCustomLocationProvider(@Nullable Double viewRef, Promise promise); } diff --git a/docs/MapView.md b/docs/MapView.md index 9885b4ee5..3d91a8b4a 100644 --- a/docs/MapView.md +++ b/docs/MapView.md @@ -730,4 +730,26 @@ Show the attribution and telemetry action sheet.
If you implement a custom a +### setCustomLocation(latitude, longitude[, heading]) + +Sets up a custom location provider and applies the supplied location + +#### arguments +| Name | Type | Required | Description | +| ---- | :--: | :------: | :----------: | +| `latitude` | `number` | `Yes` | undefined | +| `longitude` | `number` | `Yes` | undefined | +| `heading` | `number` | `No` | undefined | + + +### removeCustomLocationProvider() + +Removes any previously set custom location provider + +#### arguments +| Name | Type | Required | Description | +| ---- | :--: | :------: | :----------: | + + + diff --git a/docs/NativeUserLocation.md b/docs/NativeUserLocation.md index 0e06bc5d0..81dd48198 100644 --- a/docs/NativeUserLocation.md +++ b/docs/NativeUserLocation.md @@ -39,6 +39,51 @@ iOS only. A Boolean value indicating whether the user location annotation may di +### topImageAsset + +```tsx +string +``` +The name of native image asset to use as the top layer for the location indicator. Native asset are under Image.xcassets on iOS and the drawables directory on android + + + +### bearingImageAsset + +```tsx +string +``` +The name of native image asset to use as the middle layer for the location indicator. Native asset are under Image.xcassets on iOS and the drawables directory on android + + + +### shadowImageAsset + +```tsx +string +``` +The name of native image asset to use as the background0 for the location indicator. Native asset are under Image.xcassets on iOS and the drawables directory on android + + + +### scale + +```tsx +number +``` +The size of the images, as a scale factor applied to the size of the specified image. + + + +### visible + +```tsx +boolean +``` +Whether location icon is visible + + + diff --git a/docs/UserLocation.md b/docs/UserLocation.md index 36990cd53..d7cea0a6e 100644 --- a/docs/UserLocation.md +++ b/docs/UserLocation.md @@ -82,6 +82,7 @@ Callback that is triggered on location update ```tsx UserLocationRenderMode ``` +@deprecated use NativeUserLocation component instead of UserLocationRenderMode.Native Which render mode to use. _defaults to:_ `UserLocationRenderMode.Normal` diff --git a/docs/docs.json b/docs/docs.json index b8ab4a1c6..8f7e173ab 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -3530,6 +3530,50 @@ "returns": null, "description": "Show the attribution and telemetry action sheet.\nIf you implement a custom attribution button, you should add this action to the button.", "examples": [] + }, + { + "name": "setCustomLocation", + "docblock": "Sets up a custom location provider and applies the supplied location\n@param latitude\n@param longitude\n@param heading\n@returns Promise", + "modifiers": [], + "params": [ + { + "name": "latitude", + "optional": false, + "type": { + "name": "number" + } + }, + { + "name": "longitude", + "optional": false, + "type": { + "name": "number" + } + }, + { + "name": "heading", + "optional": true, + "type": { + "name": "number" + } + } + ], + "returns": { + "description": "Promise" + }, + "description": "Sets up a custom location provider and applies the supplied location", + "examples": [] + }, + { + "name": "removeCustomLocationProvider", + "docblock": "Removes any previously set custom location provider\n@returns Promise", + "modifiers": [], + "params": [], + "returns": { + "description": "Promise" + }, + "description": "Removes any previously set custom location provider", + "examples": [] } ], "props": [ @@ -4118,6 +4162,41 @@ "type": "boolean", "default": "none", "description": "iOS only. A Boolean value indicating whether the user location annotation may display a permanent heading indicator.\n\n@platform ios" + }, + { + "name": "topImageAsset", + "required": false, + "type": "string", + "default": "none", + "description": "The name of native image asset to use as the top layer for the location indicator. Native asset are under Image.xcassets on iOS and the drawables directory on android" + }, + { + "name": "bearingImageAsset", + "required": false, + "type": "string", + "default": "none", + "description": "The name of native image asset to use as the middle layer for the location indicator. Native asset are under Image.xcassets on iOS and the drawables directory on android" + }, + { + "name": "shadowImageAsset", + "required": false, + "type": "string", + "default": "none", + "description": "The name of native image asset to use as the background0 for the location indicator. Native asset are under Image.xcassets on iOS and the drawables directory on android" + }, + { + "name": "scale", + "required": false, + "type": "number", + "default": "none", + "description": "The size of the images, as a scale factor applied to the size of the specified image." + }, + { + "name": "visible", + "required": false, + "type": "boolean", + "default": "none", + "description": "Whether location icon is visible" } ], "fileNameWithExt": "NativeUserLocation.tsx", @@ -7254,7 +7333,7 @@ "required": false, "type": "UserLocationRenderMode", "default": "UserLocationRenderMode.Normal", - "description": "Which render mode to use." + "description": "@deprecated use NativeUserLocation component instead of UserLocationRenderMode.Native\nWhich render mode to use." }, { "name": "requestsAlwaysUse", diff --git a/example/src/examples/UserLocation/UserLocationNativeAnimated.tsx b/example/src/examples/UserLocation/UserLocationNativeAnimated.tsx new file mode 100644 index 000000000..49f65ff71 --- /dev/null +++ b/example/src/examples/UserLocation/UserLocationNativeAnimated.tsx @@ -0,0 +1,108 @@ +import React, { useRef, useEffect } from 'react'; +import { SafeAreaView } from 'react-native'; +import { + MapView, + Camera, + UserTrackingMode, + NativeUserLocation, +} from '@rnmapbox/maps'; +import { + Feature, + LineString, + Properties, + lineString as makeLineString, +} from '@turf/helpers'; + +import { directionsClient } from '../../MapboxClient'; +import { DEFAULT_CENTER_COORDINATE, SF_OFFICE_COORDINATE } from '../../utils'; +import { ExampleWithMetadata } from '../common/ExampleMetadata'; + +const styles = { matchParent: { flex: 1 } }; +const SF_ZOO_COORDINATE = [-122.505412, 37.737463]; + +const UserLocationNativeAnimated = () => { + const mapRef = useRef(null); + + useEffect(() => { + const ref = mapRef.current; + const reqOptions = { + waypoints: [ + { coordinates: SF_OFFICE_COORDINATE }, + { coordinates: SF_ZOO_COORDINATE }, + ], + profile: 'driving', + geometries: 'geojson', + }; + + let timer: NodeJS.Timeout | null = null; + let route: Feature | null = null; + let idx = 0; + + const setPosition = () => { + if (route == null) { + console.error('no route given'); + return; + } + if (idx + 1 > route.geometry.coordinates.length) { + idx = 0; + } + const coordinates = route.geometry.coordinates[idx++]; + ref?.setCustomLocation(coordinates[1], coordinates[0]); + timer = setTimeout(setPosition, 1000); + }; + + directionsClient + .getDirections(reqOptions) + .send() + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + /* @ts-ignore */ + .then((res) => { + route = makeLineString(res.body.routes[0].geometry.coordinates); + setPosition(); + }); + + return () => { + ref?.removeCustomLocationProvider(); + if (timer != null) { + clearTimeout(timer); + } + }; + }, []); + + return ( + + + + + + + ); +}; + +export default UserLocationNativeAnimated; + +const metadata: ExampleWithMetadata['metadata'] = { + title: 'User Location Native Animated', + tags: [ + 'UserLocation', + 'UserLocation#nativeTopImage', + 'MapView#setCustomLocation', + 'MapView#removeCustomLocationProvider', + ], + docs: ` + Demonstrates native UserLocation being natively animated using a custom location provider + `, +}; +UserLocationNativeAnimated.metadata = metadata; diff --git a/example/src/examples/UserLocation/index.js b/example/src/examples/UserLocation/index.js index 7b1a90bc5..b91e28bef 100644 --- a/example/src/examples/UserLocation/index.js +++ b/example/src/examples/UserLocation/index.js @@ -3,6 +3,7 @@ export { default as SetTintColor } from './SetTintColor'; export { default as UserLocationPadding } from './UserLocationPadding'; export { default as UserLocationRenderMode } from './UserLocationRenderMode'; export { default as UserLocationUpdates } from './UserLocationUpdates'; +export { default as UserLocationNativeAnimated } from './UserLocationNativeAnimated'; export const metadata = { title: 'User Location', diff --git a/ios/RNMBX/RNMBXMapView.swift b/ios/RNMBX/RNMBXMapView.swift index 22923ff93..60dac4941 100644 --- a/ios/RNMBX/RNMBXMapView.swift +++ b/ios/RNMBX/RNMBXMapView.swift @@ -14,6 +14,130 @@ class FeatureEntry { } } +#if RNMBX_11 +class CustomHeadingProvider: HeadingProvider { + var latestHeading: Heading? + private let observers: NSHashTable = .weakObjects() + + func add(headingObserver: HeadingObserver) { + observers.add(headingObserver) + } + + func remove(headingObserver: HeadingObserver) { + observers.remove(headingObserver) + } + + func setHeading(heading: NSNumber) { + let latestHeading = Heading(direction: CLLocationDirection(truncating: heading), accuracy: CLLocationDirection(truncating: 1)) + self.latestHeading = latestHeading + for observer in observers.allObjects { + (observer as? HeadingObserver)?.onHeadingUpdate(latestHeading) + } + } +} + +class CustomLocationProvider: LocationProvider { + private var observers: NSHashTable = .weakObjects() + private var location: Location? = nil + + func addLocationObserver(for observer: LocationObserver) { + observers.add(observer) + } + + func removeLocationObserver(for observer: LocationObserver) { + observers.remove(observer) + } + + func getLastObservedLocation() -> Location? { + return location + } + + func setLocation(latitude: NSNumber, longitude: NSNumber) { + let lat = CLLocationDegrees(truncating: latitude) + let lon = CLLocationDegrees(truncating: longitude) + self.location = Location(clLocation: CLLocation(latitude: lat, longitude: lon)) + for observer in observers.allObjects { + (observer as? LocationObserver)?.onLocationUpdateReceived(for: [self.location!]) + } + } +} +#else +final public class CustomHeading: CLHeading { + private var _magneticHeading: CLLocationDirection = 0 + + public override var trueHeading: CLLocationDirection { + get { -1 } + } + + public override var magneticHeading: CLLocationDirection { + get { _magneticHeading } + set { _magneticHeading = newValue } + } +} + +class CustomLocationProvider: LocationProvider { + var locationProviderOptions: MapboxMaps.LocationOptions = .init() + + var authorizationStatus: CLAuthorizationStatus = .authorizedAlways + + var accuracyAuthorization: CLAccuracyAuthorization = .fullAccuracy + + var heading: CLHeading? + var location: CLLocation? + var updateLocation = false + var updateHeading = false + + var locationProviderDelegate: MapboxMaps.LocationProviderDelegate? + + func setDelegate(_ delegate: MapboxMaps.LocationProviderDelegate) { + locationProviderDelegate = delegate + } + + func setLocation(latitude: NSNumber, longitude: NSNumber) { + let lat = CLLocationDegrees(truncating: latitude) + let lon = CLLocationDegrees(truncating: longitude) + self.location = CLLocation(latitude: lat, longitude: lon) + if (updateLocation) { + locationProviderDelegate?.locationProvider(self, didUpdateLocations: [self.location!]) + } + } + + func setHeading(heading: NSNumber) { + let latestHeading = CustomHeading() + latestHeading.magneticHeading = CLLocationDirection(truncating: heading) + self.heading = latestHeading + if (self.updateHeading) { + locationProviderDelegate?.locationProvider(self, didUpdateHeading: self.heading!) + } + } + + func requestAlwaysAuthorization() { } + + func requestWhenInUseAuthorization() { } + + func requestTemporaryFullAccuracyAuthorization(withPurposeKey purposeKey: String) { } + + func startUpdatingLocation() { + self.updateLocation = true + } + + func stopUpdatingLocation() { + self.updateLocation = false + } + + var headingOrientation: CLDeviceOrientation = .unknown + + func startUpdatingHeading() { + self.updateHeading = true + } + + func stopUpdatingHeading() { + self.updateHeading = false + } + + func dismissHeadingCalibrationDisplay() { } +} +#endif #if RNMBX_11 extension QueriedRenderedFeature { @@ -151,6 +275,13 @@ open class RNMBXMapView : MapView { get { return self } } + var customLocationProvider: CustomLocationProvider? = nil + #if RNMBX_11 + var customHeadingProvider: CustomHeadingProvider? = nil + #else + var defaultLocationProvider: LocationProvider? + #endif + @objc public func addToMap(_ subview: UIView) { if let mapComponent = subview as? RNMBXMapComponent { let style = mapView.mapboxMap.style @@ -535,6 +666,46 @@ open class RNMBXMapView : MapView { } } } + + func setCustomLocation( + latitude: NSNumber, + longitude: NSNumber, + heading: NSNumber + ) -> Void { + #if RNMBX_11 + if (customLocationProvider == nil && customHeadingProvider == nil) { + customLocationProvider = CustomLocationProvider() + customHeadingProvider = CustomHeadingProvider() + mapView.location.override(locationProvider: customLocationProvider!, headingProvider: customHeadingProvider) + } + + customLocationProvider?.setLocation(latitude: latitude, longitude: longitude) + customHeadingProvider?.setHeading(heading: heading) + #else + if (customLocationProvider == nil) { + if (defaultLocationProvider == nil) { + defaultLocationProvider = mapView.location.locationProvider + } + customLocationProvider = CustomLocationProvider() + mapView.location.overrideLocationProvider(with: customLocationProvider!) + } + customLocationProvider?.setLocation(latitude: latitude, longitude: longitude) + customLocationProvider?.setHeading(heading: heading) + #endif + } + + func removeCustomLocationProvider() { + #if RNMBX_11 + mapView.location.override(provider: AppleLocationProvider()) + customLocationProvider = nil + customHeadingProvider = nil + #else + if let provider = defaultLocationProvider { + mapView.location.overrideLocationProvider(with: provider) + } + customLocationProvider = nil + #endif + } } // MARK: - event handlers diff --git a/ios/RNMBX/RNMBXMapViewManager.swift b/ios/RNMBX/RNMBXMapViewManager.swift index 619066ee1..21d8a8b00 100644 --- a/ios/RNMBX/RNMBXMapViewManager.swift +++ b/ios/RNMBX/RNMBXMapViewManager.swift @@ -246,5 +246,25 @@ extension RNMBXMapViewManager { } } } - + + @objc public static func setCustomLocation( + _ view: RNMBXMapView, + latitude: NSNumber, + longitude: NSNumber, + heading: NSNumber, + resolver: @escaping RCTPromiseResolveBlock, + rejecter: @escaping RCTPromiseRejectBlock + ) -> Void { + view.setCustomLocation(latitude: latitude, longitude: longitude, heading: heading) + resolver(nil) + } + + @objc public static func removeCustomLocationProvider( + _ view: RNMBXMapView, + resolver: @escaping RCTPromiseResolveBlock, + rejecter: @escaping RCTPromiseRejectBlock + ) -> Void { + view.removeCustomLocationProvider() + resolver(nil) + } } diff --git a/ios/RNMBX/RNMBXMapViewModule.mm b/ios/RNMBX/RNMBXMapViewModule.mm index 607867e9a..4bdc630b8 100644 --- a/ios/RNMBX/RNMBXMapViewModule.mm +++ b/ios/RNMBX/RNMBXMapViewModule.mm @@ -139,6 +139,17 @@ - (void)withMapView:(nonnull NSNumber*)viewRef block:(void (^)(RNMBXMapView *))b } reject:reject methodName:@"querySourceFeatures"]; } +RCT_EXPORT_METHOD(setCustomLocation:(nonnull NSNumber*)viewRef latitude:(nonnull NSNumber*)latitude longitude:(nonnull NSNumber*)longitude heading:(nonnull NSNumber*)heading resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { + [self withMapView:viewRef block:^(RNMBXMapView *view) { + [RNMBXMapViewManager setCustomLocation:view latitude:latitude longitude:longitude heading:heading resolver:resolve rejecter:reject]; + } reject:reject methodName:@"setCustomLocation"]; +} + +RCT_EXPORT_METHOD(removeCustomLocationProvider:(nonnull NSNumber*)viewRef resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { + [self withMapView:viewRef block:^(RNMBXMapView *view) { + [RNMBXMapViewManager removeCustomLocationProvider:view resolver:resolve rejecter:reject]; + } reject:reject methodName:@"removeCustomLocationProvider"]; +} // Thanks to this guard, we won't compile this code when we build for the old architecture. #ifdef RCT_NEW_ARCH_ENABLED diff --git a/ios/RNMBX/RNMBXNativeUserLocation.swift b/ios/RNMBX/RNMBXNativeUserLocation.swift index 2443154aa..04a660f0f 100644 --- a/ios/RNMBX/RNMBXNativeUserLocation.swift +++ b/ios/RNMBX/RNMBXNativeUserLocation.swift @@ -15,8 +15,45 @@ public class RNMBXNativeUserLocation : UIView, RNMBXMapComponent { } } + @objc + var topImage : NSString? = nil { + didSet { + if let map = self.map { _applySettings(map) } + } + } + + @objc + var bearingImage : NSString? = nil { + didSet { + if let map = self.map { _applySettings(map) } + } + } + + @objc + var shadowImage : NSString? = nil { + didSet { + if let map = self.map { _applySettings(map) } + } + } + + @objc + var scale : NSNumber? = nil { + didSet { + if let map = self.map { _applySettings(map) } + } + } + func _applySettings(_ map: RNMBXMapView) { - map.location.options.puckType = .puck2D(.makeDefault(showBearing: iosShowsUserHeadingIndicator)) + if (self.topImage != nil || self.bearingImage != nil || self.shadowImage != nil) { + map.location.options.puckType = .puck2D(Puck2DConfiguration( + topImage: self.topImage != nil ? UIImage(named: self.topImage! as String, in: .main, compatibleWith: nil)! : nil, + bearingImage: self.bearingImage != nil ? UIImage(named: self.bearingImage! as String, in: .main, compatibleWith: nil)! : nil, + shadowImage: self.shadowImage != nil ? UIImage(named: self.shadowImage! as String, in: .main, compatibleWith: nil)! : nil, + scale: self.scale != nil ? .constant(scale as! Double) : nil + )) + } else { + map.location.options.puckType = .puck2D(.makeDefault(showBearing: iosShowsUserHeadingIndicator)) + } if (iosShowsUserHeadingIndicator) { #if RNMBX_11 map.location.options.puckBearing = .heading diff --git a/ios/RNMBX/RNMBXNativeUserLocationViewManager.m b/ios/RNMBX/RNMBXNativeUserLocationViewManager.m index 8dc6c6711..b61898763 100644 --- a/ios/RNMBX/RNMBXNativeUserLocationViewManager.m +++ b/ios/RNMBX/RNMBXNativeUserLocationViewManager.m @@ -4,6 +4,10 @@ @interface RCT_EXTERN_REMAP_MODULE(RNMBXNativeUserLocation, RNMBXNativeUserLocationViewManager, RCTViewManager) RCT_EXPORT_VIEW_PROPERTY(iosShowsUserHeadingIndicator, BOOL); +RCT_EXPORT_VIEW_PROPERTY(topImage, NSString); +RCT_EXPORT_VIEW_PROPERTY(bearingImage, NSString); +RCT_EXPORT_VIEW_PROPERTY(shadowImage, NSString); +RCT_EXPORT_VIEW_PROPERTY(scale, NSNumber); @end diff --git a/src/Mapbox.ts b/src/Mapbox.ts index 8bdba4413..524e5493e 100644 --- a/src/Mapbox.ts +++ b/src/Mapbox.ts @@ -5,6 +5,8 @@ export { type CameraPadding, type CameraAnimationMode, type CameraBounds, + type CameraStop, + type CameraStops, } from './components/Camera'; export { Atmosphere } from './components/Atmosphere'; export { default as MapView, type MapState } from './components/MapView'; @@ -17,6 +19,7 @@ export { default as UserLocation, UserLocationRenderMode, } from './components/UserLocation'; +export { default as NativeUserLocation } from './components/NativeUserLocation'; export { default as VectorSource } from './components/VectorSource'; export { ShapeSource } from './components/ShapeSource'; export { default as RasterSource } from './components/RasterSource'; diff --git a/src/components/MapView.tsx b/src/components/MapView.tsx index 111538d28..6be48dace 100644 --- a/src/components/MapView.tsx +++ b/src/components/MapView.tsx @@ -914,6 +914,29 @@ class MapView extends NativeBridgeComponent( return this._runNative('showAttribution'); } + /** + * Sets up a custom location provider and applies the supplied location + * @param latitude + * @param longitude + * @param heading + * @returns Promise + */ + setCustomLocation(latitude: number, longitude: number, heading?: number) { + return this._runNative('setCustomLocation', [ + latitude, + longitude, + heading ?? 0, + ]); + } + + /** + * Removes any previously set custom location provider + * @returns Promise + */ + removeCustomLocationProvider() { + return this._runNative('removeCustomLocationProvider'); + } + _decodePayload(payload: T | string): T { if (typeof payload === 'string') { return JSON.parse(payload); diff --git a/src/components/NativeUserLocation.tsx b/src/components/NativeUserLocation.tsx index 7f9cd2c96..29768ecb4 100644 --- a/src/components/NativeUserLocation.tsx +++ b/src/components/NativeUserLocation.tsx @@ -20,10 +20,58 @@ export type Props = { * @platform ios */ iosShowsUserHeadingIndicator?: boolean; + + /** + * The name of native image asset to use as the top layer for the location indicator. Native asset are under Image.xcassets on iOS and the drawables directory on android + */ + topImageAsset?: string; + + /** + * The name of native image asset to use as the middle layer for the location indicator. Native asset are under Image.xcassets on iOS and the drawables directory on android + */ + bearingImageAsset?: string; + + /** + * The name of native image asset to use as the background0 for the location indicator. Native asset are under Image.xcassets on iOS and the drawables directory on android + */ + shadowImageAsset?: string; + + /** + * The size of the images, as a scale factor applied to the size of the specified image. + */ + scale?: number; + + /** + * Whether location icon is visible + */ + visible?: boolean; }; const NativeUserLocation = memo((props: Props) => { - return ; + const { + bearingImageAsset: bearingImage, + shadowImageAsset: shadowImage, + topImageAsset: topImage, + androidRenderMode, + iosShowsUserHeadingIndicator, + scale, + visible, + } = props; + + if (visible === false) { + return null; + } + + return ( + + ); }); export default NativeUserLocation; diff --git a/src/components/UserLocation.tsx b/src/components/UserLocation.tsx index edc6bc747..01f459f10 100644 --- a/src/components/UserLocation.tsx +++ b/src/components/UserLocation.tsx @@ -100,6 +100,7 @@ type Props = { onUpdate?: (location: Location) => void; /** + * @deprecated use NativeUserLocation component instead of UserLocationRenderMode.Native * Which render mode to use. */ renderMode?: UserLocationRenderMode; diff --git a/src/specs/NativeMapViewModule.ts b/src/specs/NativeMapViewModule.ts index d57abde80..79fec2118 100644 --- a/src/specs/NativeMapViewModule.ts +++ b/src/specs/NativeMapViewModule.ts @@ -49,6 +49,13 @@ export interface Spec extends TurboModule { withFilter: ReadonlyArray, withSourceLayerIDs: ReadonlyArray, ) => Promise; + setCustomLocation: ( + viewRef: Int32 | null, + latitude: number, + longitude: number, + heading: number | null, + ) => Promise; + removeCustomLocationProvider: (viewRef: Int32 | null) => Promise; } export default TurboModuleRegistry.getEnforcing('RNMBXMapViewModule'); diff --git a/src/specs/RNMBXNativeUserLocationNativeComponent.ts b/src/specs/RNMBXNativeUserLocationNativeComponent.ts index 15731f1a2..c2bd38ac8 100644 --- a/src/specs/RNMBXNativeUserLocationNativeComponent.ts +++ b/src/specs/RNMBXNativeUserLocationNativeComponent.ts @@ -6,6 +6,10 @@ import { UnsafeMixed } from './codegenUtils'; export interface NativeProps extends ViewProps { androidRenderMode?: UnsafeMixed; iosShowsUserHeadingIndicator?: UnsafeMixed; + bearingImage?: UnsafeMixed; + shadowImage?: UnsafeMixed; + topImage?: UnsafeMixed; + scale?: UnsafeMixed; } export default codegenNativeComponent(