react-native-brouter 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +125 -0
  3. package/android/build.gradle +110 -0
  4. package/android/generated/java/com/jhotadhari/reactnative/brouter/NativeBRouterSpec.java +39 -0
  5. package/android/generated/jni/CMakeLists.txt +28 -0
  6. package/android/generated/jni/RNBRouterSpec-generated.cpp +32 -0
  7. package/android/generated/jni/RNBRouterSpec.h +31 -0
  8. package/android/generated/jni/react/renderer/components/RNBRouterSpec/RNBRouterSpecJSI.h +38 -0
  9. package/android/gradle.properties +5 -0
  10. package/android/src/main/AndroidManifest.xml +3 -0
  11. package/android/src/main/AndroidManifestNew.xml +2 -0
  12. package/android/src/main/aidl/btools/routingapp/IBRouterService.aidl +47 -0
  13. package/android/src/main/java/com/jhotadhari/reactnative/brouter/BRouterClient.java +205 -0
  14. package/android/src/main/java/com/jhotadhari/reactnative/brouter/BRouterError.java +38 -0
  15. package/android/src/main/java/com/jhotadhari/reactnative/brouter/BRouterModule.java +135 -0
  16. package/android/src/main/java/com/jhotadhari/reactnative/brouter/BRouterPackage.kt +33 -0
  17. package/android/src/main/java/com/jhotadhari/reactnative/brouter/BRouterServiceConnection.java +54 -0
  18. package/android/src/main/java/com/jhotadhari/reactnative/brouter/ParamMapper.java +174 -0
  19. package/android/src/test/java/com/jhotadhari/reactnative/brouter/BRouterClientTest.java +247 -0
  20. package/android/src/test/java/com/jhotadhari/reactnative/brouter/BRouterErrorTest.java +48 -0
  21. package/android/src/test/java/com/jhotadhari/reactnative/brouter/BRouterModuleTest.java +137 -0
  22. package/android/src/test/java/com/jhotadhari/reactnative/brouter/ParamMapperTest.java +279 -0
  23. package/lib/module/NativeBRouter.js +5 -0
  24. package/lib/module/NativeBRouter.js.map +1 -0
  25. package/lib/module/geojson/index.js +235 -0
  26. package/lib/module/geojson/index.js.map +1 -0
  27. package/lib/module/index.js +297 -0
  28. package/lib/module/index.js.map +1 -0
  29. package/lib/module/package.json +1 -0
  30. package/lib/module/types.js +2 -0
  31. package/lib/module/types.js.map +1 -0
  32. package/lib/typescript/package.json +1 -0
  33. package/lib/typescript/release.config.d.ts +11 -0
  34. package/lib/typescript/release.config.d.ts.map +1 -0
  35. package/lib/typescript/src/NativeBRouter.d.ts +9 -0
  36. package/lib/typescript/src/NativeBRouter.d.ts.map +1 -0
  37. package/lib/typescript/src/geojson/index.d.ts +122 -0
  38. package/lib/typescript/src/geojson/index.d.ts.map +1 -0
  39. package/lib/typescript/src/index.d.ts +13 -0
  40. package/lib/typescript/src/index.d.ts.map +1 -0
  41. package/lib/typescript/src/types.d.ts +93 -0
  42. package/lib/typescript/src/types.d.ts.map +1 -0
  43. package/package.json +159 -0
  44. package/react-native.config.js +12 -0
  45. package/src/NativeBRouter.ts +8 -0
  46. package/src/geojson/index.ts +371 -0
  47. package/src/index.tsx +344 -0
  48. package/src/types.ts +112 -0
