From fe0e267b2d4aa9fa2b5d900677a7f00e533d5500 Mon Sep 17 00:00:00 2001 From: g4rb4g3 Date: Fri, 10 Nov 2023 06:33:34 +0100 Subject: [PATCH 1/3] feat: custom location provider --- .../components/mapview/NativeMapViewModule.kt | 18 +++++++ .../rnmbx/components/mapview/RNMBXMapView.kt | 48 +++++++++++++++++++ .../rnmbx/NativeMapViewModuleSpec.java | 8 ++++ docs/MapView.md | 22 +++++++++ docs/docs.json | 44 +++++++++++++++++ src/components/MapView.tsx | 23 +++++++++ src/specs/NativeMapViewModule.ts | 7 +++ 7 files changed, 170 insertions(+) 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 57a812fc1..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 @@ -176,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 324c73735..15aae36ae 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 @@ -224,6 +224,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 @@ -1121,6 +1124,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/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 43a630409..cca48b1f9 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/docs.json b/docs/docs.json index 6c2040a9d..c1bbbbae2 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -3547,6 +3547,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": [ 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/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'); From cf504afc0fe55c309e04227efff4ef9d62755413 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Mon, 13 Nov 2023 16:18:04 +0100 Subject: [PATCH 2/3] feat: refactor custom location provider to component --- .../components/mapview/NativeMapViewModule.kt | 18 ------- .../rnmbx/components/mapview/RNMBXMapView.kt | 48 ------------------- .../rnmbx/NativeMapViewModuleSpec.java | 8 ---- docs/MapView.md | 22 --------- docs/docs.json | 44 ----------------- src/components/MapView.tsx | 23 --------- src/specs/NativeMapViewModule.ts | 7 --- 7 files changed, 170 deletions(-) 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 1062a4d62..57a812fc1 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 @@ -176,24 +176,6 @@ 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 15aae36ae..324c73735 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 @@ -224,9 +224,6 @@ 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 @@ -1124,51 +1121,6 @@ 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/old-arch/com/rnmapbox/rnmbx/NativeMapViewModuleSpec.java b/android/src/main/old-arch/com/rnmapbox/rnmbx/NativeMapViewModuleSpec.java index 5f53d5fe6..5014f4c73 100644 --- a/android/src/main/old-arch/com/rnmapbox/rnmbx/NativeMapViewModuleSpec.java +++ b/android/src/main/old-arch/com/rnmapbox/rnmbx/NativeMapViewModuleSpec.java @@ -86,12 +86,4 @@ 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 cca48b1f9..43a630409 100644 --- a/docs/MapView.md +++ b/docs/MapView.md @@ -730,26 +730,4 @@ 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/docs.json b/docs/docs.json index c1bbbbae2..6c2040a9d 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -3547,50 +3547,6 @@ "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": [ diff --git a/src/components/MapView.tsx b/src/components/MapView.tsx index 6be48dace..111538d28 100644 --- a/src/components/MapView.tsx +++ b/src/components/MapView.tsx @@ -914,29 +914,6 @@ 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/specs/NativeMapViewModule.ts b/src/specs/NativeMapViewModule.ts index 79fec2118..d57abde80 100644 --- a/src/specs/NativeMapViewModule.ts +++ b/src/specs/NativeMapViewModule.ts @@ -49,13 +49,6 @@ 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'); From 7546d9637ccec3e2393b3491ddd2e7092063fe00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Tue, 14 Nov 2023 08:46:40 +0100 Subject: [PATCH 3/3] feat(viewport): add FollowPuckViewportStateOptions --- .../java/com/rnmapbox/rnmbx/RNMBXPackage.kt | 25 +++-- .../rnmbx/components/camera/RNMBXViewport.kt | 98 +++++++++++++++++-- .../rnmbx/utils/extensions/ReadableMap.kt | 45 +++++++++ example/src/examples/Camera/Viewport.tsx | 5 +- ios/RNMBX/RNMBXViewport.swift | 87 ++++++++++++++-- ios/RNMBX/RNMBXViewportManager.swift | 2 +- src/components/Viewport.tsx | 43 ++++++++ src/specs/RNMBXViewportNativeComponent.ts | 19 +++- 8 files changed, 295 insertions(+), 29 deletions(-) diff --git a/android/src/main/java/com/rnmapbox/rnmbx/RNMBXPackage.kt b/android/src/main/java/com/rnmapbox/rnmbx/RNMBXPackage.kt index 747cb395e..f2865a3f1 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/RNMBXPackage.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/RNMBXPackage.kt @@ -51,7 +51,7 @@ import com.rnmapbox.rnmbx.utils.ViewTagResolver class RNMBXPackage : TurboReactPackage() { var viewTagResolver: ViewTagResolver? = null - fun getViewTagResolver(context: ReactApplicationContext) : ViewTagResolver { + fun getViewTagResolver(context: ReactApplicationContext, module: String) : ViewTagResolver { val viewTagResolver = viewTagResolver if (viewTagResolver == null) { val result = ViewTagResolver(context) @@ -61,6 +61,10 @@ class RNMBXPackage : TurboReactPackage() { return viewTagResolver } + fun resetViewTagResolver() { + viewTagResolver = null + } + override fun getModule( s: String, reactApplicationContext: ReactApplicationContext @@ -71,11 +75,11 @@ class RNMBXPackage : TurboReactPackage() { RNMBXOfflineModule.REACT_CLASS -> return RNMBXOfflineModule(reactApplicationContext) RNMBXSnapshotModule.REACT_CLASS -> return RNMBXSnapshotModule(reactApplicationContext) RNMBXLogging.REACT_CLASS -> return RNMBXLogging(reactApplicationContext) - NativeMapViewModule.NAME -> return NativeMapViewModule(reactApplicationContext, getViewTagResolver(reactApplicationContext)) - RNMBXViewportModule.NAME -> return RNMBXViewportModule(reactApplicationContext, getViewTagResolver(reactApplicationContext)) - RNMBXShapeSourceModule.NAME -> return RNMBXShapeSourceModule(reactApplicationContext, getViewTagResolver(reactApplicationContext)) - RNMBXImageModule.NAME -> return RNMBXImageModule(reactApplicationContext, getViewTagResolver(reactApplicationContext)) - RNMBXPointAnnotationModule.NAME -> return RNMBXPointAnnotationModule(reactApplicationContext, getViewTagResolver(reactApplicationContext)) + NativeMapViewModule.NAME -> return NativeMapViewModule(reactApplicationContext, getViewTagResolver(reactApplicationContext, s)) + RNMBXViewportModule.NAME -> return RNMBXViewportModule(reactApplicationContext, getViewTagResolver(reactApplicationContext, s)) + RNMBXShapeSourceModule.NAME -> return RNMBXShapeSourceModule(reactApplicationContext, getViewTagResolver(reactApplicationContext, s)) + RNMBXImageModule.NAME -> return RNMBXImageModule(reactApplicationContext, getViewTagResolver(reactApplicationContext, s)) + RNMBXPointAnnotationModule.NAME -> return RNMBXPointAnnotationModule(reactApplicationContext, getViewTagResolver(reactApplicationContext, s)) } return null } @@ -91,26 +95,26 @@ class RNMBXPackage : TurboReactPackage() { // components managers.add(RNMBXCameraManager(reactApplicationContext)) managers.add(RNMBXViewportManager(reactApplicationContext)) - managers.add(RNMBXMapViewManager(reactApplicationContext, getViewTagResolver(reactApplicationContext))) + managers.add(RNMBXMapViewManager(reactApplicationContext, getViewTagResolver(reactApplicationContext, "RNMBXMapViewManager"))) managers.add(RNMBXStyleImportManager(reactApplicationContext)) // annotations managers.add(RNMBXMarkerViewManager(reactApplicationContext)) - managers.add(RNMBXPointAnnotationManager(reactApplicationContext, getViewTagResolver(reactApplicationContext))) + managers.add(RNMBXPointAnnotationManager(reactApplicationContext, getViewTagResolver(reactApplicationContext, "RNMBXPointAnnotationManager"))) managers.add(RNMBXCalloutManager()) managers.add(RNMBXNativeUserLocationManager()) managers.add(RNMBXCustomLocationProviderManager()) // sources managers.add(RNMBXVectorSourceManager(reactApplicationContext)) - managers.add(RNMBXShapeSourceManager(reactApplicationContext, getViewTagResolver(reactApplicationContext))) + managers.add(RNMBXShapeSourceManager(reactApplicationContext, getViewTagResolver(reactApplicationContext, "RNMBXShapeSourceManager"))) managers.add(RNMBXRasterDemSourceManager(reactApplicationContext)) managers.add(RNMBXRasterSourceManager(reactApplicationContext)) managers.add(RNMBXImageSourceManager()) // images managers.add(RNMBXImagesManager(reactApplicationContext)) - managers.add(RNMBXImageManager(reactApplicationContext, getViewTagResolver(reactApplicationContext))) + managers.add(RNMBXImageManager(reactApplicationContext, getViewTagResolver(reactApplicationContext, "RNMBXImageManager"))) // layers managers.add(RNMBXFillLayerManager()) @@ -129,6 +133,7 @@ class RNMBXPackage : TurboReactPackage() { } override fun getReactModuleInfoProvider(): ReactModuleInfoProvider { + resetViewTagResolver() return ReactModuleInfoProvider { val moduleInfos: MutableMap = HashMap() val isTurboModule = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED diff --git a/android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXViewport.kt b/android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXViewport.kt index ab466fe48..6795fdf90 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXViewport.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXViewport.kt @@ -7,6 +7,7 @@ import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.Callback import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.ReadableType import com.facebook.react.bridge.UIManager import com.facebook.react.bridge.WritableMap import com.facebook.react.uimanager.UIManagerHelper @@ -27,12 +28,13 @@ import com.rnmapbox.rnmbx.components.AbstractMapFeature import com.rnmapbox.rnmbx.components.mapview.RNMBXMapView import com.rnmapbox.rnmbx.modules.RNMBXLogging import com.rnmapbox.rnmbx.utils.Logger -import com.rnmapbox.rnmbx.utils.extensions.getAndLogIfNotBoolean -import com.rnmapbox.rnmbx.utils.extensions.getAndLogIfNotDouble -import com.rnmapbox.rnmbx.utils.extensions.getAndLogIfNotString +import com.rnmapbox.rnmbx.utils.extensions.* import com.rnmapbox.rnmbx.utils.writableMapOf import com.facebook.react.uimanager.events.Event +import com.mapbox.maps.EdgeInsets +import com.mapbox.maps.plugin.viewport.data.FollowPuckViewportStateBearing +import com.mapbox.maps.plugin.viewport.data.FollowPuckViewportStateOptions import com.rnmapbox.rnmbx.events.constants.EventKeys class BaseEvent( @@ -116,9 +118,11 @@ mContext applyHasStatusChanged(mapView.mapView) } - fun toState(viewport: ViewportPlugin, state: ReadableMap): ViewportState? { + private fun toState(viewport: ViewportPlugin, state: ReadableMap): ViewportState? { return when (val kind = state.getAndLogIfNotString("kind")) { - "followPuck" -> viewport.makeFollowPuckViewportState() + "followPuck" -> viewport.makeFollowPuckViewportState( + parseFollowViewportOptions(state) + ) //"overview" -> return viewport.makeOverviewViewportState() else -> { Logger.e(LOG_TAG, "toState: unexpected state: $kind") @@ -127,7 +131,79 @@ mContext } } - fun toDefaultViewportTransitionOptions(state: ReadableMap?): DefaultViewportTransitionOptions { + data class FollowPuckViewportStateBearingOrNull(val state: FollowPuckViewportStateBearing?) + private fun parseFollowViewportOptions(state: ReadableMap): FollowPuckViewportStateOptions { + val builder = FollowPuckViewportStateOptions.Builder() + state.getAndLogIfNotMap("options", LOG_TAG)?.let { options -> + if (options.hasKey("zoom")) { + if (options.isKeep("zoom")) { + builder.zoom(null) + } else { + options.getAndLogIfNotDouble("zoom", LOG_TAG)?.let { zoom -> + builder.zoom(zoom) + } + } + } + if (options.hasKey("pitch")) { + if (options.isKeep("pitch")) { + builder.pitch(null) + } else { + options.getAndLogIfNotDouble("pitch", LOG_TAG)?.let {pitch -> + builder.pitch(pitch) + } + } + } + + if (options.hasKey("bearing")) { + when (options.getType("bearing")) { + ReadableType.Number -> + FollowPuckViewportStateBearingOrNull(FollowPuckViewportStateBearing.Constant(options.getDouble("bearing"))) + + ReadableType.String -> + when (options.getString("bearing")) { + "course" -> + FollowPuckViewportStateBearingOrNull(FollowPuckViewportStateBearing.SyncWithLocationPuck) + "heading" -> + FollowPuckViewportStateBearingOrNull(FollowPuckViewportStateBearing.SyncWithLocationPuck) + "keep" -> + FollowPuckViewportStateBearingOrNull(null) + else -> { + Logger.e( + LOG_TAG, + "bearing in viewport options should be either a constant number or syncWithLocationPuck" + ) + null + } + } + else -> { + Logger.e( + LOG_TAG, + "bearing in viewport options should be either constant number or course or heading or keep" + ) + null + } + }?.let { bearing -> + builder.bearing(bearing.state) + } + } + if (options.hasKey("padding")) { + if (options.isNull("padding")) { + builder.padding(null) + } else { + options.getAndLogIfNotMap("padding", LOG_TAG)?.let { paddingMap -> + paddingMap?.toPadding(LOG_TAG)?.let { padding -> + builder.padding(padding) + } + } + } + } + + } + return builder.build() + } + + + private fun toDefaultViewportTransitionOptions(state: ReadableMap?): DefaultViewportTransitionOptions { val builder = DefaultViewportTransitionOptions.Builder() if (state?.hasKey("maxDurationMs") == true) { val maxDurationMs = state.getAndLogIfNotDouble("maxDurationMs", LOG_TAG) @@ -139,7 +215,7 @@ mContext return builder.build() } - fun toTransition(viewport: ViewportPlugin, state: ReadableMap?): ViewportTransition? { + private fun toTransition(viewport: ViewportPlugin, state: ReadableMap?): ViewportTransition? { viewport.idle() return when (val kind = state?.getAndLogIfNotString("kind", LOG_TAG)) { "default" -> viewport.makeDefaultViewportTransition( @@ -256,4 +332,10 @@ mContext companion object { const val LOG_TAG = "RNMBXViewport" } -} \ No newline at end of file +} + +private fun ReadableMap.isKeep(s: String): Boolean { + return ((getType(s) == ReadableType.String) && (getString(s) == "keep")) +} + + diff --git a/android/src/main/java/com/rnmapbox/rnmbx/utils/extensions/ReadableMap.kt b/android/src/main/java/com/rnmapbox/rnmbx/utils/extensions/ReadableMap.kt index a0e3f8133..be71e9e01 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/utils/extensions/ReadableMap.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/utils/extensions/ReadableMap.kt @@ -5,6 +5,7 @@ import com.facebook.react.bridge.ReadableType import com.google.gson.JsonArray import com.google.gson.JsonElement import com.google.gson.JsonObject +import com.mapbox.maps.EdgeInsets import com.rnmapbox.rnmbx.utils.ConvertUtils import com.rnmapbox.rnmbx.utils.Logger @@ -44,6 +45,7 @@ fun ReadableMap.getAndLogIfNotBoolean(key: String, tag:String = "RNMBXReadableMa } } +/* If key is there it should be number or log otherwise */ fun ReadableMap.getAndLogIfNotDouble(key: String, tag: String = "RNMBXReadableMap"): Double? { return if (hasKey(key)) { if (getType(key) == ReadableType.Number) { @@ -57,6 +59,7 @@ fun ReadableMap.getAndLogIfNotDouble(key: String, tag: String = "RNMBXReadableMa } } +/* If key is there it should be string or log otherwise */ fun ReadableMap.getAndLogIfNotString(key: String, tag: String = "RNMBXReadableMap"): String? { return if (hasKey(key)) { if (getType(key) == ReadableType.String) { @@ -70,6 +73,19 @@ fun ReadableMap.getAndLogIfNotString(key: String, tag: String = "RNMBXReadableMa } } +fun ReadableMap.getAndLogIfNotMap(key: String, tag: String = "RNMBXReadableMap"): ReadableMap? { + return if (hasKey(key)) { + if (getType(key) == ReadableType.Map) { + getMap(key) + } else { + Logger.e("RNMBXReadableMap", "$key is exected to be a Map but was: ${getType(key)}") + null + } + } else { + null + } +} + fun ReadableMap.toJsonObject() : JsonObject { val result = JsonObject() val it = keySetIterator() @@ -86,3 +102,32 @@ fun ReadableMap.toJsonObject() : JsonObject { } return result } + +fun ReadableMap.toPadding(tag: String = "RNMBXReadableMap"): EdgeInsets? { + var top: Double = 0.0 + var bottom: Double = 0.0 + var left: Double = 0.0 + var right: Double = 0.0 + var empty = true + + getAndLogIfNotDouble("top", tag)?.let { + top = it + empty = false + } + getAndLogIfNotDouble("bottom", tag)?.let { + bottom = it + empty = false + } + getAndLogIfNotDouble("left", tag)?.let { + left = it + empty = false + } + getAndLogIfNotDouble("right", tag)?.let { + right = it + empty = false + } + if (empty) { + return null + } + return EdgeInsets(top, left, bottom, right) +} diff --git a/example/src/examples/Camera/Viewport.tsx b/example/src/examples/Camera/Viewport.tsx index 9b9aefbc8..c2788b93f 100644 --- a/example/src/examples/Camera/Viewport.tsx +++ b/example/src/examples/Camera/Viewport.tsx @@ -12,7 +12,10 @@ export default function ViewportExample() { title="followPuck" onPress={async () => { const completed = await viewport.current?.transitionTo( - { kind: 'followPuck' }, + { + kind: 'followPuck', + options: { zoom: 'keep', padding: { top: 200, left: 200 } }, + }, { kind: 'default', maxDurationMs: 5000 }, ); console.log(' => transitionTo completed:', completed); diff --git a/ios/RNMBX/RNMBXViewport.swift b/ios/RNMBX/RNMBXViewport.swift index 54a9804ff..b94e8047e 100644 --- a/ios/RNMBX/RNMBXViewport.swift +++ b/ios/RNMBX/RNMBXViewport.swift @@ -147,27 +147,77 @@ open class RNMBXViewport : UIView, RNMBXMapComponent, ViewportStatusObserver { mapView.viewport.idle() } - func toState(_ from: [String:String], _ viewport: ViewportManager?) -> ViewportState? { + func toState(_ viewport: ViewportManager?,_ state: [String:Any]) -> ViewportState? { guard let viewport = viewport else { Logger.log(level:.error, message: "no viewport") return nil } - guard let kind = from["kind"] else { + guard let kind = state["kind"] as? String else { Logger.log(level:.error, message: "no kind found in state") return nil } switch (kind) { case "followPuck": - return viewport.makeFollowPuckViewportState() -// case "overview": -// viewport.makeOverviewViewportState(options: ) + return viewport.makeFollowPuckViewportState(options: + parseFollowViewportOptions(state) + ) +// case "overview": +// viewport.makeOverviewViewportState(options: ) default: Logger.log(level:.error, message: "unexpected state kind: \(kind)") return nil } } + func parseFollowViewportOptions(_ state: [String:Any]) -> FollowPuckViewportStateOptions { + var result = FollowPuckViewportStateOptions() + if let options = state["options"] as? [String:Any] { + if let zoom = options["zoom"] as? String, (zoom == "keep") { + result.zoom = nil + } else if let zoom = options["zoom"] as? Double { + result.zoom = zoom + } else if options["zoom"] != nil { + Logger.log(level: .error, message: "parseFollowViewportOptions expected zoom to be number or 'keep', but was \(options["zoom"])") + } + + if let pitch = options["pitch"] as? String, (pitch == "pitch") { + result.pitch = nil + } else if let pitch = options["pitch"] as? Double { + result.pitch = pitch + } else if options["pitch"] != nil{ + Logger.log(level: .error, message: "parseFollowViewportOptions expected pitch to be number or 'keep', but was \(options["pitch"])") + } + + if let bearing = options["bearing"] as? String { + switch (bearing) { + case "keep": + result.bearing = nil + case "course": + result.bearing = .course + case "heading": + result.bearing = .heading + default: + Logger.log(level: .error, message: "bearing expected to be a number or 'keep' or 'course' or 'heading', but was \(options["bearing"])") + } + } else if let bearing = options["bearing"] as? NSNumber { + result.bearing = .constant(bearing.doubleValue) + } else if options["bearing"] != nil { + Logger.log(level: .error, message: "bearing expected to be a number or 'keep' or 'course' or 'heading', but was \(options["bearing"])") + } + + if let padding = options["padding"] as? String, (padding == "keep") { + result.padding = nil + } else if let padding = options["padding"] as? [String: NSNumber] { + result.padding = toPadding(padding) + } else if (options["padding"] != nil) { + Logger.log(level: .error, message: "padding expected to be an object or 'keep' or but was \(options["bearing"])") + } + } + + return result + } + func toTransition(_ from: [String: Any], _ viewport: ViewportManager?) -> ViewportTransition? { guard let viewport = viewport else { Logger.log(level:.error, message: "no viewport") @@ -197,7 +247,7 @@ open class RNMBXViewport : UIView, RNMBXMapComponent, ViewportStatusObserver { } func transitionTo( - state: [String: String], + state: [String: Any], transition: [String: Any], resolve: @escaping (NSNumber) -> Void ) { @@ -205,7 +255,7 @@ open class RNMBXViewport : UIView, RNMBXMapComponent, ViewportStatusObserver { Logger.log(level:.error, message: "mapView is null in RNMBXViewport.transitionTo") return } - guard let state = toState(state, mapView.viewport) else { + guard let state = toState(mapView.viewport, state) else { Logger.log(level:.error, message: "unable to parse toState in RNMBXViewport.transitionTo") return } @@ -218,3 +268,26 @@ open class RNMBXViewport : UIView, RNMBXMapComponent, ViewportStatusObserver { } } } + + +func toPadding(_ value: [String: NSNumber]) -> UIEdgeInsets { + var result = UIEdgeInsets() + + if let top = value["top"] as? NSNumber { + result.top = top.CGFloat + } + + if let bottom = value["bottom"] as? NSNumber { + result.bottom = bottom.CGFloat + } + + if let left = value["left"] as? NSNumber { + result.left = left.CGFloat + } + + if let right = value["right"] as? NSNumber { + result.right = right.CGFloat + } + + return result +} diff --git a/ios/RNMBX/RNMBXViewportManager.swift b/ios/RNMBX/RNMBXViewportManager.swift index 1f0a1c247..78119842d 100644 --- a/ios/RNMBX/RNMBXViewportManager.swift +++ b/ios/RNMBX/RNMBXViewportManager.swift @@ -26,7 +26,7 @@ public class RNMBXViewportManager : RCTViewManager { @objc public static func transitionTo( _ view: RNMBXViewport, - state: [String: String], + state: [String: Any], transition: [String: Any], resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock diff --git a/src/components/Viewport.tsx b/src/components/Viewport.tsx index a84722933..25fe0c8ef 100644 --- a/src/components/Viewport.tsx +++ b/src/components/Viewport.tsx @@ -20,9 +20,52 @@ import NativeViewport, { } from '../specs/RNMBXViewportNativeComponent'; import RNMBXViewportModule from '../specs/NativeRNMBXViewportModule'; +type FollowPuckOptions = { + /** + * The value to use for setting zoom. If 'keep', zoom will not be modified by the FollowPuckViewportState. + * @default DEFAULT_FOLLOW_PUCK_VIEWPORT_STATE_ZOOM. + */ + zoom?: number | 'keep'; + + /** + * The value to use for setting pitch. If 'keep', pitch will not be modified by the FollowPuckViewportState. + * @default DEFAULT_FOLLOW_PUCK_VIEWPORT_STATE_PITCH degrees. + */ + pitch?: number | 'keep'; + + /** + * Indicates how to obtain the value to use for bearing when setting the camera. + * If set to 'keep', bearing will not be modified by the FollowPuckViewportState. + * - heading: sets bearing to the heading of the device + * - course: sets bearing based on the direction of travel + * - number: sets the camera bearing to the constant value on every frame + * + * On Android, 'heading' and 'coruse' sets the camera bearing to the same as the location puck's bearing. See + * [syncWithLocationPuck](https://docs.mapbox.com/android/maps/api/11.0.0-rc.1/mapbox-maps-android/com.mapbox.maps.plugin.viewport.data/-follow-puck-viewport-state-bearing/-sync-with-location-puck/) + * + * @default 'heading' + */ + bearing?: 'heading' | 'course' | number | 'keep'; + + /** + * The value to use for setting CameraOptions.padding. If 'keep', padding will not be modified by the FollowPuckViewportState. + * + * @default 0 padding + */ + padding?: + | { + top?: number; + left?: number; + bottom?: number; + right?: number; + } + | 'keep'; +}; + type ViewportState = | { kind: 'followPuck'; + options?: FollowPuckOptions; } | { kind: 'overview'; diff --git a/src/specs/RNMBXViewportNativeComponent.ts b/src/specs/RNMBXViewportNativeComponent.ts index 36454f04a..e48767b45 100644 --- a/src/specs/RNMBXViewportNativeComponent.ts +++ b/src/specs/RNMBXViewportNativeComponent.ts @@ -7,9 +7,24 @@ import type { UnsafeMixed } from './codegenUtils'; // see https://github.com/rnmapbox/maps/wiki/FabricOptionalProp type OptionalProp = UnsafeMixed; -type ViewportState = +export type FollowPuckOptionsNative = { + zoom?: number | 'keep'; + pitch?: number | 'keep'; + bearing?: 'course' | 'heading' | number | 'keep'; + padding?: + | { + top?: number; + left?: number; + bottom?: number; + right?: number; + } + | 'keep'; +}; + +export type ViewportStateNative = | { kind: 'followPuck'; + options?: FollowPuckOptionsNative; } | { kind: 'overview'; @@ -21,7 +36,7 @@ type ViewportStatus = } | { kind: 'transition'; - toState: ViewportState; + toState: ViewportStateNative; transition: ViewportTransition; };