Skip to content

Commit

Permalink
feat: shape animators
Browse files Browse the repository at this point in the history
  • Loading branch information
mfazekas committed Nov 19, 2023
1 parent 68d8ee9 commit 80f2b51
Show file tree
Hide file tree
Showing 18 changed files with 631 additions and 24 deletions.
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

0 comments on commit 80f2b51

Please sign in to comment.