@@ -0,0 +1,205 @@
1
+ package com.jhotadhari.reactnative.brouter;
2
+
3
+ import android.content.Context;
4
+
5
+ import androidx.annotation.NonNull;
6
+ import androidx.annotation.Nullable;
7
+
8
+ import btools.routingapp.IBRouterService;
9
+
10
+ /**
11
+ * Manages the bind lifecycle to the external BRouter Android app's AIDL service.
12
+ *
13
+ * <p>This replaces the previous {@code BRouterConnector} singleton with an
14
+ * instance-based design — each {@link BRouterModule} owns one client, keyed
15
+ * to its {@code ReactContext}, so connection timeouts and lifecycle are
16
+ * scoped correctly.
17
+ *
18
+ * <p>The bind-and-poll pattern is derived from OsmAnd's
19
+ * {@code BRouterServiceConnection} usage (the polling loop is necessary
20
+ * because the external service process needs time to start after the bind
21
+ * intent is sent).
22
+ */
23
+ public class BRouterClient {
24
+
25
+ private final Context ctx;
26
+ private final int connectTimeoutMs;
27
+
28
+ @Nullable
29
+ private BRouterServiceConnection connection;
30
+
31
+ /**
32
+ * The error code from the most recent failed {@link #connect()} call,
33
+ * or {@code null} if the last connection attempt succeeded.
34
+ */
35
+ @Nullable
36
+ private String lastError;
37
+
38
+ /**
39
+ * @param ctx Android context used to bind the BRouter service.
40
+ * @param connectTimeoutMs maximum time (ms) to wait for the service process
41
+ * to start. Default 1000.
42
+ */
43
+ public BRouterClient( @NonNull Context ctx, int connectTimeoutMs ) {
44
+ this.ctx = ctx.getApplicationContext();
45
+ this.connectTimeoutMs = connectTimeoutMs;
46
+ }
47
+
48
+ /**
49
+ * Bind to the BRouter service and wait for it to become available.
50
+ *
51
+ * <p>Safe to call multiple times — if already connected this is a no-op.
52
+ *
53
+ * <p>The early-return / connection-creation path is synchronized, but the
54
+ * polling loop runs outside the monitor so that a main-thread
55
+ * {@link #disconnect()} (via {@code invalidate()}) is not blocked.
56
+ *
57
+ * @return the AIDL service interface, or {@code null} if the connection
58
+ * could not be established. Call {@link #getLastError()} for
59
+ * the specific failure code.
60
+ */
61
+ @Nullable
62
+ public IBRouterService connect() {
63
+ BRouterServiceConnection localConn;
64
+
65
+ synchronized ( this ) {
66
+ if ( connection != null ) {
67
+ IBRouterService svc = connection.getBRouterService();
68
+ if ( svc != null && svc.asBinder().isBinderAlive() ) {
69
+ lastError = null;
70
+ return svc;
71
+ }
72
+ // Binder died — reconnect below
73
+ disconnect();
74
+ }
75
+
76
+ connection = BRouterServiceConnection.connect( ctx );
77
+ if ( connection == null ) {
78
+ lastError = BRouterError.SERVICE_NOT_INSTALLED;
79
+ return null;
80
+ }
81
+ localConn = connection;
82
+ }
83
+
84
+ // Poll until the service process starts, or timeout.
85
+ // Pattern copied from OsmAnd — the service process needs time to start
86
+ // after the bind intent fires, and there is no callback for "process ready".
87
+ // Polling runs OUTSIDE the synchronized block so that a main-thread
88
+ // invalidate() -> disconnect() can proceed without blocking.
89
+ int i = 0;
90
+ try {
91
+ while ( localConn.getBRouterService() == null && i * 100 < connectTimeoutMs ) {
92
+ Thread.sleep( 100 );
93
+ i += 1;
94
+ }
95
+ } catch ( InterruptedException e ) {
96
+ Thread.currentThread().interrupt();
97
+ lastError = BRouterError.CONNECTION_TIMEOUT;
98
+ disconnect();
99
+ return null;
100
+ } catch ( Exception e ) {
101
+ e.printStackTrace();
102
+ lastError = BRouterError.UNKNOWN;
103
+ disconnect();
104
+ return null;
105
+ }
106
+
107
+ if ( localConn.getBRouterService() == null ) {
108
+ lastError = BRouterError.CONNECTION_TIMEOUT;
109
+ disconnect();
110
+ return null;
111
+ }
112
+
113
+ lastError = null;
114
+ return localConn.getBRouterService();
115
+ }
116
+
117
+ /**
118
+ * Return the error code from the most recent failed {@link #connect()}
119
+ * call, or {@code null} if the last connection attempt succeeded.
120
+ *
121
+ * <p>Use this after {@link #connect()} returns {@code null} (or
122
+ * {@link #getRoute} throws) to distinguish "app not installed" from a
123
+ * transient connection timeout.
124
+ */
125
+ @Nullable
126
+ public String getLastError() {
127
+ return lastError;
128
+ }
129
+
130
+ /**
131
+ * Return the connection timeout in milliseconds.
132
+ */
133
+ public int getConnectTimeoutMs() {
134
+ return connectTimeoutMs;
135
+ }
136
+
137
+ /**
138
+ * Return the current service interface, reconnecting transparently if the
139
+ * binder has died.
140
+ *
141
+ * <p>Copied pattern from OsmAnd's {@code getBRouterService()}.
142
+ */
143
+ @Nullable
144
+ public synchronized IBRouterService getService() {
145
+ if ( connection == null ) {
146
+ return connect();
147
+ }
148
+ IBRouterService service = connection.getBRouterService();
149
+ if ( service == null ) {
150
+ return connect();
151
+ }
152
+ if ( ! service.asBinder().isBinderAlive() ) {
153
+ return connect();
154
+ }
155
+ lastError = null;
156
+ return service;
157
+ }
158
+
159
+ /**
160
+ * Call the BRouter service with the given parameter bundle.
161
+ *
162
+ * @param params the Bundle produced by {@link ParamMapper#toBundle}.
163
+ * @return the track result string, or {@code null} if routing failed.
164
+ * @throws IllegalStateException if the service is unavailable (call
165
+ * {@link #getLastError()} for the specific error code).
166
+ * @throws Exception if the AIDL call itself fails.
167
+ */
168
+ @Nullable
169
+ public String getRoute( @NonNull android.os.Bundle params ) throws Exception {
170
+ IBRouterService service = getService();
171
+ if ( service == null ) {
172
+ String code = lastError != null ? lastError : BRouterError.SERVICE_UNAVAILABLE;
173
+ throw new IllegalStateException( code );
174
+ }
175
+ return service.getTrackFromParams( params );
176
+ }
177
+
178
+ /**
179
+ * Whether the service is currently connected and alive.
180
+ */
181
+ public synchronized boolean isConnected() {
182
+ if ( connection == null ) {
183
+ return false;
184
+ }
185
+ IBRouterService svc = connection.getBRouterService();
186
+ return svc != null && svc.asBinder().isBinderAlive();
187
+ }
188
+
189
+ /**
190
+ * Unbind from the BRouter service and release resources.
191
+ *
192
+ * <p>Safe to call multiple times. After calling this, the next
193
+ * {@link #connect()} will create a fresh binding.
194
+ */
195
+ public synchronized void disconnect() {
196
+ if ( connection != null ) {
197
+ try {
198
+ connection.disconnect( ctx );
199
+ } catch ( Exception e ) {
200
+ // Best-effort cleanup — the context may already be gone.
201
+ }
202
+ connection = null;
203
+ }
204
+ }
205
+ }
@@ -0,0 +1,38 @@
1
+ package com.jhotadhari.reactnative.brouter;
2
+
3
+ /**
4
+ * Structured error codes for the BRouter native module.
5
+ *
6
+ * <p>Each error code maps to a distinct failure scenario so that JS consumers
7
+ * can react programmatically (e.g. prompt the user to install BRouter vs.
8
+ * retry a transient connection failure).
9
+ *
10
+ * <p>Errors are passed to JS via {@code promise.reject(code, message)}.
11
+ */
12
+ public final class BRouterError {
13
+
14
+ /** The BRouter Android app is not installed on the device. */
15
+ public static final String SERVICE_NOT_INSTALLED = "SERVICE_NOT_INSTALLED";
16
+
17
+ /** Bound to the BRouter process but could not obtain the AIDL interface. */
18
+ public static final String SERVICE_UNAVAILABLE = "SERVICE_UNAVAILABLE";
19
+
20
+ /** The BRouter service process did not start within the connect timeout. */
21
+ public static final String CONNECTION_TIMEOUT = "CONNECTION_TIMEOUT";
22
+
23
+ /** The routing calculation exceeded {@code maxRunningTime}. */
24
+ public static final String ROUTING_TIMEOUT = "ROUTING_TIMEOUT";
25
+
26
+ /** The request parameters are invalid (e.g. fewer than 2 waypoints). */
27
+ public static final String INVALID_PARAMS = "INVALID_PARAMS";
28
+
29
+ /** BRouter could not find a route between the given waypoints. */
30
+ public static final String ROUTING_FAILED = "ROUTING_FAILED";
31
+
32
+ /** An unexpected error occurred. See the message for details. */
33
+ public static final String UNKNOWN = "UNKNOWN";
34
+
35
+ private BRouterError() {
36
+ // static constants only
37
+ }
38
+ }
@@ -0,0 +1,135 @@
1
+ package com.jhotadhari.reactnative.brouter;
2
+
3
+ import android.os.Bundle;
4
+
5
+ import androidx.annotation.NonNull;
6
+ import androidx.annotation.Nullable;
7
+
8
+ import com.facebook.react.bridge.Promise;
9
+ import com.facebook.react.bridge.ReactApplicationContext;
10
+ import com.facebook.react.bridge.ReactMethod;
11
+ import com.facebook.react.bridge.ReadableMap;
12
+ import com.facebook.react.module.annotations.ReactModule;
13
+
14
+ import btools.routingapp.IBRouterService;
15
+
16
+ @ReactModule(name = BRouterModule.NAME)
17
+ public class BRouterModule extends NativeBRouterSpec {
18
+
19
+ public static final String NAME = "BRouter";
20
+
21
+ private static final int DEFAULT_CONNECT_TIMEOUT_MS = 1000;
22
+
23
+ @Nullable
24
+ BRouterClient client;
25
+
26
+ public BRouterModule( ReactApplicationContext reactContext ) {
27
+ super( reactContext );
28
+ }
29
+
30
+ @NonNull
31
+ @Override
32
+ public String getName() {
33
+ return NAME;
34
+ }
35
+
36
+ /**
37
+ * Return the BRouter client, creating it lazily if necessary.
38
+ *
39
+ * <p>Synchronized so two concurrent {@code getRoute()} calls cannot
40
+ * race and create duplicate clients (each with its own active
41
+ * {@code ServiceConnection} binding).
42
+ *
43
+ * <p>If the client already exists and the caller passes a different
44
+ * {@code connectTimeout}, the old client is disconnected and a fresh
45
+ * one is created — per-request timeouts are supported rather than
46
+ * being locked to the first call's value.
47
+ */
48
+ @NonNull
49
+ synchronized BRouterClient getClient( @NonNull ReadableMap params ) {
50
+ int timeout = params.hasKey( "connectTimeout" )
51
+ ? params.getInt( "connectTimeout" )
52
+ : DEFAULT_CONNECT_TIMEOUT_MS;
53
+
54
+ if ( client != null ) {
55
+ if ( client.getConnectTimeoutMs() != timeout ) {
56
+ client.disconnect();
57
+ client = null;
58
+ }
59
+ }
60
+
61
+ if ( client == null ) {
62
+ client = new BRouterClient( getReactApplicationContext(), timeout );
63
+ }
64
+ return client;
65
+ }
66
+
67
+ @Override
68
+ @ReactMethod
69
+ public void getRoute( @NonNull ReadableMap params, @NonNull Promise promise ) {
70
+ android.util.Log.d( "BRouterModule", "getRoute keys: " + params.toHashMap().keySet() );
71
+ try {
72
+ // Validate waypoints
73
+ if ( ! params.hasKey( "lonlats" ) || ! params.hasKey( "lats" ) || ! params.hasKey( "lons" ) ) {
74
+ promise.reject(
75
+ BRouterError.INVALID_PARAMS,
76
+ "At least 2 waypoints are required"
77
+ );
78
+ return;
79
+ }
80
+
81
+ // Build the AIDL Bundle
82
+ Bundle brouterParams = ParamMapper.toBundle( params );
83
+
84
+ // Use client.getRoute() which handles transparent reconnect
85
+ // on binder death — the raw service reference is never exposed.
86
+ BRouterClient brouterClient = getClient( params );
87
+ String track = brouterClient.getRoute( brouterParams );
88
+
89
+ if ( track == null ) {
90
+ promise.reject(
91
+ BRouterError.ROUTING_FAILED,
92
+ "No route found between the given waypoints"
93
+ );
94
+ return;
95
+ }
96
+
97
+ promise.resolve( track );
98
+ } catch ( Exception e ) {
99
+ e.printStackTrace();
100
+
101
+ // Map known error codes from BRouterClient.getLastError()
102
+ String code = BRouterError.UNKNOWN;
103
+ String message = e.toString();
104
+
105
+ if ( e instanceof IllegalStateException ) {
106
+ String detail = e.getMessage();
107
+ if ( detail != null ) {
108
+ // BRouterClient.getRoute() passes the error code as the
109
+ // exception message when the service is unavailable.
110
+ if ( detail.equals( BRouterError.SERVICE_NOT_INSTALLED )
111
+ || detail.equals( BRouterError.CONNECTION_TIMEOUT )
112
+ || detail.equals( BRouterError.SERVICE_UNAVAILABLE ) ) {
113
+ code = detail;
114
+ message = detail.equals( BRouterError.SERVICE_NOT_INSTALLED )
115
+ ? "The BRouter app is not installed on this device"
116
+ : detail.equals( BRouterError.CONNECTION_TIMEOUT )
117
+ ? "Timed out waiting for the BRouter service to start"
118
+ : "BRouter service is not available";
119
+ }
120
+ }
121
+ }
122
+
123
+ promise.reject( code, message );
124
+ }
125
+ }
126
+
127
+ @Override
128
+ public void invalidate() {
129
+ if ( client != null ) {
130
+ client.disconnect();
131
+ client = null;
132
+ }
133
+ super.invalidate();
134
+ }
135
+ }
@@ -0,0 +1,33 @@
1
+ package com.jhotadhari.reactnative.brouter
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfo
7
+ import com.facebook.react.module.model.ReactModuleInfoProvider
8
+ import java.util.HashMap
9
+
10
+ class BRouterPackage : BaseReactPackage() {
11
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
12
+ return if (name == BRouterModule.NAME) {
13
+ BRouterModule(reactContext)
14
+ } else {
15
+ null
16
+ }
17
+ }
18
+
19
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
20
+ return ReactModuleInfoProvider {
21
+ val moduleInfos: MutableMap<String, ReactModuleInfo> = HashMap()
22
+ moduleInfos[BRouterModule.NAME] = ReactModuleInfo(
23
+ BRouterModule.NAME,
24
+ BRouterModule.NAME,
25
+ false, // canOverrideExistingModule
26
+ false, // needsEagerInit
27
+ false, // isCxxModule
28
+ true // isTurboModule
29
+ )
30
+ moduleInfos
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,54 @@
1
+ package com.jhotadhari.reactnative.brouter;
2
+
3
+ import android.content.ComponentName;
4
+ import android.content.Context;
5
+ import android.content.Intent;
6
+ import android.content.ServiceConnection;
7
+ import android.os.IBinder;
8
+
9
+ import androidx.annotation.NonNull;
10
+ import androidx.annotation.Nullable;
11
+
12
+ import btools.routingapp.IBRouterService;
13
+
14
+ /**
15
+ * Copy of https://github.com/osmandapp/OsmAnd/blob/master/OsmAnd/src/btools/routingapp/BRouterServiceConnection.java
16
+ */
17
+ public class BRouterServiceConnection implements ServiceConnection {
18
+
19
+ private volatile IBRouterService brouterService;
20
+
21
+ public void onServiceConnected(ComponentName className, IBinder boundService) {
22
+ brouterService = IBRouterService.Stub.asInterface(boundService);
23
+ }
24
+
25
+ public void onServiceDisconnected(ComponentName className) {
26
+ brouterService = null;
27
+ }
28
+
29
+ public void disconnect(@NonNull Context ctx) {
30
+ ctx.unbindService(this);
31
+ }
32
+
33
+ @Nullable
34
+ public IBRouterService getBRouterService() {
35
+ return brouterService;
36
+ }
37
+
38
+ @Nullable
39
+ public static BRouterServiceConnection connect( @NonNull Context ctx ) {
40
+ BRouterServiceConnection conn = new BRouterServiceConnection();
41
+ Intent intent = new Intent();
42
+ intent.setClassName("btools.routingapp", "btools.routingapp.BRouterService");
43
+ boolean hasBRouter = ctx.bindService(
44
+ intent,
45
+ conn,
46
+ Context.BIND_AUTO_CREATE
47
+ );
48
+ if ( ! hasBRouter ) {
49
+ conn = null;
50
+ }
51
+ return conn;
52
+ }
53
+ }
54
+
@@ -0,0 +1,174 @@
1
+ package com.jhotadhari.reactnative.brouter;
2
+
3
+ import android.os.Bundle;
4
+
5
+ import com.facebook.react.bridge.ReadableArray;
6
+ import com.facebook.react.bridge.ReadableMap;
7
+ import com.facebook.react.bridge.ReadableType;
8
+
9
+ import androidx.annotation.NonNull;
10
+ import androidx.annotation.Nullable;
11
+
12
+ /**
13
+ * Converts a {@link ReadableMap} (serialized from the JS {@code RouteRequest})
14
+ * into an Android {@link Bundle} matching the key/type contract documented in
15
+ * {@code IBRouterService.aidl}.
16
+ *
17
+ * <p>Every key listed in the AIDL comment block is mapped here. This is the
18
+ * single place where JS-to-AIDL key name translation lives — adding a new
19
+ * param means adding one field to the JS type and one {@code putX} call here.
20
+ */
21
+ public final class ParamMapper {
22
+
23
+ private ParamMapper() {
24
+ // static utility — no instances
25
+ }
26
+
27
+ /**
28
+ * Convert the JS-serialized route request into a Bundle suitable for
29
+ * {@code IBRouterService.getTrackFromParams(Bundle)}.
30
+ */
31
+ @NonNull
32
+ public static Bundle toBundle( @NonNull ReadableMap params ) {
33
+ return toBundle( params, new Bundle() );
34
+ }
35
+
36
+ /**
37
+ * Overload that writes into an existing Bundle (useful for testing
38
+ * with a mocked Bundle).
39
+ */
40
+ @NonNull
41
+ static Bundle toBundle( @NonNull ReadableMap params, @NonNull Bundle b ) {
42
+
43
+ // ── Waypoints ──────────────────────────────────────────────
44
+ // JS pre-serializes waypoints into lonlats, lats, lons, straight
45
+ putString( b, params, "lonlats" );
46
+ putDoubleArray( b, params, "lats" );
47
+ putDoubleArray( b, params, "lons" );
48
+ putString( b, params, "straight" );
49
+
50
+ // ── Profile ────────────────────────────────────────────────
51
+ putString( b, params, "profile" );
52
+ putString( b, params, "remoteProfile" );
53
+
54
+ // ── Vehicle / speed ────────────────────────────────────────
55
+ putString( b, params, "v" );
56
+ if ( params.hasKey( "fast" ) ) {
57
+ b.putInt( "fast", params.getInt( "fast" ) );
58
+ }
59
+
60
+ // ── Output format ──────────────────────────────────────────
61
+ putString( b, params, "trackFormat" );
62
+
63
+ // ── Alternative index ──────────────────────────────────────
64
+ if ( params.hasKey( "alternativeidx" ) ) {
65
+ b.putInt( "alternativeidx", params.getInt( "alternativeidx" ) );
66
+ }
67
+
68
+ // ── Export waypoints ───────────────────────────────────────
69
+ if ( params.hasKey( "exportWaypoints" ) ) {
70
+ b.putInt( "exportWaypoints", params.getInt( "exportWaypoints" ) );
71
+ }
72
+
73
+ // ── Turn instructions ──────────────────────────────────────
74
+ putString( b, params, "turnInstructionFormat" );
75
+ if ( params.hasKey( "timode" ) ) {
76
+ b.putInt( "timode", params.getInt( "timode" ) );
77
+ }
78
+
79
+ // ── Heading / direction ────────────────────────────────────
80
+ if ( params.hasKey( "heading" ) ) {
81
+ b.putDouble( "heading", params.getDouble( "heading" ) );
82
+ }
83
+ if ( params.hasKey( "direction" ) ) {
84
+ b.putDouble( "direction", params.getDouble( "direction" ) );
85
+ }
86
+
87
+ // ── Engine mode (elevation) ────────────────────────────────
88
+ if ( params.hasKey( "engineMode" ) ) {
89
+ b.putInt( "engineMode", params.getInt( "engineMode" ) );
90
+ }
91
+
92
+ // ── Timeout ────────────────────────────────────────────────
93
+ if ( params.hasKey( "maxRunningTime" ) ) {
94
+ b.putString( "maxRunningTime", String.valueOf( params.getInt( "maxRunningTime" ) ) );
95
+ }
96
+
97
+ // ── File output ────────────────────────────────────────────
98
+ putString( b, params, "pathToFileResult" );
99
+
100
+ // ── Compression ────────────────────────────────────────────
101
+ if ( params.hasKey( "acceptCompressedResult" ) ) {
102
+ b.putBoolean( "acceptCompressedResult", params.getBoolean( "acceptCompressedResult" ) );
103
+ }
104
+
105
+ // ── Extra params (profile setup key=value) ─────────────────
106
+ if ( params.hasKey( "extraParams" ) ) {
107
+ ReadableMap extra = params.getMap( "extraParams" );
108
+ if ( extra != null ) {
109
+ Bundle extraBundle = new Bundle();
110
+ com.facebook.react.bridge.ReadableMapKeySetIterator iterator =
111
+ extra.keySetIterator();
112
+ while ( iterator.hasNextKey() ) {
113
+ String key = iterator.nextKey();
114
+ ReadableType type = extra.getType( key );
115
+ if ( type == ReadableType.String ) {
116
+ extraBundle.putString( key, extra.getString( key ) );
117
+ } else if ( type == ReadableType.Number ) {
118
+ extraBundle.putDouble( key, extra.getDouble( key ) );
119
+ } else if ( type == ReadableType.Boolean ) {
120
+ extraBundle.putBoolean( key, extra.getBoolean( key ) );
121
+ }
122
+ }
123
+ b.putBundle( "extraParams", extraBundle );
124
+ }
125
+ }
126
+
127
+ // ── Nogo areas ─────────────────────────────────────────────
128
+ putString( b, params, "nogos" );
129
+ putDoubleArray( b, params, "nogoLats" );
130
+ putDoubleArray( b, params, "nogoLons" );
131
+ putDoubleArray( b, params, "nogoRadi" );
132
+
133
+ // ── Polylines / polygons ──────────────────────────────────
134
+ putString( b, params, "polylines" );
135
+ putString( b, params, "polygons" );
136
+
137
+ // ── POIs ──────────────────────────────────────────────────
138
+ putString( b, params, "pois" );
139
+
140
+ return b;
141
+ }
142
+
143
+ // ── Helpers ────────────────────────────────────────────────────
144
+
145
+ private static void putString(
146
+ @NonNull Bundle b,
147
+ @NonNull ReadableMap params,
148
+ @NonNull String key
149
+ ) {
150
+ if ( params.hasKey( key ) ) {
151
+ String value = params.getString( key );
152
+ if ( value != null ) {
153
+ b.putString( key, value );
154
+ }
155
+ }
156
+ }
157
+
158
+ private static void putDoubleArray(
159
+ @NonNull Bundle b,
160
+ @NonNull ReadableMap params,
161
+ @NonNull String key
162
+ ) {
163
+ if ( params.hasKey( key ) ) {
164
+ ReadableArray arr = params.getArray( key );
165
+ if ( arr != null && arr.size() > 0 ) {
166
+ double[] doubles = new double[ arr.size() ];
167
+ for ( int i = 0; i < arr.size(); i++ ) {
168
+ doubles[ i ] = arr.getDouble( i );
169
+ }
170
+ b.putDoubleArray( key, doubles );
171
+ }
172
+ }
173
+ }
174
+ }