From eca2b71fb43175c6769dbf262ffa9f4b13702b2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Thu, 27 Jun 2024 11:29:05 +0300 Subject: [PATCH] chore(rnmbxcodegen): add codegen to generate rn boilerplate --- .../java/com/rnmapbox/rnmbx/RNMBXPackage.kt | 163 +++------- .../rnmbx/components/camera/RNMBXViewport.kt | 21 +- .../location/LocationProviderManager.kt | 73 +++++ .../components/location/RNMBXLocation.kt | 175 ++++++++++ .../rnmbx/components/mapview/RNMBXMapView.kt | 3 + .../com/rnmapbox/rnmbx/events/BaseEvent.kt | 24 ++ .../rnmbx/events/constants/EventKeys.kt | 7 +- .../com/rnmapbox/rnmbx/v11compat/Location.kt | 12 + .../com/rnmapbox/rnmbx/v11compat/Location.kt | 5 +- docs/docs.json | 25 +- example/package.json | 14 +- example/src/examples/BugReportExample.js | 1 + ios/RNMBX/RNMBXEvent.swift | 3 + ios/RNMBX/RNMBXImageSourceViewManager.m | 2 +- ios/RNMBX/RNMBXLocation.swift | 130 ++++++++ ios/RNMBX/RNMBXLocationComponentView.mm | 84 ----- .../RNMBXLocationComponentModule.h | 5 +- .../RNMBXLocationComponentModule.mm | 21 +- .../RNMBXLocationComponentView.h | 4 +- .../generated/RNMBXLocationComponentView.mm | 158 +++++++++ ios/RNMBX/generated/RNMBXLocationManager.m | 20 ++ .../generated/RNMBXLocationManager.swift | 14 + .../generated/RNMBXLocationProtocol.swift | 33 ++ package.json | 12 +- scripts/rnmbxcodegen/README.md | 13 + .../android/component-view-manager.kt.ejs | 56 ++++ .../component/android/component-view.kt.ejs | 18 ++ .../component/ios/_objc_to_cpp.ejs | 29 ++ .../ios/component-view-manager.m.ejs | 16 + .../ios/component-view-manager.swift | 14 + .../ios/component-view-protocol.swift | 27 ++ .../component/ios/component-view.h.ejs | 19 ++ .../component/ios/component-view.mm.ejs | 115 +++++++ .../component/ios/component.swift.ejs | 15 + .../componentmodule/ios/module.h.ejs | 21 ++ .../componentmodule/ios/module.mm.ejs | 73 +++++ scripts/rnmbxcodegen/rnmbxcodegen.ts | 301 ++++++++++++++++++ scripts/tsconfig.json | 2 +- src/Mapbox.ts | 1 + src/components/Location.tsx | 35 +- ... => NativeRNMBXLocationComponentModule.ts} | 8 +- src/specs/RNMBXLocationNativeComponent.ts | 23 +- 42 files changed, 1535 insertions(+), 260 deletions(-) create mode 100644 android/src/main/java/com/rnmapbox/rnmbx/components/location/LocationProviderManager.kt create mode 100644 android/src/main/java/com/rnmapbox/rnmbx/components/location/RNMBXLocation.kt create mode 100644 android/src/main/java/com/rnmapbox/rnmbx/events/BaseEvent.kt delete mode 100644 ios/RNMBX/RNMBXLocationComponentView.mm rename ios/RNMBX/{ => generated}/RNMBXLocationComponentModule.h (72%) rename ios/RNMBX/{ => generated}/RNMBXLocationComponentModule.mm (79%) rename ios/RNMBX/{ => generated}/RNMBXLocationComponentView.h (81%) create mode 100644 ios/RNMBX/generated/RNMBXLocationComponentView.mm create mode 100644 ios/RNMBX/generated/RNMBXLocationManager.m create mode 100644 ios/RNMBX/generated/RNMBXLocationManager.swift create mode 100644 ios/RNMBX/generated/RNMBXLocationProtocol.swift create mode 100644 scripts/rnmbxcodegen/README.md create mode 100644 scripts/rnmbxcodegen/component/android/component-view-manager.kt.ejs create mode 100644 scripts/rnmbxcodegen/component/android/component-view.kt.ejs create mode 100644 scripts/rnmbxcodegen/component/ios/_objc_to_cpp.ejs create mode 100644 scripts/rnmbxcodegen/component/ios/component-view-manager.m.ejs create mode 100644 scripts/rnmbxcodegen/component/ios/component-view-manager.swift create mode 100644 scripts/rnmbxcodegen/component/ios/component-view-protocol.swift create mode 100644 scripts/rnmbxcodegen/component/ios/component-view.h.ejs create mode 100644 scripts/rnmbxcodegen/component/ios/component-view.mm.ejs create mode 100644 scripts/rnmbxcodegen/component/ios/component.swift.ejs create mode 100644 scripts/rnmbxcodegen/componentmodule/ios/module.h.ejs create mode 100644 scripts/rnmbxcodegen/componentmodule/ios/module.mm.ejs create mode 100644 scripts/rnmbxcodegen/rnmbxcodegen.ts rename src/specs/{NativeRNMBXLocationModule.ts => NativeRNMBXLocationComponentModule.ts} (69%) diff --git a/android/src/main/java/com/rnmapbox/rnmbx/RNMBXPackage.kt b/android/src/main/java/com/rnmapbox/rnmbx/RNMBXPackage.kt index 7f481f45e..5d6049064 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/RNMBXPackage.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/RNMBXPackage.kt @@ -21,6 +21,7 @@ import com.rnmapbox.rnmbx.components.images.RNMBXImageManager import com.rnmapbox.rnmbx.components.images.RNMBXImageModule import com.rnmapbox.rnmbx.components.images.RNMBXImagesManager import com.rnmapbox.rnmbx.components.location.RNMBXCustomLocationProviderManager +import com.rnmapbox.rnmbx.components.location.RNMBXLocationManager import com.rnmapbox.rnmbx.components.location.RNMBXNativeUserLocationManager import com.rnmapbox.rnmbx.components.mapview.NativeMapViewModule import com.rnmapbox.rnmbx.components.mapview.RNMBXMapViewManager @@ -60,7 +61,7 @@ import com.rnmapbox.rnmbx.utils.ViewTagResolver class RNMBXPackage : TurboReactPackage() { var viewTagResolver: ViewTagResolver? = null - fun getViewTagResolver(context: ReactApplicationContext, module: String) : ViewTagResolver { + fun getViewTagResolver(context: ReactApplicationContext, @Suppress("UNUSED_PARAMETER") module: String) : ViewTagResolver { val viewTagResolver = viewTagResolver if (viewTagResolver == null) { val result = ViewTagResolver(context) @@ -71,7 +72,7 @@ class RNMBXPackage : TurboReactPackage() { } var shapeAnimators: ShapeAnimatorManager? = null - fun getShapeAnimators(module: String): ShapeAnimatorManager { + fun getShapeAnimators(@Suppress("UNUSED_PARAMETER") module: String): ShapeAnimatorManager { val shapeAnimators = shapeAnimators if (shapeAnimators == null) { val result = ShapeAnimatorManager() @@ -123,6 +124,7 @@ class RNMBXPackage : TurboReactPackage() { managers.add(RNMBXMapViewManager(reactApplicationContext, getViewTagResolver(reactApplicationContext, "RNMBXMapViewManager"))) managers.add(RNMBXStyleImportManager(reactApplicationContext)) managers.add(RNMBXModelsManager(reactApplicationContext)) + managers.add(RNMBXLocationManager(reactApplicationContext, getViewTagResolver(reactApplicationContext, "RNMBXLocationManager"))) // annotations managers.add(RNMBXMarkerViewManager(reactApplicationContext)) @@ -163,145 +165,64 @@ class RNMBXPackage : TurboReactPackage() { return managers } + fun reactModuleInfo( + name: String, + isTurboModule: Boolean = false + ): ReactModuleInfo { + return ReactModuleInfo( + /*name=*/name, + /*className=*/name, + /*canOverrideExistingModule=*/false, + /*needsEagerInit=*/false, + /*hasConstants=*/true, + /*isCxxModule=*/false, + /*isTurboModule=*/isTurboModule + ) + } + override fun getReactModuleInfoProvider(): ReactModuleInfoProvider { resetViewTagResolver() return ReactModuleInfoProvider { val moduleInfos: MutableMap = HashMap() val isTurboModule = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED - moduleInfos[RNMBXModule.REACT_CLASS] = ReactModuleInfo( - RNMBXModule.REACT_CLASS, - RNMBXModule.REACT_CLASS, - false, // canOverrideExistingModule - false, // needsEagerInit - true, // hasConstants - false, // isCxxModule - false // isTurboModule - ) - moduleInfos[RNMBXLocationModule.REACT_CLASS] = ReactModuleInfo( - RNMBXLocationModule.REACT_CLASS, - RNMBXLocationModule.REACT_CLASS, - false, // canOverrideExistingModule - false, // needsEagerInit - true, // hasConstants - false, // isCxxModule - false // isTurboModule - ) - moduleInfos[RNMBXOfflineModule.REACT_CLASS] = ReactModuleInfo( - RNMBXOfflineModule.REACT_CLASS, - RNMBXOfflineModule.REACT_CLASS, - false, // canOverrideExistingModule - false, // needsEagerInit - true, // hasConstants - false, // isCxxModule - false // isTurboModule - ) - moduleInfos[RNMBXTileStoreModule.REACT_CLASS] = ReactModuleInfo( - RNMBXTileStoreModule.REACT_CLASS, - RNMBXTileStoreModule.REACT_CLASS, - false, // canOverrideExistingModule - false, // needsEagerInit - true, // hasConstants - false, // isCxxModule - false // isTurboModule - ) - moduleInfos[RNMBXOfflineModuleLegacy.REACT_CLASS] = ReactModuleInfo( - RNMBXOfflineModuleLegacy.REACT_CLASS, - RNMBXOfflineModuleLegacy.REACT_CLASS, - false, // canOverrideExistingModule - false, // needsEagerInit - true, // hasConstants - false, // isCxxModule - false // isTurboModule - ) - moduleInfos[RNMBXSnapshotModule.REACT_CLASS] = ReactModuleInfo( - RNMBXSnapshotModule.REACT_CLASS, - RNMBXSnapshotModule.REACT_CLASS, - false, // canOverrideExistingModule - false, // needsEagerInit - true, // hasConstants - false, // isCxxModule - false // isTurboModule - ) - moduleInfos[RNMBXLogging.REACT_CLASS] = ReactModuleInfo( - RNMBXLogging.REACT_CLASS, - RNMBXLogging.REACT_CLASS, - false, // canOverrideExistingModule - false, // needsEagerInit - true, // hasConstants - false, // isCxxModule - false // isTurboModule - ) - moduleInfos[NativeMapViewModule.NAME] = ReactModuleInfo( + moduleInfos[RNMBXModule.REACT_CLASS] = reactModuleInfo(RNMBXModule.REACT_CLASS) + moduleInfos[RNMBXLocationModule.REACT_CLASS] = reactModuleInfo(RNMBXLocationModule.REACT_CLASS) + moduleInfos[RNMBXOfflineModule.REACT_CLASS] = reactModuleInfo(RNMBXOfflineModule.REACT_CLASS) + moduleInfos[RNMBXTileStoreModule.REACT_CLASS] = reactModuleInfo(RNMBXTileStoreModule.REACT_CLASS) + moduleInfos[RNMBXOfflineModuleLegacy.REACT_CLASS] = reactModuleInfo(RNMBXOfflineModuleLegacy.REACT_CLASS) + moduleInfos[RNMBXSnapshotModule.REACT_CLASS] = reactModuleInfo(RNMBXSnapshotModule.REACT_CLASS) + moduleInfos[RNMBXLogging.REACT_CLASS] = reactModuleInfo(RNMBXLogging.REACT_CLASS) + moduleInfos[NativeMapViewModule.NAME] = reactModuleInfo( NativeMapViewModule.NAME, - NativeMapViewModule.NAME, - false, // canOverrideExistingModule - false, // needsEagerInit - false, // hasConstants - false, // isCxxModule - isTurboModule // isTurboModule + isTurboModule=isTurboModule ) - moduleInfos[RNMBXViewportModule.NAME] = ReactModuleInfo( - RNMBXViewportModule.NAME, + moduleInfos[RNMBXViewportModule.NAME] = reactModuleInfo( RNMBXViewportModule.NAME, - false, // canOverrideExistingModule - false, // needsEagerInit - false, // hasConstants - false, // isCxxModule - isTurboModule // isTurboModule + isTurboModule= isTurboModule ) - moduleInfos[RNMBXCameraModule.NAME] = ReactModuleInfo( + moduleInfos[RNMBXCameraModule.NAME] = reactModuleInfo( RNMBXCameraModule.NAME, - RNMBXCameraModule.NAME, - false, // canOverrideExistingModule - false, // needsEagerInit - false, // hasConstants - false, // isCxxModule - isTurboModule // isTurboModule + isTurboModule= isTurboModule ) - moduleInfos[RNMBXShapeSourceModule.NAME] = ReactModuleInfo( - RNMBXShapeSourceModule.NAME, + moduleInfos[RNMBXShapeSourceModule.NAME] = reactModuleInfo( RNMBXShapeSourceModule.NAME, - false, // canOverrideExistingModule - false, // needsEagerInit - false, // hasConstants - false, // isCxxModule - isTurboModule // isTurboModule + isTurboModule= isTurboModule ) - moduleInfos[RNMBXImageModule.NAME] = ReactModuleInfo( + moduleInfos[RNMBXImageModule.NAME] = reactModuleInfo( RNMBXImageModule.NAME, - RNMBXImageModule.NAME, - false, // canOverrideExistingModule - false, // needsEagerInit - false, // hasConstants - false, // isCxxModule - isTurboModule // isTurboModule + isTurboModule= isTurboModule ) - moduleInfos[RNMBXPointAnnotationModule.NAME] = ReactModuleInfo( - RNMBXPointAnnotationModule.NAME, + moduleInfos[RNMBXPointAnnotationModule.NAME] = reactModuleInfo( RNMBXPointAnnotationModule.NAME, - false, // canOverrideExistingModule - false, // needsEagerInit - false, // hasConstants - false, // isCxxModule - isTurboModule // isTurboModule + isTurboModule= isTurboModule ) - moduleInfos[RNMBXMovePointShapeAnimatorModule.NAME] = ReactModuleInfo( + moduleInfos[RNMBXMovePointShapeAnimatorModule.NAME] = reactModuleInfo( RNMBXMovePointShapeAnimatorModule.NAME, - RNMBXMovePointShapeAnimatorModule.NAME, - false, - false, - false, - false, - isTurboModule + isTurboModule= isTurboModule ) - moduleInfos[RNMBXChangeLineOffsetsShapeAnimatorModule.NAME] = ReactModuleInfo( - RNMBXChangeLineOffsetsShapeAnimatorModule.NAME, + moduleInfos[RNMBXChangeLineOffsetsShapeAnimatorModule.NAME] = reactModuleInfo( RNMBXChangeLineOffsetsShapeAnimatorModule.NAME, - false, - false, - false, - false, - isTurboModule + isTurboModule= isTurboModule ) moduleInfos } 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 fa058a85b..19e3ff21c 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 @@ -37,28 +37,9 @@ import com.mapbox.maps.EdgeInsets import com.mapbox.maps.plugin.viewport.data.FollowPuckViewportStateBearing import com.mapbox.maps.plugin.viewport.data.FollowPuckViewportStateOptions import com.mapbox.maps.plugin.viewport.data.OverviewViewportStateOptions +import com.rnmapbox.rnmbx.events.BaseEvent import com.rnmapbox.rnmbx.events.constants.EventKeys -class BaseEvent( - private val surfaceId: Int, - private val viewTag: Int, - private val eventName: String, - private val eventData: WritableMap, - private val canCoalesce: Boolean = false -): Event(surfaceId, viewTag) { - override fun getEventName(): String { - return eventName - } - - override fun canCoalesce(): Boolean { - return canCoalesce - } - - override fun getEventData(): WritableMap? { - return eventData - } -} - class RNMBXViewport(private val mContext: Context, private val mManager: RNMBXViewportManager) : AbstractMapFeature( mContext diff --git a/android/src/main/java/com/rnmapbox/rnmbx/components/location/LocationProviderManager.kt b/android/src/main/java/com/rnmapbox/rnmbx/components/location/LocationProviderManager.kt new file mode 100644 index 000000000..2c8b0233a --- /dev/null +++ b/android/src/main/java/com/rnmapbox/rnmbx/components/location/LocationProviderManager.kt @@ -0,0 +1,73 @@ +package com.rnmapbox.rnmbx.components.location + +import com.rnmapbox.rnmbx.v11compat.Cancelable +import com.mapbox.maps.MapView +import com.mapbox.maps.plugin.locationcomponent.DefaultLocationProvider +import com.mapbox.maps.plugin.locationcomponent.LocationProvider +import com.mapbox.maps.plugin.locationcomponent.location +import com.rnmapbox.rnmbx.components.mapview.RNMBXMapView +import java.lang.ref.WeakReference + +typealias CallbackType = (old: LocationProvider, new: LocationProvider) -> Unit; + + +class CallbackList { + class Entry(val callback: T, val list: WeakReference>): Cancelable { + + override fun cancel() { + list.get()?.remove(this) + } + } + private val callbacks = mutableListOf>() + + fun add(callback: T): Cancelable { + val result = Entry(callback, WeakReference(this)); + callbacks.add(result) + return result; + } + + internal fun remove(entry: Entry) { + callbacks.remove(entry) + } + + fun forEach(callback: (it: T) -> Unit) { + callbacks.map { it.callback }.forEach(callback) + } +} + + +class LocationProviderManager(val mapView: RNMBXMapView) { + var callbacks = CallbackList() + + fun getLocationProvider(mapView: MapView): LocationProvider { + val location = mapView.location + val result = location.getLocationProvider() + + if (result != null) { + return result + } else { + val result = DefaultLocationProvider(mapView.context) + location.setLocationProvider(result) + return result + } + } + + fun update(provider: LocationProvider) { + mapView.withMapView { mapView: MapView -> + val location = mapView.location + val oldProvider = location.getLocationProvider() + location.setLocationProvider(provider) + + if (oldProvider != null) { + apply(oldProvider, provider) + } + } + } + + fun onChange(callback: CallbackType): Cancelable = + callbacks.add(callback) + + fun apply(old: LocationProvider, new: LocationProvider) { + callbacks.forEach { it(old, new) } + } +} \ No newline at end of file diff --git a/android/src/main/java/com/rnmapbox/rnmbx/components/location/RNMBXLocation.kt b/android/src/main/java/com/rnmapbox/rnmbx/components/location/RNMBXLocation.kt new file mode 100644 index 000000000..5f7560679 --- /dev/null +++ b/android/src/main/java/com/rnmapbox/rnmbx/components/location/RNMBXLocation.kt @@ -0,0 +1,175 @@ +/*** +to: android/src/main/java/com/rnmapbox/rnmbx/components/location/RNMBXLocation.kt +***/ +package com.rnmapbox.rnmbx.components.location + +import android.animation.ValueAnimator +import android.content.Context +import android.util.Log +import com.facebook.react.bridge.ReactContext +import com.facebook.react.bridge.WritableMap +import com.facebook.react.uimanager.UIManagerHelper +import com.facebook.react.uimanager.events.Event +import com.facebook.react.views.view.ReactViewGroup +import com.mapbox.common.location.LocationError +import com.mapbox.geojson.Point +import com.mapbox.maps.MapView +import com.mapbox.maps.plugin.locationcomponent.DefaultLocationProvider +import com.mapbox.maps.plugin.locationcomponent.LocationComponentPlugin +import com.mapbox.maps.plugin.locationcomponent.location +import com.rnmapbox.rnmbx.v11compat.location.LocationConsumer +import com.rnmapbox.rnmbx.components.AbstractMapFeature +import com.rnmapbox.rnmbx.events.BaseEvent +import com.rnmapbox.rnmbx.components.mapview.RNMBXMapView +import com.rnmapbox.rnmbx.events.constants.EventKeys +import com.rnmapbox.rnmbx.utils.PropertyChanges +import com.rnmapbox.rnmbx.utils.PropertyUpdaterWithName +import com.rnmapbox.rnmbx.utils.writableArrayOf +import com.rnmapbox.rnmbx.utils.writableMapOf + +class RNMBXLocation(private val context: Context, private val manager: RNMBXLocationManager) : +AbstractMapFeature( + context +) { + enum class Property(val _apply: (RNMBXLocation) -> Unit) : + PropertyUpdaterWithName { + ON_LOCATION_OR_BEARING_CHANGE(RNMBXLocation::applyLocationConsumer),; + + override fun apply(location: RNMBXLocation) { + _apply(location) + } + } + + val changes = PropertyChanges(); + + private var locationConsumer: LocationConsumer? = null + + var hasOnBearingChange: Boolean = false + set(value) { + field = value + changes.add(Property.ON_LOCATION_OR_BEARING_CHANGE) + } + + var hasOnLocationChange: Boolean = false + set(value) { + field = value + changes.add(Property.ON_LOCATION_OR_BEARING_CHANGE) + } + + override fun addToMap(mapView: RNMBXMapView) { + super.addToMap(mapView) + changes.apply(this) + } + + private fun withLocation(callback: (mapView: MapView, location: LocationComponentPlugin, locationProviderManager: LocationProviderManager) -> Unit) { + withMapView { mapView -> + callback(mapView.mapView, mapView.mapView.location, mapView.locationProviderManager) + } + } + + private fun createLocationConsumer() = object: LocationConsumer { + override fun onBearingUpdated( + vararg bearing: Double, + options: (ValueAnimator.() -> Unit)? + ) { + if (hasOnBearingChange) { + manager.dispatchEvent( + BaseEvent( + UIManagerHelper.getSurfaceId(context), + id, + EventKeys.LOCATION_ON_BEARING_CHANGE.value, + writableMapOf( + "bearing" to writableArrayOf( bearing.map { it }) + ) + ) + ) + Log.e("RNMBXLocation", "onBearingUpdated:${bearing} ${options}") + } + } + + override fun onError(error: LocationError) { + Log.e("RNMBXLocation", "onError:${error}") + } + + override fun onHorizontalAccuracyRadiusUpdated( + vararg radius: Double, + options: (ValueAnimator.() -> Unit)? + ) { + Log.e("RNMBXLocation", "onHorizontalAccuracyRadiusUpdated:${radius} ${options}") + } + + override fun onLocationUpdated( + vararg location: Point, + options: (ValueAnimator.() -> Unit)? + ) { + + if (hasOnLocationChange) { + manager.dispatchEvent( + BaseEvent( + UIManagerHelper.getSurfaceId(context), + id, + EventKeys.LOCATION_ON_LOCATION_CHANGE.value, + writableMapOf( + "locations" to writableArrayOf( + *location.map { writableMapOf( + "longitude" to it.longitude(), + "latitude" to it.latitude(), + *(if(it.hasAltitude()) arrayOf("altitude" to it.altitude()) else arrayOf()) + ) }.toTypedArray() + ) + ) + ) + ) + } + } + + override fun onPuckAccuracyRadiusAnimatorDefaultOptionsUpdated(options: ValueAnimator.() -> Unit) { + Log.e("RNMBXLocation", "onPuckAccuracyRadiusAnimatorDefaultOptionsUpdated:${options}") + } + + override fun onPuckBearingAnimatorDefaultOptionsUpdated(options: ValueAnimator.() -> Unit) { + Log.e("RNMBXLocation", "onPuckBearingAnimatorDefaultOptionsUpdated:${options}") + + } + + override fun onPuckLocationAnimatorDefaultOptionsUpdated(options: ValueAnimator.() -> Unit) { + Log.e("RNMBXLocation", "onPuckLocationAnimatorDefaultOptionsUpdated:${options}") + } + } + + private fun subscribeForLocationUpdates() { + withLocation { mapView, _, locationProviderManager -> + if (this.locationConsumer != null) { + return@withLocation + } + + val locationConsumer = createLocationConsumer(); + locationProviderManager.getLocationProvider(mapView)?.registerLocationConsumer(locationConsumer) + this.locationConsumer = locationConsumer + + locationProviderManager.onChange { old, new -> + val locationConsumer = this.locationConsumer + if (locationConsumer != null) { + old.unRegisterLocationConsumer(locationConsumer) + new.registerLocationConsumer(locationConsumer) + } + } + } + } + + private fun applyLocationConsumer() { + withLocation { mapView, location, locationProviderManager -> + if (hasOnLocationChange || hasOnBearingChange) { + val locationConsumer = createLocationConsumer() + locationProviderManager.getLocationProvider(mapView).registerLocationConsumer(locationConsumer) + this.locationConsumer = locationConsumer + } else { + val locationConsumer = this.locationConsumer + if (locationConsumer != null) { + this.locationConsumer = null; + locationProviderManager.getLocationProvider(mapView).unRegisterLocationConsumer(locationConsumer) + } + } + } + } +} \ No newline at end of file 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 95398fb6c..7ff2fdfc4 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 @@ -68,6 +68,7 @@ import java.util.* import com.rnmapbox.rnmbx.components.annotation.RNMBXPointAnnotationCoordinator import com.rnmapbox.rnmbx.components.images.ImageManager +import com.rnmapbox.rnmbx.components.location.LocationProviderManager import com.rnmapbox.rnmbx.v11compat.event.* import com.rnmapbox.rnmbx.v11compat.feature.* @@ -259,6 +260,8 @@ open class RNMBXMapView(private val mContext: Context, var mManager: RNMBXMapVie val mapView: MapView get() = this.mMapView + val locationProviderManager by lazy { LocationProviderManager(this) } + val viewAnnotationManager: ViewAnnotationManager get() = mapView.viewAnnotationManager diff --git a/android/src/main/java/com/rnmapbox/rnmbx/events/BaseEvent.kt b/android/src/main/java/com/rnmapbox/rnmbx/events/BaseEvent.kt new file mode 100644 index 000000000..8161ab8bd --- /dev/null +++ b/android/src/main/java/com/rnmapbox/rnmbx/events/BaseEvent.kt @@ -0,0 +1,24 @@ +package com.rnmapbox.rnmbx.events + +import com.facebook.react.bridge.WritableMap +import com.facebook.react.uimanager.events.Event + +class BaseEvent( + private val surfaceId: Int, + private val viewTag: Int, + private val eventName: String, + private val eventData: WritableMap, + private val canCoalesce: Boolean = false +): Event(surfaceId, viewTag) { + override fun getEventName(): String { + return eventName + } + + override fun canCoalesce(): Boolean { + return canCoalesce + } + + override fun getEventData(): WritableMap? { + return eventData + } +} \ No newline at end of file diff --git a/android/src/main/java/com/rnmapbox/rnmbx/events/constants/EventKeys.kt b/android/src/main/java/com/rnmapbox/rnmbx/events/constants/EventKeys.kt index d059b2cbc..0b44aea0e 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/events/constants/EventKeys.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/events/constants/EventKeys.kt @@ -32,7 +32,12 @@ enum class EventKeys(val value: String) { USER_LOCATION_UPDATE(ns("user.location.update")), // viewport events - VIEWPORT_STATUS_CHANGE(ns("viewport.statuschange")) + VIEWPORT_STATUS_CHANGE(ns("viewport.statuschange")), + + // location events + LOCATION_ON_BEARING_CHANGE(ns("location.onbearingchange")), + + LOCATION_ON_LOCATION_CHANGE(ns("location.onlocationchange")), } fun eventMapOf(vararg values: Pair): Map { diff --git a/android/src/main/mapbox-v11-compat/v10/com/rnmapbox/rnmbx/v11compat/Location.kt b/android/src/main/mapbox-v11-compat/v10/com/rnmapbox/rnmbx/v11compat/Location.kt index ad24cab5a..354d2f679 100644 --- a/android/src/main/mapbox-v11-compat/v10/com/rnmapbox/rnmbx/v11compat/Location.kt +++ b/android/src/main/mapbox-v11-compat/v10/com/rnmapbox/rnmbx/v11compat/Location.kt @@ -2,6 +2,7 @@ package com.rnmapbox.rnmbx.v11compat.location; import android.Manifest.permission.ACCESS_COARSE_LOCATION import android.Manifest.permission.ACCESS_FINE_LOCATION +import android.animation.ValueAnimator import android.content.Context import android.os.Looper import androidx.annotation.RequiresPermission @@ -14,6 +15,8 @@ import com.mapbox.android.core.location.LocationEngine as _LocationEngine import com.mapbox.android.core.location.LocationEngineCallback as _LocationEngineCallback import com.mapbox.android.core.location.LocationEngineRequest import com.mapbox.android.core.location.LocationEngineProvider +import com.mapbox.common.location.LocationError +import com.mapbox.maps.plugin.locationcomponent.LocationConsumer as _LocationConsumer import android.location.Location as _Location @@ -59,4 +62,13 @@ val Location.timestamp: Long fun createLocationEngine(context: Context): LocationEngine { return LocationEngineProvider.getBestLocationEngine(context) +} + +interface LocationConsumer : _LocationConsumer { + fun onError(error: LocationError); + fun onHorizontalAccuracyRadiusUpdated( + vararg radius: Double, + options: (ValueAnimator.() -> Unit)? + ); + fun onPuckAccuracyRadiusAnimatorDefaultOptionsUpdated(options: ValueAnimator.() -> Unit); } \ No newline at end of file diff --git a/android/src/main/mapbox-v11-compat/v11/com/rnmapbox/rnmbx/v11compat/Location.kt b/android/src/main/mapbox-v11-compat/v11/com/rnmapbox/rnmbx/v11compat/Location.kt index 8cda49e73..131ae447f 100644 --- a/android/src/main/mapbox-v11-compat/v11/com/rnmapbox/rnmbx/v11compat/Location.kt +++ b/android/src/main/mapbox-v11-compat/v11/com/rnmapbox/rnmbx/v11compat/Location.kt @@ -18,6 +18,7 @@ import com.mapbox.common.location.LocationService import com.mapbox.common.location.LocationServiceFactory import com.rnmapbox.rnmbx.utils.Logger import kotlin.math.absoluteValue +import com.mapbox.maps.plugin.locationcomponent.LocationConsumer as _LocationConsumer typealias PuckBearing = _PuckBearing @@ -113,4 +114,6 @@ fun createLocationEngine(context: Context): LocationEngine? { return LocationEngine(locationProvider, request) } return null -} \ No newline at end of file +} + +typealias LocationConsumer = _LocationConsumer; diff --git a/docs/docs.json b/docs/docs.json index 1aa98237c..ec3842965 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -3216,8 +3216,31 @@ "description": "", "displayName": "Location", "methods": [], + "props": [ + { + "name": "onHeadingChange", + "required": true, + "type": { + "name": "func", + "funcSignature": "(event:OnHeadingChangeEventType['payload']) => void" + }, + "default": "none", + "description": "FIX ME NO DESCRIPTION\n*signature:*`(event:OnHeadingChangeEventType['payload']) => void`" + }, + { + "name": "onLocationChange", + "required": true, + "type": { + "name": "func", + "funcSignature": "(event:OnLocationChangeEventType['payload']) => void" + }, + "default": "none", + "description": "FIX ME NO DESCRIPTION\n*signature:*`(event:OnLocationChangeEventType['payload']) => void`" + } + ], "fileNameWithExt": "Location.tsx", - "relPath": "src/components/Location.tsx" + "relPath": "src/components/Location.tsx", + "name": "Location" }, "LocationPuck": { "description": "Renders a puck on the map that shows the device's current location.", diff --git a/example/package.json b/example/package.json index e41dd767e..3ef1a397d 100644 --- a/example/package.json +++ b/example/package.json @@ -24,6 +24,7 @@ "@react-navigation/native": "^6.0.17", "@react-navigation/native-stack": "^6.9.26", "@rneui/base": "^4.0.0-rc.6", + "@rnmapbox/maps": "link:..", "@turf/along": "^6.5.0", "@turf/bbox": "^6.5.0", "@turf/bbox-polygon": "^6.5.0", @@ -40,8 +41,7 @@ "react-native": "0.74.2", "react-native-safe-area-context": "4.10.4", "react-native-screens": "3.31.1", - "react-native-vector-icons": "9.2.0", - "@rnmapbox/maps": "link:../" + "react-native-vector-icons": "9.2.0" }, "optionalDependencies": { "@expo/metro-runtime": "3.2.1", @@ -63,15 +63,15 @@ "@types/react": "^18.2.45", "@types/react-test-renderer": "^18.0.0", "babel-jest": "^29.6.3", + "babel-plugin-module-resolver": "^5.0.0", + "detox": "^20.12.1", "eslint": "^8.19.0", + "glob-to-regexp": "^0.4.0", "jest": "^29.6.3", + "pod-install": "^0.1.0", "prettier": "^2.8.8", "react-test-renderer": "18.2.0", - "typescript": "^5.3.3", - "detox": "^20.12.1", - "glob-to-regexp": "^0.4.0", - "babel-plugin-module-resolver": "^5.0.0", - "pod-install": "^0.1.0" + "typescript": "^5.3.3" }, "engines": { "node": ">=18" diff --git a/example/src/examples/BugReportExample.js b/example/src/examples/BugReportExample.js index da35154e9..414766243 100644 --- a/example/src/examples/BugReportExample.js +++ b/example/src/examples/BugReportExample.js @@ -9,6 +9,7 @@ import { Camera, VectorSource, LineLayer, + Location, } from '@rnmapbox/maps'; const styles = { diff --git a/ios/RNMBX/RNMBXEvent.swift b/ios/RNMBX/RNMBXEvent.swift index 4306adae4..d4b99729f 100644 --- a/ios/RNMBX/RNMBXEvent.swift +++ b/ios/RNMBX/RNMBXEvent.swift @@ -40,6 +40,9 @@ class RNMBXEvent : NSObject, RNMBXEventProtocol { case shapeSourceLayerPress case annotationSelected = "annotationselected" case annotationDeselected = "annotationdeselected" + + case headingChange + case locationChange } init(type: EventType, payload: [String:Any?]?) { diff --git a/ios/RNMBX/RNMBXImageSourceViewManager.m b/ios/RNMBX/RNMBXImageSourceViewManager.m index 5d4bc4a2b..39ae91152 100644 --- a/ios/RNMBX/RNMBXImageSourceViewManager.m +++ b/ios/RNMBX/RNMBXImageSourceViewManager.m @@ -1,7 +1,7 @@ #import #import -@interface RCT_EXTERN_REMAP_MODULE(RNMBXImageSource, RNMBXImageSourceViewManager, RCTViewManager) +@interface RCT_EXTERN_REMAP_MODULE(<%= Name %>, RNMBXImageSourceViewManager, RCTViewManager) RCT_EXPORT_VIEW_PROPERTY(id, NSString) RCT_EXPORT_VIEW_PROPERTY(existing, BOOL) diff --git a/ios/RNMBX/RNMBXLocation.swift b/ios/RNMBX/RNMBXLocation.swift index a6d9b76e8..0df94a97c 100644 --- a/ios/RNMBX/RNMBXLocation.swift +++ b/ios/RNMBX/RNMBXLocation.swift @@ -1,3 +1,133 @@ +/*** +to: ios/rnmbx/RNMBXLocation.swift +userEditable: true +***/ + +import MapboxMaps + @objc(RNMBXLocation) open class RNMBXLocation : RNMBXMapComponentBase { + let changes : PropertyChanges = PropertyChanges() + + var hasOnHeadingChange: Bool = false { + didSet { changed(.onHeadingChange) } + } + var onHeadingChange: RCTBubblingEventBlock? + var onHeadingChangeSubscription: AnyCancelable? + + var hasOnLocationChange: Bool = false { + didSet { changed(.onLocationChange) } + } + var onLocationChange: RCTBubblingEventBlock? + var onLocationChangeSubscription: AnyCancelable? + + + enum Property : String { + case onHeadingChange + case onLocationChange + + func apply(_ location: RNMBXLocation) -> Void { + switch self { + case .onHeadingChange: + location.applyOnHeadingChange() + case .onLocationChange: + location.applyOnLocationChange() + } + } + } + + func changed(_ property: Property) { + changes.add(name: property.rawValue, update: property.apply) + } + + func applyOnLocationChange() { + self.onLocationChangeSubscription?.cancel() + self.onLocationChangeSubscription = nil + + withLocation { location in + if let locationChange = self.onLocationChange { + self.onLocationChangeSubscription = location.onLocationChange.observe { locations in + let event = RNMBXEvent( + type: .locationChange, + payload: [ + "locations": locations.map { + [ + "altitude": $0.altitude, + "longitude": $0.coordinate.longitude, + "latitude": $0.coordinate.latitude, + "horizontalAccuracy": $0.horizontalAccuracy, + "verticalAccuracy": $0.verticalAccuracy, + "source": $0.source, + "bearing": $0.bearing, + "bearingAccuracy": $0.bearingAccuracy, + "floor": $0.floor, + "speed": $0.speed, + "speedAccuracy": $0.speedAccuracy, + "extra": $0.extra, + "timestamp": $0.timestamp.timeIntervalSince1970 + ] + } + ] + ) + self.fireEvent(event: event, callback: locationChange) + } + } + } + } + + func applyOnHeadingChange() { + self.onHeadingChangeSubscription?.cancel() + self.onHeadingChangeSubscription = nil + + withLocation { location in + if let headingChange = self.onHeadingChange { + self.onHeadingChangeSubscription = location.onHeadingChange.observe { heading in + let event = RNMBXEvent( + type: .headingChange, + payload: [ + "direction": heading.direction, + "accuracy": heading.accuracy, + "timestamp": heading.timestamp.timeIntervalSince1970 + ] + ) + self.fireEvent(event: event, callback: headingChange) + } + } + } + } + + private func fireEvent(event: RNMBXEvent, callback: @escaping RCTBubblingEventBlock) { + callback(event.toJSON()) + } + + func withLocation(_ callback: @escaping (_ location: LocationManager) -> Void) { + withMapView { mapView in callback(mapView.location) } + } + + override public func addToMap(_ map: RNMBXMapView, style: Style) { + super.addToMap(map, style: style) + changes.apply(self) + } +} + +@objc extension RNMBXLocation: RNMBXLocationProtocol { + public func setHasOnHeadingChange(_ value: Bool) { + self.hasOnHeadingChange = value + } + + public func setHasOnLocationChange(_ value: Bool) { + self.hasOnLocationChange = value + } + + public func setOnLocationChange(_ callback: RCTBubblingEventBlock?) { + self.onLocationChange = callback + } + + public func setOnHeadingChange(_ callback: RCTBubblingEventBlock?) { + self.onHeadingChange = callback + } + + @objc public static func someMethod(_ view: RNMBXLocation, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + // TODO implement + } } diff --git a/ios/RNMBX/RNMBXLocationComponentView.mm b/ios/RNMBX/RNMBXLocationComponentView.mm deleted file mode 100644 index 4c3338f54..000000000 --- a/ios/RNMBX/RNMBXLocationComponentView.mm +++ /dev/null @@ -1,84 +0,0 @@ -#ifdef RCT_NEW_ARCH_ENABLED - -#import "RNMBXLocationComponentView.h" - -#import -#import - -#import -#import -#import -#import - -#import "rnmapbox_maps-Swift.pre.h" - -#import "RCTFollyConvert.h" - -using namespace facebook::react; - - -@implementation RNMBXLocationComponentView { - RNMBXLocation *_view; -} - -- (instancetype)initWithFrame:(CGRect)frame -{ - if (self = [super initWithFrame:frame]) { - static const auto defaultProps = std::make_shared(); - _props = defaultProps; - _view = [[RNMBXLocation alloc] init]; - [self prepareView]; - - self.contentView = _view; - } - - return self; -} - -- (void)prepareView -{ - __weak __typeof__(self) weakSelf = self; - - [_view setOnStatusChanged:^(NSDictionary* event) { - __typeof__(self) strongSelf = weakSelf; - - if (strongSelf != nullptr && strongSelf->_eventEmitter != nullptr) { - auto type = std::string([[event objectForKey:@"type"] UTF8String]); - auto payload = convertIdToFollyDynamic([event objectForKey:@"payload"]); - RNMBXLocationEventEmitter::OnStatusChanged event = {type, payload}; - strongSelf->_eventEmitter->onStatusChanged(event); - } - }]; -} - - -#pragma mark - RCTComponentViewProtocol - -+ (ComponentDescriptorProvider)componentDescriptorProvider -{ - return concreteComponentDescriptorProvider(); -} - -- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps -{ - const auto &oldViewProps = static_cast(*oldProps); - const auto &newViewProps = static_cast(*props); - - if (!oldProps.get() || oldViewProps.transitionsToIdleUponUserInteraction != newViewProps.transitionsToIdleUponUserInteraction) { - _view.transitionsToIdleUponUserInteraction = convertDynamicToOptional_boolean(newViewProps.transitionsToIdleUponUserInteraction, @"transitionsToIdleUponUserInteraction"); - } - if (!oldProps.get() || - oldViewProps.hasStatusChanged != - newViewProps.hasStatusChanged) { - _view.hasStatusChanged = newViewProps.hasStatusChanged; - } - [super updateProps:props oldProps:oldProps]; -} -@end - -Class RNMBXViewportCls(void) -{ - return RNMBXLocationComponentView.class; -} - -#endif // RCT_NEW_ARCH_ENABLED diff --git a/ios/RNMBX/RNMBXLocationComponentModule.h b/ios/RNMBX/generated/RNMBXLocationComponentModule.h similarity index 72% rename from ios/RNMBX/RNMBXLocationComponentModule.h rename to ios/RNMBX/generated/RNMBXLocationComponentModule.h index 22ef7c9fd..a1202c03d 100644 --- a/ios/RNMBX/RNMBXLocationComponentModule.h +++ b/ios/RNMBX/generated/RNMBXLocationComponentModule.h @@ -1,3 +1,6 @@ +/*** +to: ios/RNMBX/generated/RNMBXLocationComponentModule.h +***/ #import #import @@ -9,7 +12,7 @@ @interface RNMBXLocationComponentModule : NSObject #ifdef RCT_NEW_ARCH_ENABLED - + #else #endif diff --git a/ios/RNMBX/RNMBXLocationComponentModule.mm b/ios/RNMBX/generated/RNMBXLocationComponentModule.mm similarity index 79% rename from ios/RNMBX/RNMBXLocationComponentModule.mm rename to ios/RNMBX/generated/RNMBXLocationComponentModule.mm index 8bb9bcaf8..54228768e 100644 --- a/ios/RNMBX/RNMBXLocationComponentModule.mm +++ b/ios/RNMBX/generated/RNMBXLocationComponentModule.mm @@ -1,16 +1,19 @@ +/*** +to: ios/RNMBX/generated/RNMBXLocationComponentModule.mm +***/ #import #import #import #import "RNMBXLocationComponentModule.h" + +#import "RNMBXLocationComponentView.h" #ifdef RCT_NEW_ARCH_ENABLED #import "RNMBXLocationComponentView.h" #endif // RCT_NEW_ARCH_ENABLED #import "rnmapbox_maps-Swift.pre.h" -@class RNMBXLocation; - @implementation RNMBXLocationComponentModule RCT_EXPORT_MODULE(); @@ -32,11 +35,11 @@ - (dispatch_queue_t)methodQueue - (std::shared_ptr)getTurboModule: (const facebook::react::ObjCTurboModule::InitParams &)params { - return std::make_shared(params); + return std::make_shared(params); } #endif // RCT_NEW_ARCH_ENABLED -- (void)withLocation:(nonnull NSNumber*)viewRef block:(void (^)(RNMBXLocation *))block reject:(RCTPromiseRejectBlock)reject methodName:(NSString *)methodName +- (void)withRNMBXLocationComponentModule:(nonnull NSNumber*)viewRef block:(void (^)(RNMBXLocation *))block reject:(RCTPromiseRejectBlock)reject methodName:(NSString *)methodName { #ifdef RCT_NEW_ARCH_ENABLED [self.viewRegistry_DEPRECATED addUIBlock:^(RCTViewRegistry *viewRegistry) { @@ -49,22 +52,22 @@ - (void)withLocation:(nonnull NSNumber*)viewRef block:(void (^)(RNMBXLocation *) RNMBXLocation *view = [uiManager viewForReactTag:viewRef]; #endif // RCT_NEW_ARCH_ENABLED if (view != nil) { - block(view); + block(view); } else { reject(methodName, [NSString stringWithFormat:@"Unknown reactTag: %@", viewRef], nil); } }]; } -#if false + RCT_EXPORT_METHOD(someMethod:(nonnull NSNumber *)viewRef resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { - [self withLocation:viewRef block:^(RNMBXCamera *view) { - [RNMBXLocationManager someMethod:view resolve:resolve reject:reject]; + [self withRNMBXLocationComponentModule:viewRef block:^(RNMBXLocation *view) { + [RNMBXLocation someMethod:view resolve:resolve reject:reject]; } reject:reject methodName:@"someMethod"]; } -#endif + @end diff --git a/ios/RNMBX/RNMBXLocationComponentView.h b/ios/RNMBX/generated/RNMBXLocationComponentView.h similarity index 81% rename from ios/RNMBX/RNMBXLocationComponentView.h rename to ios/RNMBX/generated/RNMBXLocationComponentView.h index 8972db940..3b3d7a161 100644 --- a/ios/RNMBX/RNMBXLocationComponentView.h +++ b/ios/RNMBX/generated/RNMBXLocationComponentView.h @@ -1,3 +1,6 @@ +/*** +to: ios/rnmbx/generated/RNMBXLocationComponentView.h +***/ #ifdef RCT_NEW_ARCH_ENABLED #import @@ -7,7 +10,6 @@ NS_ASSUME_NONNULL_BEGIN - @interface RNMBXLocationComponentView : RCTViewComponentView @end diff --git a/ios/RNMBX/generated/RNMBXLocationComponentView.mm b/ios/RNMBX/generated/RNMBXLocationComponentView.mm new file mode 100644 index 000000000..be87f9de9 --- /dev/null +++ b/ios/RNMBX/generated/RNMBXLocationComponentView.mm @@ -0,0 +1,158 @@ +/*** +to: ios/rnmbx/generated/RNMBXLocationComponentView.mm +***/ +#ifdef RCT_NEW_ARCH_ENABLED + +#import "RNMBXLocationComponentView.h" + +#import +#import + +#import +#import +#import +#import + +#import "rnmapbox_maps-Swift.pre.h" + +#import "RCTFollyConvert.h" + +using namespace facebook::react; + + +@implementation RNMBXLocationComponentView { + RNMBXLocation *_view; +} + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + static const auto defaultProps = std::make_shared(); + _props = defaultProps; + _view = [[RNMBXLocation alloc] init]; + [self prepareView]; + + self.contentView = _view; + } + + return self; +} + +- (void)prepareView +{ + __weak __typeof__(self) weakSelf = self; + + /* + [_view setOnStatusChanged:^(NSDictionary* event) { + __typeof__(self) strongSelf = weakSelf; + + if (strongSelf != nullptr && strongSelf->_eventEmitter != nullptr) { + auto type = std::string([[event objectForKey:@"type"] UTF8String]); + auto payload = convertIdToFollyDynamic([event objectForKey:@"payload"]); + RNMBXLocationEventEmitter::OnStatusChanged event = {type, payload}; + strongSelf->_eventEmitter->onStatusChanged(event); + } + }];*/ +} + + +#pragma mark - RCTComponentViewProtocol + ++ (ComponentDescriptorProvider)componentDescriptorProvider +{ + return concreteComponentDescriptorProvider(); +} + +- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps +{ + const auto &oldViewProps = static_cast(*oldProps); + const auto &newViewProps = static_cast(*props); + + /* + if (!oldProps.get() || oldViewProps.transitionsToIdleUponUserInteraction != newViewProps.transitionsToIdleUponUserInteraction) { + _view.transitionsToIdleUponUserInteraction = convertDynamicToOptional_boolean(newViewProps.transitionsToIdleUponUserInteraction, @"transitionsToIdleUponUserInteraction"); + } +*/ + + + if (!oldProps.get() || oldViewProps.hasOnBearingChange != newViewProps.hasOnBearingChange) { + _view.hasOnBearingChange = newViewProps.hasOnBearingChange; + } + + + + if (!oldProps.get() || oldViewProps.hasOnLocationChange != newViewProps.hasOnLocationChange) { + _view.hasOnLocationChange = newViewProps.hasOnLocationChange; + } + + + + [super updateProps:props oldProps:oldProps]; + + + + + if (!oldProps.get() || oldViewProps.hasOnBearingChange != newViewProps.hasOnBearingChange) { + if (newViewProps.hasOnBearingChange) { + __weak __typeof__(self) weakSelf = self; + [_view setOnBearingChange:^(NSDictionary* event) { + __typeof__(self) strongSelf = weakSelf; + if (strongSelf != nullptr && strongSelf->_eventEmitter != nullptr) { + + + auto direction = [[event objectForKey:@"direction"] doubleValue]; + + + auto accuracy = [[event objectForKey:@"accuracy"] doubleValue]; + + + auto timestamp = [[event objectForKey:@"timestamp"] doubleValue]; + + std::dynamic_pointer_cast(strongSelf->_eventEmitter)->onBearingChange({direction,accuracy,timestamp}); + } + }]; + } else { + [_view setOnBearingChange:nil]; + } + } + + + + + if (!oldProps.get() || oldViewProps.hasOnLocationChange != newViewProps.hasOnLocationChange) { + if (newViewProps.hasOnLocationChange) { + __weak __typeof__(self) weakSelf = self; + [_view setOnLocationChange:^(NSDictionary* event) { + __typeof__(self) strongSelf = weakSelf; + if (strongSelf != nullptr && strongSelf->_eventEmitter != nullptr) { + + + auto altitude = [[event objectForKey:@"altitude"] doubleValue]; + + + auto longitude = [[event objectForKey:@"longitude"] doubleValue]; + + + auto latitude = [[event objectForKey:@"latitude"] doubleValue]; + + + auto timestamp = [[event objectForKey:@"timestamp"] doubleValue]; + + std::dynamic_pointer_cast(strongSelf->_eventEmitter)->onLocationChange({altitude,longitude,latitude,timestamp}); + } + }]; + } else { + [_view setOnLocationChange:nil]; + } + } + + +} +@end + +Class RNMBXLocationCls(void) +{ + return RNMBXLocationComponentView.class; +} + +#endif // RCT_NEW_ARCH_ENABLED diff --git a/ios/RNMBX/generated/RNMBXLocationManager.m b/ios/RNMBX/generated/RNMBXLocationManager.m new file mode 100644 index 000000000..d5e857f86 --- /dev/null +++ b/ios/RNMBX/generated/RNMBXLocationManager.m @@ -0,0 +1,20 @@ +/*** +to: ios/rnmbx/generated/RNMBXLocationManager.m +***/ +#import +#import + +@interface RCT_EXTERN_REMAP_MODULE(RNMBXLocation, RNMBXLocationManager, RCTViewManager) + + + RCT_EXPORT_VIEW_PROPERTY(onBearingChange, RCTBubblingEventBlock) + + RCT_EXPORT_VIEW_PROPERTY(onLocationChange, RCTBubblingEventBlock) + + + RCT_EXPORT_VIEW_PROPERTY(hasOnBearingChange, BOOL) + + RCT_EXPORT_VIEW_PROPERTY(hasOnLocationChange, BOOL) + + +@end diff --git a/ios/RNMBX/generated/RNMBXLocationManager.swift b/ios/RNMBX/generated/RNMBXLocationManager.swift new file mode 100644 index 000000000..21bf59ca5 --- /dev/null +++ b/ios/RNMBX/generated/RNMBXLocationManager.swift @@ -0,0 +1,14 @@ +/*** +to: ios/rnmbx/generated/RNMBXLocationManager.swift +***/ +@objc(RNMBXLocationManager) +open class RNMBXLocationManager: RCTViewManager { + @objc + public override static func requiresMainQueueSetup() -> Bool { + return true + } + + public override func view() -> UIView! { + return RNMBXLocation() + } +} \ No newline at end of file diff --git a/ios/RNMBX/generated/RNMBXLocationProtocol.swift b/ios/RNMBX/generated/RNMBXLocationProtocol.swift new file mode 100644 index 000000000..729f695a8 --- /dev/null +++ b/ios/RNMBX/generated/RNMBXLocationProtocol.swift @@ -0,0 +1,33 @@ +/*** +to: ios/rnmbx/generated/RNMBXLocationProtocol.swift +***/ +/* Generated protocol used by Paper and RNMBXLocationManager. Methods must be implemented by the view RNMBXLocation. */ + +@objc public protocol RNMBXLocationProtocol { + +// MARK: - events + + + @objc func setOnBearingChange(_ callback: RCTBubblingEventBlock?); + + @objc func setOnLocationChange(_ callback: RCTBubblingEventBlock?); + + +// MARK: - props + + + + @objc func setHasOnBearingChange(_ value: Bool); + + + + @objc func setHasOnLocationChange(_ value: Bool); + + + + + @objc static func someMethod(_ view: RNMBXLocation, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock); + + + +} \ No newline at end of file diff --git a/package.json b/package.json index 996deed17..5b3d78597 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "@sinonjs/fake-timers": "^8.0.1", "@testing-library/react-native": "^12.4.0", "@types/debounce": "^1.2.1", + "@types/ejs": "^3.1.5", "@types/mapbox-gl": "^2.7.5", "@typescript-eslint/eslint-plugin": "^5.37.0", "@typescript-eslint/parser": "^5.37.0", @@ -105,6 +106,7 @@ "eslint-plugin-jest": "^27.0.1", "expo": "^47.0.0", "expo-module-scripts": "^3.0.4", + "gray-matter": "^4.0.2", "husky": "^8.0.1", "jest": "29.7.0", "jest-cli": "29.7.0", @@ -115,17 +117,21 @@ "prettier": "2.7.1", "react": "18.2.0", "react-docgen": "rnmapbox/react-docgen#rnmapbox-dist-react-docgen-v6", - "react-native": "0.73.0-rc.4", + "react-native": "0.74.2", "react-native-builder-bob": "^0.23.1", "react-test-renderer": "18.2.0", "ts-node": "10.9.1", - "typescript": "5.1.3", - "@mdx-js/mdx": "^3.0.0" + "typescript": "5.1.3" }, "codegenConfig": { "name": "rnmapbox_maps_specs", "type": "all", "jsSrcsDir": "src/specs", + "includesGeneratedCode": true, + "outputDir": { + "android": "android/src/main/codegen", + "ios": "ios/codegen" + }, "android": { "javaPackageName": "com.rnmapbox.rnmbx" } diff --git a/scripts/rnmbxcodegen/README.md b/scripts/rnmbxcodegen/README.md new file mode 100644 index 000000000..a986b3807 --- /dev/null +++ b/scripts/rnmbxcodegen/README.md @@ -0,0 +1,13 @@ +## Events + +Thre is no way to check [1] if event handler is registered. As a workaround if onHeadingChange + + onHeadingChange?: DirectEventHandler; + hasOnHeadingChange: boolean; + +## Questions to new arch working group: + + +### Check if specific event handler has listener [1] +- is thera a way in new arch to check if event callback is registered? + For now as a workaround, we have a boolean field hasOnHeadingChange for an onHeadingChange callback diff --git a/scripts/rnmbxcodegen/component/android/component-view-manager.kt.ejs b/scripts/rnmbxcodegen/component/android/component-view-manager.kt.ejs new file mode 100644 index 000000000..cf0cb6f16 --- /dev/null +++ b/scripts/rnmbxcodegen/component/android/component-view-manager.kt.ejs @@ -0,0 +1,56 @@ +/*** +to: android/src/main/java/<%= meta.javapkg.replaceAll('.','/') %>/<%= Name %>Manager.kt +***/ +package <%= meta.javapkg %> + +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.uimanager.ThemedReactContext +import com.rnmapbox.rnmbx.components.AbstractEventEmitter +import com.rnmapbox.rnmbx.utils.ViewTagResolver +import <%= meta.javapkg %>.<%= Name %> +import com.facebook.react.viewmanagers.<%= Name %>ManagerInterface +import com.facebook.react.uimanager.annotations.ReactProp +import com.rnmapbox.rnmbx.events.constants.EventKeys +import com.rnmapbox.rnmbx.events.constants.eventMapOf + +class <%= Name %>Manager(private val mContext: ReactApplicationContext, val viewTagResolver: ViewTagResolver) : AbstractEventEmitter<<%= Name %>?>( + mContext +), <%= Name %>ManagerInterface<<%= Name %>> { + + override fun getName(): String { + return NAME + } + + override fun createViewInstance(reactContext: ThemedReactContext): <%= Name %> { + return <%= Name %>(reactContext, this) + } + + <%_ component.props.forEach(function(prop) { _%> + @ReactProp(name = "<%= prop.name %>") + override fun set<%= pascelCase(prop.name) %>(view: <%= Name %>, value: <%= typeAnnotationKotlinType(prop.typeAnnotation) %>) { + view?.<%= prop.name %> = value; + } + <%_ }); _%> + + <%_ if (component.events.length > 0) { _%> + //region Custom Events + override fun customEvents(): Map? { + return eventMapOf( + <%_ component.events.forEach(function(event, index) { _%> + EventKeys.<%= camelToUpperSnakeCase(removePrefix(Name, "RNMBX")) %>_<%= pascelToUpperSnakeCase(event.name) %> to "<%= event.name %>", + <%_ }); _%> + ) + } + + override fun getCommandsMap(): Map? { + return mapOf( + "_useCommandName" to 1 + ); + } + //endregion + <%_ } _%> + + companion object { + const val NAME = "<%= Name %>" + } +} \ No newline at end of file diff --git a/scripts/rnmbxcodegen/component/android/component-view.kt.ejs b/scripts/rnmbxcodegen/component/android/component-view.kt.ejs new file mode 100644 index 000000000..3f1003913 --- /dev/null +++ b/scripts/rnmbxcodegen/component/android/component-view.kt.ejs @@ -0,0 +1,18 @@ +/*** +to: android/src/main/java/<%= meta.javapkg.replaceAll('.','/') %>/<%= Name %>.kt +userEditable: true +***/ +package <%= meta.javapkg %> + +import android.content.Context +import com.rnmapbox.rnmbx.components.AbstractMapFeature + +class <%= Name %>(private val mContext: Context, private val mManager: <%= Name %>Manager) : +AbstractMapFeature( +mContext +) { + + <%_ component.props.forEach(function(prop) { _%> + var <%= prop.name %>: <%= typeAnnotationKotlinType(prop.typeAnnotation) %> = false; + <%_ }); _%> +} \ No newline at end of file diff --git a/scripts/rnmbxcodegen/component/ios/_objc_to_cpp.ejs b/scripts/rnmbxcodegen/component/ios/_objc_to_cpp.ejs new file mode 100644 index 000000000..d69cc9c1e --- /dev/null +++ b/scripts/rnmbxcodegen/component/ios/_objc_to_cpp.ejs @@ -0,0 +1,29 @@ +<%_ let type = property.typeAnnotation.type _%> +<%_ if (type == "StringTypeAnnotation") { _%> + auto <%= property.name %> = std::string([[<%= objcDictName %> objectForKey:@"<%= property.name %>"] UTF8String]); +<%_ } else if (type == "DoubleTypeAnnotation") { _%> + auto <%= property.name %> = [[<%= objcDictName %> objectForKey:@"<%= property.name %>"] doubleValue]; +<%_ } else if (type == "ArrayTypeAnnotation") { _%> + NSArray* <%= property.name %>Array = [<%= objcDictName %> objectForKey:@"<%= property.name %>"]; + std::vector<<%= cppType %>> <%= property.name %>; + <%= property.name %>.reserve(<%= property.name %>Array.count); + for (auto i in <%= property.name %>Array) { + <%- include('_objc_to_cpp.ejs', {property: {name: "element", isObj: false, typeAnnotation: property.typeAnnotation.elementType}, objcDictName: "i", cppType: cppType}) _%> + <%= property.name %>.push_back(element); + } +<%_ } else if (type == "ObjectTypeAnnotation") { _%> + auto <%= property.name %> = [&<%= objcDictName %>]() { + auto <%= property.name %>Dict = <% -%> + <%_ if (property.isObj === false) { -%> + <%_ %><%= objcDictName %>; + <%_ } else { -%> + [<%= objcDictName %> objectForKey:@"<%= property.name %>"]; + <%_ } _%> + <%_ let properties = property.typeAnnotation.properties _%> + <%_ let nestedObjcDictName = `${property.name}Dict` _%> + <%_ properties.forEach(function(property) { _%> + <%- include('_objc_to_cpp.ejs', {property: property, objcDictName: nestedObjcDictName, cppType: `${cppType}${pascelCase(property.name)}`}) %> + <%_ }); _%> + return <%= cppType %>{<%= properties.map(p => p.name).join(",") %>}; + }(); +<%_ } _%> \ No newline at end of file diff --git a/scripts/rnmbxcodegen/component/ios/component-view-manager.m.ejs b/scripts/rnmbxcodegen/component/ios/component-view-manager.m.ejs new file mode 100644 index 000000000..a7fce3260 --- /dev/null +++ b/scripts/rnmbxcodegen/component/ios/component-view-manager.m.ejs @@ -0,0 +1,16 @@ +/*** +to: ios/rnmbx/generated/<%= Name %>Manager.m +***/ +#import +#import + +@interface RCT_EXTERN_REMAP_MODULE(<%= Name %>, <%= Name %>Manager, RCTViewManager) + +<% component.events.forEach(function(event) { %> + RCT_EXPORT_VIEW_PROPERTY(<%= event.name %>, <%= event.bubblingType ? "RCTBubblingEventBlock" : "RCTDirectEventBlock" %>) +<% }); %> +<% component.props.forEach(function(prop) { %> + RCT_EXPORT_VIEW_PROPERTY(<%= prop.name %>, <%= typeAnnotationObjCType(prop.typeAnnotation) %>) +<% }); %> + +@end diff --git a/scripts/rnmbxcodegen/component/ios/component-view-manager.swift b/scripts/rnmbxcodegen/component/ios/component-view-manager.swift new file mode 100644 index 000000000..f5b739c15 --- /dev/null +++ b/scripts/rnmbxcodegen/component/ios/component-view-manager.swift @@ -0,0 +1,14 @@ +/*** +to: ios/rnmbx/generated/<%= Name %>Manager.swift +***/ +@objc(<%= Name %>Manager) +open class <%= Name %>Manager: RCTViewManager { + @objc + public override static func requiresMainQueueSetup() -> Bool { + return true + } + + public override func view() -> UIView! { + return <%= Name %>() + } +} \ No newline at end of file diff --git a/scripts/rnmbxcodegen/component/ios/component-view-protocol.swift b/scripts/rnmbxcodegen/component/ios/component-view-protocol.swift new file mode 100644 index 000000000..6720e08a2 --- /dev/null +++ b/scripts/rnmbxcodegen/component/ios/component-view-protocol.swift @@ -0,0 +1,27 @@ +/*** +to: ios/rnmbx/generated/<%= Name %>Protocol.swift +***/ +/* Generated protocol used by Paper and <%= Name %>Manager. Methods must be implemented by the view <%= Name %>. */ + +@objc public protocol <%= Name %>Protocol { + +// MARK: - events + +<% component.events.forEach(function(event) { %> + @objc func set<%= pascelCase(event.name) %>(_ callback: <%= event.bubblingType ? "RCTBubblingEventBlock" : "RCTDirectEventBlock" %>?); +<% }); %> + +// MARK: - props + +<% component.props.forEach(function(prop) { %> + <% if (prop.typeAnnotation.type == "BooleanTypeAnnotation") { %> + @objc func set<%= pascelCase(prop.name) %>(_ value: Bool); + <% } %> +<% }); %> + +<% module?.spec.properties.forEach(function (property) { %> + @objc static func <%= property.name %>(_ view: <%= ComponentName %>, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock); +<% }) %> + + +} \ No newline at end of file diff --git a/scripts/rnmbxcodegen/component/ios/component-view.h.ejs b/scripts/rnmbxcodegen/component/ios/component-view.h.ejs new file mode 100644 index 000000000..61573ade9 --- /dev/null +++ b/scripts/rnmbxcodegen/component/ios/component-view.h.ejs @@ -0,0 +1,19 @@ +/*** +to: ios/rnmbx/generated/<%= Name %>ComponentView.h +***/ +#ifdef RCT_NEW_ARCH_ENABLED + +#import + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface <%= Name %>ComponentView : RCTViewComponentView + +@end + +NS_ASSUME_NONNULL_END + +#endif // RCT_NEW_ARCH_ENABLED diff --git a/scripts/rnmbxcodegen/component/ios/component-view.mm.ejs b/scripts/rnmbxcodegen/component/ios/component-view.mm.ejs new file mode 100644 index 000000000..b3b868909 --- /dev/null +++ b/scripts/rnmbxcodegen/component/ios/component-view.mm.ejs @@ -0,0 +1,115 @@ +/*** +to: ios/rnmbx/generated/<%= Name %>ComponentView.mm +***/ +#ifdef RCT_NEW_ARCH_ENABLED + +#import "<%= Name %>ComponentView.h" + +#import +#import + +#import +#import +#import +#import + +#import "rnmapbox_maps-Swift.pre.h" + +#import "RCTFollyConvert.h" + +using namespace facebook::react; + + +@implementation <%= Name %>ComponentView { + <%= Name %> *_view; +} + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + static const auto defaultProps = std::make_sharedProps>(); + _props = defaultProps; + _view = [[<%= Name %> alloc] init]; + [self prepareView]; + + self.contentView = _view; + } + + return self; +} + +- (void)prepareView +{ + __weak __typeof__(self) weakSelf = self; + + [_view setOnStatusChanged:^(NSDictionary* event) { + __typeof__(self) strongSelf = weakSelf; + + if (strongSelf != nullptr && strongSelf->_eventEmitter != nullptr) { + auto type = std::string([[event objectForKey:@"type"] UTF8String]); + auto payload = convertIdToFollyDynamic([event objectForKey:@"payload"]); + <%= Name %>EventEmitter::OnStatusChanged event = {type, payload}; + strongSelf->_eventEmitter->onStatusChanged(event); + } + }]; +} + + +#pragma mark - RCTComponentViewProtocol + ++ (ComponentDescriptorProvider)componentDescriptorProvider +{ + return concreteComponentDescriptorProvider<<%= Name %>ComponentDescriptor>(); +} + +- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps +{ + const auto &oldViewProps = static_castProps &>(*oldProps); + const auto &newViewProps = static_castProps &>(*props); + + /* + if (!oldProps.get() || oldViewProps.transitionsToIdleUponUserInteraction != newViewProps.transitionsToIdleUponUserInteraction) { + _view.transitionsToIdleUponUserInteraction = convertDynamicToOptional_boolean(newViewProps.transitionsToIdleUponUserInteraction, @"transitionsToIdleUponUserInteraction"); + } +*/ + <% component.props.forEach(function(prop) { %> + <% if (prop.typeAnnotation.type == "BooleanTypeAnnotation") { %> + if (!oldProps.get() || oldViewProps.<%= prop.name %> != newViewProps.<%= prop.name %>) { + _view.<%= prop.name %> = newViewProps.<%= prop.name %>; + } + <% } %> + <% }); %> + + [super updateProps:props oldProps:oldProps]; + + <% component.events.forEach(function (event) { %> + <% let eventGuardProp = component.findEventGuard(event) %> + <% if (eventGuardProp != null) { %> + if (!oldProps.get() || oldViewProps.<%= eventGuardProp.name %> != newViewProps.<%= eventGuardProp.name %>) { + if (newViewProps.<%= eventGuardProp.name %>) { + __weak __typeof__(self) weakSelf = self; + [_view set<%= pascelCase(event.name) %>:^(NSDictionary* event) { + __typeof__(self) strongSelf = weakSelf; + if (strongSelf != nullptr && strongSelf->_eventEmitter != nullptr) { + <% let arguments = event.typeAnnotation.argument.properties %> + <%_ arguments.forEach(function (property) { %> + <%- include('_objc_to_cpp.ejs', { property, objcDictName: "event", cppType: `${Name}EventEmitter::${pascelCase(event.name)}${pascelCase(property.name)}` }) %> + <%_ }); _%> + std::dynamic_pointer_castEventEmitter>(strongSelf->_eventEmitter)-><%= event.name %>({<%= arguments.map(a => a.name).join(",") %>}); + } + }]; + } else { + [_view set<%= pascelCase(event.name) %>:nil]; + } + } + <% } %> + <% }); %> +} +@end + +Class <%= Name %>Cls(void) +{ + return <%= Name %>ComponentView.class; +} + +#endif // RCT_NEW_ARCH_ENABLED diff --git a/scripts/rnmbxcodegen/component/ios/component.swift.ejs b/scripts/rnmbxcodegen/component/ios/component.swift.ejs new file mode 100644 index 000000000..043aeba74 --- /dev/null +++ b/scripts/rnmbxcodegen/component/ios/component.swift.ejs @@ -0,0 +1,15 @@ +/*** +to: ios/rnmbx/<%= Name %>.swift +userEditable: true +***/ + +@objc(<%= Name %>) +open class <%= Name %> : RNMBXMapComponentBase { + +<% module.spec.properties.forEach(function (property) { %> + @objc + public static func <%= property.name %>(_ view: <%= ComponentName %>, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + // TODO implement + } +<% }) %> +} \ No newline at end of file diff --git a/scripts/rnmbxcodegen/componentmodule/ios/module.h.ejs b/scripts/rnmbxcodegen/componentmodule/ios/module.h.ejs new file mode 100644 index 000000000..68c2da2eb --- /dev/null +++ b/scripts/rnmbxcodegen/componentmodule/ios/module.h.ejs @@ -0,0 +1,21 @@ +/*** +to: ios/RNMBX/generated/<%= Name %>.h +***/ +#import +#import + +#ifdef RCT_NEW_ARCH_ENABLED +#import "rnmapbox_maps_specs.h" +#else +#import +#endif + +@interface <%= Name %> : NSObject +#ifdef RCT_NEW_ARCH_ENABLED +Spec> +#else + +#endif + +@end + diff --git a/scripts/rnmbxcodegen/componentmodule/ios/module.mm.ejs b/scripts/rnmbxcodegen/componentmodule/ios/module.mm.ejs new file mode 100644 index 000000000..d57eca0ea --- /dev/null +++ b/scripts/rnmbxcodegen/componentmodule/ios/module.mm.ejs @@ -0,0 +1,73 @@ +/*** +to: ios/RNMBX/generated/<%= Name %>.mm +***/ +#import +#import +#import + +#import "<%= Name %>.h" + +#import "<%= ComponentName %>ComponentView.h" +#ifdef RCT_NEW_ARCH_ENABLED +#import "<%= ComponentName %>ComponentView.h" +#endif // RCT_NEW_ARCH_ENABLED + +#import "rnmapbox_maps-Swift.pre.h" + +@implementation <%= Name %> + +RCT_EXPORT_MODULE(); + +#ifdef RCT_NEW_ARCH_ENABLED +@synthesize viewRegistry_DEPRECATED = _viewRegistry_DEPRECATED; +#endif // RCT_NEW_ARCH_ENABLED +@synthesize bridge = _bridge; + +- (dispatch_queue_t)methodQueue +{ + // It seems that due to how UIBlocks work with uiManager, we need to call the methods there + // for the blocks to be dispatched before the batch is completed + return RCTGetUIManagerQueue(); +} + +// Thanks to this guard, we won't compile this code when we build for the old architecture. +#ifdef RCT_NEW_ARCH_ENABLED +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params +{ + return std::make_sharedSpecJSI>(params); +} +#endif // RCT_NEW_ARCH_ENABLED + +- (void)with<%= Name %>:(nonnull NSNumber*)viewRef block:(void (^)(<%= ComponentName %> *))block reject:(RCTPromiseRejectBlock)reject methodName:(NSString *)methodName +{ +#ifdef RCT_NEW_ARCH_ENABLED + [self.viewRegistry_DEPRECATED addUIBlock:^(RCTViewRegistry *viewRegistry) { + <%= ComponentName %>ComponentView *componentView = [self.viewRegistry_DEPRECATED viewForReactTag:viewRef]; + <%= ComponentName %> *view = componentView.contentView; + +#else + [self.bridge.uiManager + addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { + <%= ComponentName %> *view = [uiManager viewForReactTag:viewRef]; +#endif // RCT_NEW_ARCH_ENABLED + if (view != nil) { + block(view); + } else { + reject(methodName, [NSString stringWithFormat:@"Unknown reactTag: %@", viewRef], nil); + } + }]; +} + +<% module.spec.properties.forEach(function (property) { %> +RCT_EXPORT_METHOD(<%= property.name %>:(nonnull NSNumber *)viewRef + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + [self with<%= Name %>:viewRef block:^(<%= ComponentName %> *view) { + [<%= ComponentName %> <%= property.name %>:view resolve:resolve reject:reject]; + } reject:reject methodName:@"<%= property.name %>"]; +} +<% }) %> + +@end diff --git a/scripts/rnmbxcodegen/rnmbxcodegen.ts b/scripts/rnmbxcodegen/rnmbxcodegen.ts new file mode 100644 index 000000000..7d92e684e --- /dev/null +++ b/scripts/rnmbxcodegen/rnmbxcodegen.ts @@ -0,0 +1,301 @@ +import path from 'path'; +import fs from 'fs'; + +import ejs from 'ejs'; +import _fm from 'gray-matter'; +import type { + SchemaType, + NativeModuleSchema, + ComponentShape, + EventTypeShape, + PropTypeAnnotation, + NamedShape, +} from '@react-native/codegen/lib/CodegenSchema'; + +const ROOT_DIR = path.resolve(__dirname, '..', '..'); +const PAK_JSON_PATH = path.join(ROOT_DIR, 'package.json'); +const pak = JSON.parse(fs.readFileSync(PAK_JSON_PATH, 'utf-8')); +const SPEC_DIR = path.join(ROOT_DIR, pak.codegenConfig.jsSrcsDir); + +const SCHEMA = path.join(ROOT_DIR, 'tmp/generated/schema.json'); +const COMPONENT_TEMPLATE_ROOT = path.join(__dirname, 'component'); +const MODULE_TEMPLATE_ROOT = path.join(__dirname, 'componentmodule'); +const TEMPLATE_SUBDIRS = ['ios', 'android']; + +type ComponentShapeWithUtils = ComponentShape & { + findEventGuard: ( + event: EventTypeShape, + ) => NamedShape | undefined; +}; + +function warning(...args: string[]) { + console.log('[rnmbxcodegen] WARNING:', ...args); +} + +function componentWithMethods( + component: ComponentShape, +): ComponentShapeWithUtils { + return { + ...component, + findEventGuard: (event: EventTypeShape) => { + const guardPropName = `has${config.pascelCase(event.name)}`; + const eventGuard = component.props.find( + (prop) => prop.name === guardPropName, + ); + if (eventGuard == null) { + warning( + `No event guard found for ${event.name} declare a property named: ${guardPropName} as boolean`, + ); + } + return eventGuard; + }, + }; +} + +function fm(body: string): { body: string; attributes: T } { + const result = _fm(body, { + delimiters: ['/***', '***/'], + }); + + return { + body, + attributes: result.data as T, + }; +} + +function readRNCodegenSchema(): SchemaType { + const schema = fs.readFileSync(SCHEMA, 'utf8'); + return JSON.parse(schema); +} + +function renderTemplate( + template: string, + args: { [key: string]: string | object }, + config: { [key: string]: string | object }, + options: { filename?: string } = {}, +) { + return ejs.render(template, { ...args, ...config }, options); +} + +function generate( + templateRoomPath: string, + name: string, + config: T, + getMetadata?: (name: string) => T, +) { + const templateRoots = TEMPLATE_SUBDIRS.map((subdir) => + path.join(templateRoomPath, subdir), + ); + + templateRoots.forEach((templateRoot) => { + if (!fs.existsSync(templateRoot)) return; + + const files = fs.readdirSync(templateRoot); + files.forEach((file) => { + if (file.startsWith('_')) return; + const content = fm<{ [key: string]: string }>( + fs.readFileSync(path.join(templateRoot, file)).toString(), + ); + const args = { Name: name }; + const { attributes } = content; + + const metadata = getMetadata ? getMetadata(name) : {}; + const actConfig = { ...config, ...metadata }; + + const renderedAttrs: { [key: string]: string } = Object.entries( + attributes, + ).reduce((obj, [key, value]) => { + if (typeof value !== 'string') return obj; + return { + ...obj, + [key]: renderTemplate(value, args, actConfig), + }; + }, {}); + + const toPath = path.join(ROOT_DIR, renderedAttrs.to); + console.log(' => toPath', toPath); + fs.mkdirSync(path.dirname(toPath), { recursive: true }); + if (attributes.userEditable && fs.existsSync(toPath)) { + console.log('Skipping user editable file - already exists:', toPath); + } else { + fs.writeFileSync( + toPath, + renderTemplate(content.body, args, actConfig, { + filename: path.join(templateRoot, file), + }), + ); + } + }); + }); +} + +function generateCodeFromComponent( + componentName: string, + componentInfo: ComponentInfo, + config: { [key: string]: string | object }, +) { + generate(COMPONENT_TEMPLATE_ROOT, componentName, { + ...config, + ComponentName: componentName, + Name: componentName, + ...componentInfo, + }); +} + +function generateCodeFromModule( + moduleName: string, + moduleInfo: ModuleInfo, + config: { [key: string]: string | object } = {}, +) { + generate(MODULE_TEMPLATE_ROOT, moduleName, { + ...config, + ModuleName: moduleName, + Name: moduleName, + ...moduleInfo, + }); +} + +type MetaDict = { [key: string]: string | object | boolean }; + +type ComponentInfo = { + component: ComponentShapeWithUtils; + module?: NativeModuleSchema; + ModuleName?: string; + meta: MetaDict; +}; + +type ModuleInfo = { + module: NativeModuleSchema; + component?: ComponentShapeWithUtils; + ComponentName?: string; +}; + +const componentsToGenerate: { + [key: string]: ComponentInfo; +} = {}; + +const modulesToGenerate: { + [key: string]: ModuleInfo; +} = {}; + +Object.entries(readRNCodegenSchema().modules).forEach( + ([moduleName, module]) => { + const filename = + module.type === 'Component' + ? `${moduleName}NativeComponent.ts` + : `${moduleName}.ts`; + const moduleSpecPath = path.join(SPEC_DIR, filename); + const moduleSpecBody = fs.readFileSync(moduleSpecPath, 'utf-8'); + const matter = fm<{ rnmbxcodegen?: boolean; component?: string }>( + moduleSpecBody, + ); + + if (matter.attributes.rnmbxcodegen === true) { + if (module.type === 'Component') { + const { components } = module; + const componentsList = Object.entries(components); + if (componentsList.length > 1) { + throw new Error( + `Only one component per file is supported ${Object.keys( + components, + ).join(', ')}`, + ); + return; + } + const [[componentName, componentData]] = componentsList; + componentsToGenerate[componentName] = { + component: componentWithMethods(componentData), + meta: matter.attributes, + }; + } else if (module.type === 'NativeModule') { + const info: ModuleInfo = { module }; + if (matter.attributes.component) { + info.ComponentName = matter.attributes.component; + } + modulesToGenerate[module.moduleName] = info; + } + } + }, +); + +Object.entries(modulesToGenerate).forEach(([moduleName, module]) => { + if (module.ComponentName != null) { + const componentName = module.ComponentName; + const componentToGenerate = componentsToGenerate[componentName]; + module.component = componentToGenerate.component; + componentToGenerate.module = module.module; + componentToGenerate.ModuleName = moduleName; + } +}); + +const config = { + pascelCase: (str: string, delimiter = '-') => { + const parts = str.split(delimiter); + return parts + .map((part, _index) => { + return part.charAt(0).toUpperCase() + part.substring(1); + }) + .join(''); + }, + removePrefix: (str: string, prefix: string) => { + return str.replace(new RegExp(`^${prefix}`), ''); + }, + pascelToUpperSnakeCase: (str: string) => { + return str.replace(/([A-Z])/g, '_$1').toUpperCase(); + }, + camelToUpperSnakeCase: (str: string) => { + return config.pascelToUpperSnakeCase( + str.charAt(0).toLowerCase() + str.slice(1), + ); + }, + typeAnnotationKotlinType: (typeAnnotation: PropTypeAnnotation) => { + switch (typeAnnotation.type) { + case 'BooleanTypeAnnotation': + return 'Boolean'; + case 'StringTypeAnnotation': + return 'String'; + case 'Int32TypeAnnotation': + return 'Int'; + case 'DoubleTypeAnnotation': + return 'Double'; + case 'FloatTypeAnnotation': + return 'Float'; + case 'ArrayTypeAnnotation': + return 'List<*>'; + case 'ObjectTypeAnnotation': + return 'Map'; + default: + return 'Any'; + } + }, + typeAnnotationObjCType: (typeAnnotation: PropTypeAnnotation) => { + switch (typeAnnotation.type) { + case 'BooleanTypeAnnotation': + return 'BOOL'; + case 'StringTypeAnnotation': + return 'NSString *'; + case 'Int32TypeAnnotation': + return 'int'; + case 'DoubleTypeAnnotation': + return 'double'; + case 'FloatTypeAnnotation': + return 'float'; + case 'ArrayTypeAnnotation': + return 'NSArray *'; + case 'ObjectTypeAnnotation': + return 'NSDictionary *'; + default: + return 'id'; + } + }, +}; + +Object.entries(componentsToGenerate).forEach( + ([componentName, componentInfo]) => { + generateCodeFromComponent(componentName, componentInfo, config); + }, +); + +Object.entries(modulesToGenerate).forEach(([moduleName, moduleInfo]) => { + generateCodeFromModule(moduleName, moduleInfo, config); +}); diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json index b29b284ec..b3467fefe 100644 --- a/scripts/tsconfig.json +++ b/scripts/tsconfig.json @@ -3,5 +3,5 @@ "compilerOptions": { "allowJs": true, }, - "include": ["*.ts", "codepart-replace.mjs"] + "include": ["*.ts", "codepart-replace.mjs", "*/**/*.ts"], } \ No newline at end of file diff --git a/src/Mapbox.ts b/src/Mapbox.ts index f7d9ec131..4f60a49c4 100644 --- a/src/Mapbox.ts +++ b/src/Mapbox.ts @@ -20,6 +20,7 @@ export { UserLocationRenderMode, } from './components/UserLocation'; export { default as LocationPuck } from './components/LocationPuck'; +export { default as Location } from './components/Location'; export { default as VectorSource } from './components/VectorSource'; export { ShapeSource } from './components/ShapeSource'; export { default as RasterSource } from './components/RasterSource'; diff --git a/src/components/Location.tsx b/src/components/Location.tsx index 0d3c1dda8..ea8da9b6b 100644 --- a/src/components/Location.tsx +++ b/src/components/Location.tsx @@ -1,12 +1,41 @@ import React from 'react'; +import { NativeSyntheticEvent } from 'react-native'; import NativeLocation from '../specs/RNMBXLocationNativeComponent'; +import type { + OnLocationChangeEventType, + OnHeadingChangeEventType +} from '../specs/RNMBXLocationNativeComponent'; -type Props = { +type Props = { + onHeadingChange: (event: OnHeadingChangeEventType['payload']) => void; + onLocationChange: (event: OnLocationChangeEventType['payload']) => void; }; - export default function Location(props: Props) { - return ; + const { onHeadingChange, onLocationChange, ...restOfProps } = props; + const callbacks = { + hasOnHeadingChange: false, + hasOnLocationChange: false, + ...(onHeadingChange != null + ? { + onHeadingChange: ( + event: NativeSyntheticEvent, + ) => onHeadingChange(event.nativeEvent.payload), + hasOnHeadingChange: true, + } + : {}), + ...(onLocationChange != null + ? { + onLocationChange: ( + event: NativeSyntheticEvent, + ) => onLocationChange(event.nativeEvent), + hasOnLocationChange: true, + } + : {}), + }; + const actProps = { ...restOfProps, ...callbacks }; + console.log(' => actProps', actProps); + return ; } diff --git a/src/specs/NativeRNMBXLocationModule.ts b/src/specs/NativeRNMBXLocationComponentModule.ts similarity index 69% rename from src/specs/NativeRNMBXLocationModule.ts rename to src/specs/NativeRNMBXLocationComponentModule.ts index a77e03114..46e858336 100644 --- a/src/specs/NativeRNMBXLocationModule.ts +++ b/src/specs/NativeRNMBXLocationComponentModule.ts @@ -1,3 +1,7 @@ +/*** +rnmbxcodegen: true +component: RNMBXLocation +***/ import type { TurboModule } from 'react-native/Libraries/TurboModule/RCTExport'; import { Int32 } from 'react-native/Libraries/Types/CodegenTypes'; import { TurboModuleRegistry } from 'react-native'; @@ -8,4 +12,6 @@ export interface Spec extends TurboModule { someMethod(viewRef: ViewRef): Promise; } -export default TurboModuleRegistry.getEnforcing('RNMBXLocationModule'); +export default TurboModuleRegistry.getEnforcing( + 'RNMBXLocationComponentModule', +); diff --git a/src/specs/RNMBXLocationNativeComponent.ts b/src/specs/RNMBXLocationNativeComponent.ts index d028bae22..e68a20820 100644 --- a/src/specs/RNMBXLocationNativeComponent.ts +++ b/src/specs/RNMBXLocationNativeComponent.ts @@ -1,17 +1,30 @@ +/*** +rnmbxcodegen: true +javapkg: com.rnmapbox.rnmbx.components.location +***/ import type { HostComponent, ViewProps } from 'react-native'; import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; import type { DirectEventHandler } from 'react-native/Libraries/Types/CodegenTypes'; import { Double } from 'react-native/Libraries/Types/CodegenTypes'; -type OnHeadingChangeEventType = { - type: string; - direction: Double; // The heading direction (measured in degrees) relative to true or magnetic north. - accuracy: Double; // The maximum deviation (measured in degrees) between the reported heading and the true geomagnetic heading. +export type OnLocationChangeEventType = { + altitude: Double; + longitude: Double; + latitude: Double; + timestamp: Double; +}; + +export type OnBearingChangeEventType = { + direction: Double; // The bearing direction (measured in degrees) relative to true or magnetic north. + accuracy?: Double; // The maximum deviation (measured in degrees) between the reported bearing and the true geomagnetic bearing. timestamp: Double; // The time at which this heading was determined. }; export interface NativeProps extends ViewProps { - onHeadingChange?: DirectEventHandler; + onBearingChange?: DirectEventHandler; + hasOnBearingChange: boolean; + onLocationChange?: DirectEventHandler; + hasOnLocationChange: boolean; } export default codegenNativeComponent(