Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: shape animators #3185

Merged
merged 1 commit into from
Nov 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions __tests__/interface.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ describe('Public Interface', () => {
// helpers
'Logger',
'Style',

'__experimental',
];
actualKeys.forEach((key) => expect(expectedKeys).toContain(key));
});
Expand Down
28 changes: 27 additions & 1 deletion android/src/main/java/com/rnmapbox/rnmbx/RNMBXPackage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ import com.rnmapbox.rnmbx.modules.RNMBXLogging
import com.rnmapbox.rnmbx.modules.RNMBXModule
import com.rnmapbox.rnmbx.modules.RNMBXOfflineModule
import com.rnmapbox.rnmbx.modules.RNMBXSnapshotModule
import com.rnmapbox.rnmbx.shape_animators.RNMBXMovePointShapeAnimatorModule
import com.rnmapbox.rnmbx.shape_animators.ShapeAnimatorManager
import com.rnmapbox.rnmbx.utils.ViewTagResolver

class RNMBXPackage : TurboReactPackage() {
Expand All @@ -61,6 +63,17 @@ class RNMBXPackage : TurboReactPackage() {
return viewTagResolver
}

var shapeAnimators: ShapeAnimatorManager? = null
fun getShapeAnimators(module: String): ShapeAnimatorManager {
val shapeAnimators = shapeAnimators
if (shapeAnimators == null) {
val result = ShapeAnimatorManager()
this.shapeAnimators = result
return result
}
return shapeAnimators
}

fun resetViewTagResolver() {
viewTagResolver = null
}
Expand All @@ -80,6 +93,7 @@ class RNMBXPackage : TurboReactPackage() {
RNMBXShapeSourceModule.NAME -> return RNMBXShapeSourceModule(reactApplicationContext, getViewTagResolver(reactApplicationContext, s))
RNMBXImageModule.NAME -> return RNMBXImageModule(reactApplicationContext, getViewTagResolver(reactApplicationContext, s))
RNMBXPointAnnotationModule.NAME -> return RNMBXPointAnnotationModule(reactApplicationContext, getViewTagResolver(reactApplicationContext, s))
RNMBXMovePointShapeAnimatorModule.NAME -> return RNMBXMovePointShapeAnimatorModule(reactApplicationContext, getShapeAnimators(s))
}
return null
}
Expand Down Expand Up @@ -107,7 +121,10 @@ class RNMBXPackage : TurboReactPackage() {

// sources
managers.add(RNMBXVectorSourceManager(reactApplicationContext))
managers.add(RNMBXShapeSourceManager(reactApplicationContext, getViewTagResolver(reactApplicationContext, "RNMBXShapeSourceManager")))
managers.add(RNMBXShapeSourceManager(reactApplicationContext,
getViewTagResolver(reactApplicationContext, "RNMBXShapeSourceManager"),
getShapeAnimators("RNMBXShapeSourceManager")
))
managers.add(RNMBXRasterDemSourceManager(reactApplicationContext))
managers.add(RNMBXRasterSourceManager(reactApplicationContext))
managers.add(RNMBXImageSourceManager())
Expand Down Expand Up @@ -227,6 +244,15 @@ class RNMBXPackage : TurboReactPackage() {
false, // isCxxModule
isTurboModule // isTurboModule
)
moduleInfos[RNMBXMovePointShapeAnimatorModule.NAME] = ReactModuleInfo(
RNMBXMovePointShapeAnimatorModule.NAME,
RNMBXMovePointShapeAnimatorModule.NAME,
false,
false,
false,
false,
isTurboModule
)
moduleInfos
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,13 @@ import com.mapbox.bindgen.Value
import com.mapbox.geojson.Feature
import com.rnmapbox.rnmbx.events.AndroidCallbackEvent
import com.mapbox.geojson.FeatureCollection
import com.mapbox.geojson.GeoJson
import com.mapbox.geojson.Geometry
import com.mapbox.maps.*
import com.mapbox.maps.extension.style.expressions.generated.Expression
import com.rnmapbox.rnmbx.shape_animators.ShapeAnimationConsumer
import com.rnmapbox.rnmbx.shape_animators.ShapeAnimator
import com.rnmapbox.rnmbx.shape_animators.ShapeAnimatorManager
import com.rnmapbox.rnmbx.utils.Logger
import java.net.URL
import java.util.ArrayList
Expand All @@ -24,9 +29,10 @@ import java.util.HashMap
import com.rnmapbox.rnmbx.v11compat.feature.*

class RNMBXShapeSource(context: Context, private val mManager: RNMBXShapeSourceManager) :
RNMBXSource<GeoJsonSource>(context) {
RNMBXSource<GeoJsonSource>(context), ShapeAnimationConsumer {
private var mURL: URL? = null
private var mShape: String? = null
private var mShapeAnimator: ShapeAnimator? = null
private var mCluster: Boolean? = null
private var mClusterRadius: Long? = null
private var mClusterMaxZoom: Long? = null
Expand Down Expand Up @@ -70,11 +76,55 @@ class RNMBXShapeSource(context: Context, private val mManager: RNMBXShapeSourceM
}

fun setShape(geoJSONStr: String) {
mShape = geoJSONStr
if (mSource != null && mMapView != null && !mMapView!!.isDestroyed) {
mSource!!.data(mShape!!)
val result = mMap!!.getStyle()!!
.setStyleSourceProperty(iD!!, "data", Value.valueOf(mShape!!))
mShapeAnimator?.unsubscribe(this)
mShapeAnimator = null

val shapeAnimatorManager = mManager.shapeAnimatorManager
if (shapeAnimatorManager.isShapeAnimatorTag(geoJSONStr)) {
shapeAnimatorManager.get(geoJSONStr)?.let { shapeAnimator ->
mShapeAnimator = shapeAnimator
shapeAnimator.subscribe(this)

shapeUpdated(shapeAnimator.getShape())
}
} else {
mShape = geoJSONStr
if (mSource != null && mMapView != null && !mMapView!!.isDestroyed) {
mSource!!.data(mShape!!)
val result = mMap!!.getStyle()!!
.setStyleSourceProperty(iD!!, "data", Value.valueOf(mShape!!))
}
}
}

private fun toGeoJSONSourceData(geoJson: GeoJson): GeoJSONSourceData? {
return when (geoJson) {
is Geometry ->
GeoJSONSourceData(geoJson)
is Feature ->
GeoJSONSourceData(geoJson)
is FeatureCollection ->
GeoJSONSourceData(geoJson.features() ?: listOf())
else -> {
Logger.e(
LOG_TAG,
"Cannot convert shape to GeoJSONSourceData, neitthe Geometry, nor Feature or FeatureCollection: $geoJson"
);
return null
}
}
}
override fun shapeUpdated(geoJson: GeoJson) {
mSource?.also {
if (mSource != null && mMapView != null && !mMapView!!.isDestroyed) {
toGeoJSONSourceData(geoJson)?.let {
mMap?.getStyle()?.setStyleGeoJSONSourceData(iD!!,
"animated-shape",
it)
}
}
} ?: run {
mShape = geoJson.toJson()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import com.mapbox.bindgen.Value
import com.mapbox.maps.extension.style.expressions.generated.Expression
import com.rnmapbox.rnmbx.events.constants.EventKeys
import com.rnmapbox.rnmbx.events.constants.eventMapOf
import com.rnmapbox.rnmbx.shape_animators.ShapeAnimatorManager
import com.rnmapbox.rnmbx.utils.ExpressionParser
import com.rnmapbox.rnmbx.utils.Logger
import com.rnmapbox.rnmbx.utils.ViewTagResolver
Expand All @@ -22,7 +23,7 @@ import java.util.ArrayList
import java.util.HashMap


class RNMBXShapeSourceManager(private val mContext: ReactApplicationContext, val viewTagResolver: ViewTagResolver) :
class RNMBXShapeSourceManager(private val mContext: ReactApplicationContext, val viewTagResolver: ViewTagResolver, val shapeAnimatorManager: ShapeAnimatorManager) :
AbstractEventEmitter<RNMBXShapeSource>(
mContext
), RNMBXShapeSourceManagerInterface<RNMBXShapeSource> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.rnmapbox.rnmbx.shape_animators

import com.facebook.react.bridge.ReadableArray
import com.rnmapbox.rnmbx.NativeRNMBXMovePointShapeAnimatorModuleSpec
import com.rnmapbox.rnmbx.components.annotation.RNMBXPointAnnotation

import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.module.annotations.ReactModule
import com.google.gson.JsonObject
import com.mapbox.geojson.GeoJson
import com.mapbox.geojson.Point
import com.rnmapbox.rnmbx.NativeRNMBXPointAnnotationModuleSpec
import com.rnmapbox.rnmbx.utils.Logger
import com.rnmapbox.rnmbx.utils.ViewTagResolver
import org.json.JSONObject
import java.util.Date
import java.util.Timer
import java.util.TimerTask
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.DurationUnit


/// Simple dummy animator that moves the point lng, lat by 0.01, 0.01 each second.
class MovePointShapeAnimator(tag: Tag, val lng: Double, val lat: Double) : ShapeAnimatorCommon(tag) {
override fun getAnimatedShape(timeSinceStart: Duration): Pair<GeoJson, Boolean> {
return Pair(Point.fromLngLat(lng + timeSinceStart.toDouble(DurationUnit.SECONDS) * 0.01, lat + timeSinceStart.toDouble(DurationUnit.SECONDS) * 0.01), true);
}
}


@ReactModule(name = RNMBXMovePointShapeAnimatorModule.NAME)
class RNMBXMovePointShapeAnimatorModule(reactContext: ReactApplicationContext?, val shapeAnimatorManager: ShapeAnimatorManager) :
NativeRNMBXMovePointShapeAnimatorModuleSpec(reactContext) {

companion object {
const val LOG_TAG = "RNMBXMovePointShapeAnimatorModule"
const val NAME = "RNMBXMovePointShapeAnimatorModule"
}

@ReactMethod
override fun start(tag: Double, promise: Promise?) {
shapeAnimatorManager?.get(tag.toLong())?.let {
it.start()
}
}

@ReactMethod
override fun create(tag: Double, from: ReadableArray, promise: Promise) {
shapeAnimatorManager.add(MovePointShapeAnimator(tag.toLong(), from.getDouble(0), from.getDouble(1)))
promise.resolve(tag.toInt())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package com.rnmapbox.rnmbx.shape_animators

import com.facebook.react.bridge.UiThreadUtil.runOnUiThread
import com.mapbox.geojson.GeoJson
import com.rnmapbox.rnmbx.utils.Logger
import org.json.JSONObject
import java.util.Date
import java.util.Timer
import java.util.TimerTask
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds

typealias Tag = Long

interface ShapeAnimationConsumer {
fun shapeUpdated(geoJson: GeoJson)
}
abstract class ShapeAnimator(val tag: Tag) {

abstract fun getShape(): GeoJson;
abstract fun start()

abstract fun subscribe(consumer: ShapeAnimationConsumer)
abstract fun unsubscribe(consumer: ShapeAnimationConsumer)
}

abstract class ShapeAnimatorCommon(tag: Tag): ShapeAnimator(tag) {
var timer: Timer? = null
var progress: Duration? = null

// region subscribers
var subscribers = mutableListOf<ShapeAnimationConsumer>()

override fun subscribe(consumer: ShapeAnimationConsumer) {
subscribers.add(consumer)
}

override fun unsubscribe(consumer: ShapeAnimationConsumer) {
subscribers.remove(consumer)
}
// endregion

override fun start() {
timer?.let { it.cancel() }
timer = null

val fps = 30.0
val period = (1000.0 / fps).toLong()
val start = Date()
val timer = Timer()
this.timer = timer
val animator = this

timer.schedule(object : TimerTask() {
override fun run() {

val now = Date()
val diff = now.time - start.time
val progress = diff.milliseconds
animator.progress = progress

val (shape,doContinue) = getAnimatedShape(progress)
if (!doContinue) {
timer.cancel()
}
runOnUiThread {
subscribers.forEach { it.shapeUpdated(shape) }
}
}
},0, period)
}

override fun getShape(): GeoJson {
return getAnimatedShape(progress ?: 0.0.milliseconds).first
}
abstract fun getAnimatedShape(timeSinceStart: Duration): Pair<GeoJson,Boolean>

}
class ShapeAnimatorManager {
private val animators = hashMapOf<Tag, ShapeAnimator>();
fun add(animator: ShapeAnimator) {
animators.put(animator.tag, animator)
}

fun isShapeAnimatorTag(shape: String): Boolean {
return shape.startsWith("{\"__nativeTag\":")
}

fun get(tag: String): ShapeAnimator? {
if (isShapeAnimatorTag(tag)) {
val obj = JSONObject(tag)
val tag = obj.getLong("__nativeTag")
return get(tag);
}
else {
return null
}
}
fun get(tag: Tag): ShapeAnimator? {
val result = animators[tag]
if (result == null) {
Logger.e(LOG_TAG, "Shape animator for tag: $tag was not found")
}
return result
}

companion object {
const val LOG_TAG = "RNMBXShapeAnimators"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@

/**
* This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
*
* Do not edit this file as changes may cause incorrect behavior and will be lost
* once the code is regenerated.
*
* @generated by codegen project: GenerateModuleJavaSpec.js
*
* @nolint
*/

package com.rnmapbox.rnmbx;

import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.turbomodule.core.interfaces.TurboModule;
import javax.annotation.Nonnull;

public abstract class NativeRNMBXMovePointShapeAnimatorModuleSpec extends ReactContextBaseJavaModule implements TurboModule {
public static final String NAME = "RNMBXMovePointShapeAnimatorModule";

public NativeRNMBXMovePointShapeAnimatorModuleSpec(ReactApplicationContext reactContext) {
super(reactContext);
}

@Override
public @Nonnull String getName() {
return NAME;
}

@ReactMethod
@DoNotStrip
public abstract void create(double tag, ReadableArray from, Promise promise);

@ReactMethod
@DoNotStrip
public abstract void start(double tag, Promise promise);
}
2 changes: 1 addition & 1 deletion example/ios/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ if linkage != nil
use_frameworks! :linkage => linkage.to_sym
end

flags = get_default_flags()
flags = ReactNativePodsUtils.get_default_flags()
if flags[:fabric_enabled]
pod 'glog', :podspec => '../node_modules/react-native/third-party-podspecs/glog.podspec', :modular_headers => false
end
Expand Down
Loading
Loading