@jacques_gordon/expo-mapbox-navigation 2.0.3 → 2.0.5

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.
@@ -2,66 +2,55 @@ package expo.modules.mapboxnavigation
2
2
 
3
3
  import android.content.Context
4
4
  import android.util.Log
5
- import androidx.lifecycle.DefaultLifecycleObserver
6
- import androidx.lifecycle.LifecycleOwner
7
5
  import com.mapbox.api.directions.v5.models.RouteOptions
8
- import com.mapbox.common.location.AccuracyLevel
9
- import com.mapbox.common.location.DeviceLocationProviderFactory
10
- import com.mapbox.common.location.IntervalSettings
11
- import com.mapbox.common.location.LocationProviderRequest
12
6
  import com.mapbox.geojson.Point
13
7
  import com.mapbox.maps.MapView
8
+ import com.mapbox.maps.MapboxMap
14
9
  import com.mapbox.maps.Style
15
- import com.mapbox.navigation.base.TimeFormat
10
+ import com.mapbox.maps.plugin.animation.camera
16
11
  import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions
17
- import com.mapbox.navigation.base.extensions.applyLanguageAndVoiceUnitOptions
18
- import com.mapbox.navigation.base.formatter.DistanceFormatterOptions
19
12
  import com.mapbox.navigation.base.formatter.UnitType
20
13
  import com.mapbox.navigation.base.options.NavigationOptions
21
14
  import com.mapbox.navigation.base.route.NavigationRoute
22
- import com.mapbox.navigation.base.route.RouterOrigin
15
+ import com.mapbox.navigation.base.route.NavigationRouterCallback
16
+ import com.mapbox.navigation.base.route.RouterFailure
23
17
  import com.mapbox.navigation.core.MapboxNavigation
24
18
  import com.mapbox.navigation.core.MapboxNavigationProvider
25
19
  import com.mapbox.navigation.core.directions.session.RoutesObserver
20
+ import com.mapbox.navigation.core.directions.session.RoutesUpdatedResult
26
21
  import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp
27
- import com.mapbox.navigation.core.replay.MapboxReplayer
28
22
  import com.mapbox.navigation.core.trip.session.RouteProgressObserver
29
- import com.mapbox.navigation.core.trip.session.VoiceInstructionsObserver
30
- import com.mapbox.navigation.dropin.NavigationView
31
- import com.mapbox.navigation.dropin.NavigationViewListener
32
- import com.mapbox.navigation.ui.base.util.MapboxNavigationConsumer
23
+ import com.mapbox.navigation.ui.maps.camera.NavigationCamera
24
+ import com.mapbox.navigation.ui.maps.camera.data.MapboxNavigationViewportDataSource
25
+ import com.mapbox.navigation.ui.maps.camera.transition.NavigationCameraTransitionOptions
26
+ import com.mapbox.navigation.ui.maps.route.line.api.MapboxRouteLineApi
27
+ import com.mapbox.navigation.ui.maps.route.line.api.MapboxRouteLineView
28
+ import com.mapbox.navigation.ui.maps.route.line.model.MapboxRouteLineApiOptions
29
+ import com.mapbox.navigation.ui.maps.route.line.model.MapboxRouteLineViewOptions
33
30
  import expo.modules.kotlin.AppContext
34
31
  import expo.modules.kotlin.views.ExpoView
35
32
  import java.util.Locale
36
33
 
37
34
  private const val TAG = "ExpoMapboxNavigation"
38
35
 
39
- /**
40
- * ExpoMapboxNavigationView — Full-featured navigation view wrapping the Mapbox Drop-In UI.
41
- *
42
- * Fixes:
43
- * - Issue #43: CameraAnimationsUtils.calculateCameraAnimationHint NoSuchMethodError
44
- * Root cause: version mismatch between Navigation SDK and Maps SDK.
45
- * Fix: build.gradle now pins mapbox-maps-android to ≥ 11.11.0 and navigation to 3.7.0,
46
- * which ensures the method signature exists at runtime. The camera call is also wrapped
47
- * in a try/catch to degrade gracefully if ever encountered.
48
- *
49
- * - Issue #31: Voice units always imperial regardless of device locale
50
- * Fix: new `voiceUnits` prop ("metric" | "imperial" | null) exposed via module.
51
- * The value is forwarded to RouteOptions via applyLanguageAndVoiceUnitOptions
52
- * with an explicit UnitType override.
53
- */
54
36
  class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
55
37
  ExpoView(context, appContext) {
56
38
 
57
- private var navigationView: NavigationView? = null
39
+ // ── UI ───────────────────────────────────────────────────────────────────
40
+ private val mapView: MapView = MapView(context)
41
+ private var navigationCamera: NavigationCamera? = null
42
+ private var viewportDataSource: MapboxNavigationViewportDataSource? = null
43
+ private var routeLineApi: MapboxRouteLineApi? = null
44
+ private var routeLineView: MapboxRouteLineView? = null
45
+
46
+ // ── Navigation ───────────────────────────────────────────────────────────
58
47
  private var mapboxNavigation: MapboxNavigation? = null
59
48
 
60
- // Props
49
+ // ── Props ────────────────────────────────────────────────────────────────
61
50
  private var coordinates: List<Map<String, Double>> = emptyList()
62
51
  private var waypointIndices: List<Int>? = null
63
52
  private var language: String? = null
64
- private var voiceUnits: String? = null // "metric" | "imperial" | null
53
+ private var voiceUnits: String? = null
65
54
  private var navigationProfile: String? = null
66
55
  private var excludeTypes: List<String>? = null
67
56
  private var mapStyle: String? = null
@@ -73,80 +62,125 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
73
62
  private var customRasterAboveLayerId: String? = null
74
63
 
75
64
  init {
76
- setupNavigationView()
65
+ mapView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
66
+ addView(mapView)
67
+ setupNavigation()
77
68
  }
78
69
 
79
- private fun setupNavigationView() {
70
+ // ─────────────────────────────────────────────────────────────────────────
71
+ // Setup
72
+ // ─────────────────────────────────────────────────────────────────────────
73
+
74
+ private fun setupNavigation() {
80
75
  val accessToken = getAccessToken() ?: run {
81
- Log.e(TAG, "Mapbox access token not found. Set it via the config plugin.")
76
+ Log.e(TAG, "Mapbox access token not found.")
82
77
  return
83
78
  }
84
79
 
85
- navigationView = NavigationView(context, accessToken).also { navView ->
86
- navView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
87
- addView(navView)
80
+ // Create MapboxNavigation instance (Nav SDK v3 API)
81
+ val navigationOptions = NavigationOptions.Builder(context)
82
+ .build()
88
83
 
89
- navView.addNavigationViewListener(object : NavigationViewListener() {
90
- override fun onDestinationPreviewOptionsVisible(visible: Boolean) {}
84
+ mapboxNavigation = MapboxNavigationProvider.create(navigationOptions)
91
85
 
92
- override fun onFreeDrive() {
93
- sendEvent("onNavigationCancelled", mapOf<String, Any>())
94
- }
86
+ // Setup map style + route line + camera once map is loaded
87
+ mapView.getMapboxMap().loadStyle(
88
+ mapStyle ?: Style.MAPBOX_STREETS
89
+ ) { style ->
90
+ setupRouteLineComponents(style)
91
+ setupCamera()
92
+ registerObservers()
93
+ }
94
+ }
95
95
 
96
- override fun onArrival() {
97
- sendEvent("onArrival", mapOf<String, Any>())
96
+ private fun setupRouteLineComponents(style: Style) {
97
+ routeLineApi = MapboxRouteLineApi(MapboxRouteLineApiOptions.Builder().build())
98
+ routeLineView = MapboxRouteLineView(MapboxRouteLineViewOptions.Builder(context).build())
99
+ }
100
+
101
+ private fun setupCamera() {
102
+ val mapboxMap = mapView.getMapboxMap()
103
+ viewportDataSource = MapboxNavigationViewportDataSource(mapboxMap)
104
+ navigationCamera = NavigationCamera(
105
+ mapboxMap,
106
+ mapView.camera,
107
+ viewportDataSource!!
108
+ )
109
+ }
110
+
111
+ private fun registerObservers() {
112
+ val nav = mapboxNavigation ?: return
113
+
114
+ // Routes observer — updates route line on map
115
+ nav.registerRoutesObserver(object : RoutesObserver {
116
+ override fun onRoutesChanged(result: RoutesUpdatedResult) {
117
+ val routes = result.navigationRoutes
118
+ if (routes.isNotEmpty()) {
119
+ routeLineApi?.setNavigationRoutes(routes) { value ->
120
+ mapView.getMapboxMap().getStyle()?.let { style ->
121
+ routeLineView?.renderRouteDrawData(style, value)
122
+ }
123
+ }
124
+ // Issue #43 fix: safe camera transition to following
125
+ safeCameraFollowing()
126
+
127
+ sendEvent("onRoutesReady", mapOf(
128
+ "routeCount" to routes.size,
129
+ "distanceMeters" to (routes.firstOrNull()?.directionsRoute?.distance() ?: 0.0),
130
+ "durationSeconds" to (routes.firstOrNull()?.directionsRoute?.duration() ?: 0.0)
131
+ ))
98
132
  }
133
+ }
134
+ })
99
135
 
100
- override fun onActiveNavigation() {}
101
- override fun onPassiveNavigation() {}
102
- override fun onPreview() {}
103
- override fun onRoutePreview() {}
104
- })
105
- }
136
+ // Route progress observer
137
+ nav.registerRouteProgressObserver(RouteProgressObserver { progress ->
138
+ sendEvent("onRouteProgressChanged", mapOf(
139
+ "distanceRemaining" to progress.distanceRemaining,
140
+ "durationRemaining" to progress.durationRemaining,
141
+ "distanceTraveled" to progress.distanceTraveled,
142
+ "fractionTraveled" to progress.fractionTraveled,
143
+ "currentStepDistanceRemaining" to (progress.currentLegProgress?.currentStepProgress?.distanceRemaining ?: 0f)
144
+ ))
145
+ })
106
146
  }
107
147
 
108
148
  // ─────────────────────────────────────────────────────────────────────────
109
- // ISSUE #43 FIX: Safe camera request
110
- // Wraps requestNavigationCameraToFollowing in try/catch to handle any
111
- // residual version mismatch at runtime without crashing the app.
112
- // The real fix is the version pinning in build.gradle.
149
+ // Issue #43 fix: safe camera transition
113
150
  // ─────────────────────────────────────────────────────────────────────────
114
- private fun safeCameraFollowing(mapboxNavigation: MapboxNavigation) {
151
+ private fun safeCameraFollowing() {
115
152
  try {
116
- mapboxNavigation.navigationCamera?.requestNavigationCameraToFollowing()
117
- } catch (e: NoSuchMethodError) {
118
- Log.e(TAG, """
119
- [Issue #43] CameraAnimationsUtils.calculateCameraAnimationHint not found.
120
- This indicates a Mapbox Maps SDK version mismatch.
121
- Ensure mapbox-maps-android >= 11.11.0 is resolved in your gradle dependencies.
122
- Error: ${e.message}
123
- """.trimIndent())
153
+ navigationCamera?.requestNavigationCameraToFollowing(
154
+ stateTransitionOptions = NavigationCameraTransitionOptions.Builder()
155
+ .maxDuration(0)
156
+ .build()
157
+ )
124
158
  } catch (e: Exception) {
125
- Log.e(TAG, "Unexpected error requesting navigation camera to following: ${e.message}")
159
+ Log.e(TAG, "Camera transition error (issue #43): ${e.message}")
126
160
  }
127
161
  }
128
162
 
129
163
  // ─────────────────────────────────────────────────────────────────────────
130
- // ISSUE #31 FIX: voiceUnits / distance units
131
- // Maps the string prop value to the Mapbox UnitType enum and applies it
132
- // to RouteOptions via applyLanguageAndVoiceUnitOptions.
164
+ // Issue #31 fix: voice units
133
165
  // ─────────────────────────────────────────────────────────────────────────
134
- private fun resolveUnitType(): UnitType {
166
+ private fun resolveUnitType(): String {
135
167
  return when (voiceUnits?.lowercase()) {
136
- "metric" -> UnitType.METRIC
137
- "imperial" -> UnitType.IMPERIAL
168
+ "metric" -> "metric"
169
+ "imperial" -> "imperial"
138
170
  else -> {
139
- // Auto-detect from device locale (default behaviour)
140
171
  val locale = language?.let { Locale.forLanguageTag(it) } ?: Locale.getDefault()
141
- // Countries that primarily use imperial: US, UK, Liberia, Myanmar
142
172
  val imperialCountries = setOf("US", "GB", "LR", "MM")
143
- if (locale.country in imperialCountries) UnitType.IMPERIAL else UnitType.METRIC
173
+ if (locale.country in imperialCountries) "imperial" else "metric"
144
174
  }
145
175
  }
146
176
  }
147
177
 
148
- private fun buildRouteOptions(): RouteOptions? {
149
- if (coordinates.size < 2) return null
178
+ // ─────────────────────────────────────────────────────────────────────────
179
+ // Route request
180
+ // ─────────────────────────────────────────────────────────────────────────
181
+ private fun fetchRoutes() {
182
+ val nav = mapboxNavigation ?: return
183
+ if (coordinates.size < 2) return
150
184
 
151
185
  val points = coordinates.map { coord ->
152
186
  Point.fromLngLat(coord["longitude"] ?: 0.0, coord["latitude"] ?: 0.0)
@@ -157,62 +191,56 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
157
191
 
158
192
  val builder = RouteOptions.builder()
159
193
  .applyDefaultNavigationOptions()
160
- .applyLanguageAndVoiceUnitOptions(context)
161
194
  .language(locale.toLanguageTag())
162
- .voiceUnits(unitType.value) // Issue #31 fix
195
+ .voiceUnits(unitType) // Issue #31 fix
163
196
  .coordinatesList(points)
164
197
 
165
- // Waypoints
166
- waypointIndices?.let { indices ->
167
- builder.waypointIndicesList(indices)
168
- }
198
+ waypointIndices?.let { builder.waypointIndicesList(it) }
169
199
 
170
- // Navigation profile
171
200
  navigationProfile?.let {
172
201
  val profile = if (it.startsWith("mapbox/")) it else "mapbox/$it"
173
202
  builder.profile(profile)
174
203
  }
175
204
 
176
- // Exclude road types
177
205
  excludeTypes?.takeIf { it.isNotEmpty() }?.let {
178
206
  builder.exclude(it.joinToString(","))
179
207
  }
180
208
 
181
- // Vehicle restrictions
182
209
  maxHeight?.let { builder.maxHeight(it) }
183
210
  maxWidth?.let { builder.maxWidth(it) }
184
211
 
185
- return builder.build()
186
- }
187
-
188
- private fun fetchAndStartRoutes() {
189
- val nav = mapboxNavigation ?: return
190
- val options = buildRouteOptions() ?: return
191
-
192
- nav.requestRoutes(options, object : MapboxNavigationConsumer<List<NavigationRoute>> {
193
- override fun accept(value: List<NavigationRoute>) {
194
- if (value.isEmpty()) {
195
- sendEvent("onRoutesFailed", mapOf("message" to "No routes returned"))
196
- return
212
+ nav.requestRoutes(
213
+ builder.build(),
214
+ object : NavigationRouterCallback {
215
+ override fun onRoutesReady(
216
+ routes: List<NavigationRoute>,
217
+ routerOrigin: com.mapbox.navigation.base.route.RouterOrigin
218
+ ) {
219
+ if (routes.isEmpty()) {
220
+ sendEvent("onRoutesFailed", mapOf("message" to "No routes returned"))
221
+ return
222
+ }
223
+ nav.setNavigationRoutes(routes)
224
+ nav.startTripSession()
197
225
  }
198
226
 
199
- sendEvent("onRoutesReady", mapOf(
200
- "routeCount" to value.size,
201
- "distanceMeters" to (value.firstOrNull()?.directionsRoute?.distance() ?: 0.0),
202
- "durationSeconds" to (value.firstOrNull()?.directionsRoute?.duration() ?: 0.0)
203
- ))
204
-
205
- nav.setNavigationRoutes(value)
206
-
207
- // Issue #43 fix: safe camera transition
208
- safeCameraFollowing(nav)
209
- }
227
+ override fun onFailure(
228
+ reasons: List<RouterFailure>,
229
+ routeOptions: RouteOptions
230
+ ) {
231
+ val msg = reasons.firstOrNull()?.message ?: "Unknown error"
232
+ Log.e(TAG, "Route request failed: $msg")
233
+ sendEvent("onRoutesFailed", mapOf("message" to msg))
234
+ }
210
235
 
211
- override fun accept(error: Exception) {
212
- Log.e(TAG, "Route request failed: ${error.message}")
213
- sendEvent("onRoutesFailed", mapOf("message" to (error.message ?: "Unknown error")))
236
+ override fun onCanceled(
237
+ routeOptions: RouteOptions,
238
+ routerOrigin: com.mapbox.navigation.base.route.RouterOrigin
239
+ ) {
240
+ Log.d(TAG, "Route request cancelled")
241
+ }
214
242
  }
215
- })
243
+ )
216
244
  }
217
245
 
218
246
  // ─────────────────────────────────────────────────────────────────────────
@@ -221,60 +249,21 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
221
249
 
222
250
  fun setCoordinates(coords: List<Map<String, Double>>) {
223
251
  coordinates = coords
224
- if (coords.size >= 2) fetchAndStartRoutes()
225
- }
226
-
227
- fun setWaypointIndices(indices: List<Int>?) {
228
- waypointIndices = indices
229
- }
230
-
231
- fun setLanguage(lang: String?) {
232
- language = lang
252
+ if (coords.size >= 2) fetchRoutes()
233
253
  }
234
254
 
235
- /** Issue #31 — Set voice/distance units: "metric", "imperial", or null for auto */
236
- fun setVoiceUnits(units: String?) {
237
- voiceUnits = units
238
- }
239
-
240
- fun setNavigationProfile(profile: String?) {
241
- navigationProfile = profile
242
- }
243
-
244
- fun setExcludeTypes(types: List<String>?) {
245
- excludeTypes = types
246
- }
247
-
248
- fun setMapStyle(style: String?) {
249
- mapStyle = style
250
- }
251
-
252
- fun setMute(shouldMute: Boolean) {
253
- mute = shouldMute
254
- navigationView?.customizeViewStyles {
255
- // Audio mute is handled by the NavigationView API
256
- }
257
- }
258
-
259
- fun setMaxHeight(height: Double?) {
260
- maxHeight = height
261
- }
262
-
263
- fun setMaxWidth(width: Double?) {
264
- maxWidth = width
265
- }
266
-
267
- fun setUseMapMatching(use: Boolean) {
268
- useMapMatching = use
269
- }
270
-
271
- fun setCustomRasterTileUrl(url: String?) {
272
- customRasterTileUrl = url
273
- }
274
-
275
- fun setCustomRasterAboveLayerId(layerId: String?) {
276
- customRasterAboveLayerId = layerId
277
- }
255
+ fun setWaypointIndices(indices: List<Int>?) { waypointIndices = indices }
256
+ fun setLanguage(lang: String?) { language = lang }
257
+ fun setVoiceUnits(units: String?) { voiceUnits = units }
258
+ fun setNavigationProfile(profile: String?) { navigationProfile = profile }
259
+ fun setExcludeTypes(types: List<String>?) { excludeTypes = types }
260
+ fun setMapStyle(style: String?) { mapStyle = style }
261
+ fun setMute(shouldMute: Boolean) { mute = shouldMute }
262
+ fun setMaxHeight(height: Double?) { maxHeight = height }
263
+ fun setMaxWidth(width: Double?) { maxWidth = width }
264
+ fun setUseMapMatching(use: Boolean) { useMapMatching = use }
265
+ fun setCustomRasterTileUrl(url: String?) { customRasterTileUrl = url }
266
+ fun setCustomRasterAboveLayerId(layerId: String?) { customRasterAboveLayerId = layerId }
278
267
 
279
268
  // ─────────────────────────────────────────────────────────────────────────
280
269
  // Helpers
@@ -286,9 +275,7 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
286
275
  "mapbox_access_token", "string", context.packageName
287
276
  )
288
277
  if (resId != 0) context.getString(resId) else null
289
- } catch (e: Exception) {
290
- null
291
- }
278
+ } catch (e: Exception) { null }
292
279
  }
293
280
 
294
281
  private fun sendEvent(name: String, data: Map<String, Any>) {
@@ -297,6 +284,10 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
297
284
 
298
285
  override fun onDetachedFromWindow() {
299
286
  super.onDetachedFromWindow()
300
- mapboxNavigation?.onDestroy()
287
+ mapboxNavigation?.stopTripSession()
288
+ MapboxNavigationProvider.destroy()
289
+ routeLineApi?.cancel()
290
+ mapView.onStop()
291
+ mapView.onDestroy()
301
292
  }
302
293
  }
package/app.plugin.js CHANGED
@@ -58,19 +58,6 @@ const withMapboxNavigation = (config, options = {}) => {
58
58
  }
59
59
  }
60
60
 
61
- // Add Notifee Maven repo if not already present
62
- // Fixes: "Could not find any matches for app.notifee:core:+"
63
- if (!contents.includes('dl.cloudsmith.io/public/notifee')) {
64
- const notifeeRepo = `
65
- maven { url 'https://dl.cloudsmith.io/public/notifee/notifee/maven/' }`;
66
-
67
- if (contents.includes('allprojects') && contents.includes('repositories')) {
68
- contents = contents.replace(
69
- /allprojects\s*\{[\s\S]*?repositories\s*\{/,
70
- (match) => match + notifeeRepo
71
- );
72
- }
73
- }
74
61
 
75
62
  mod.modResults.contents = contents;
76
63
  return mod;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jacques_gordon/expo-mapbox-navigation",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
4
4
  "description": "Expo module for Mapbox Navigation SDK with 16KB page size support, NDK27, and Mapbox Maps v11.11.0+",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -58,19 +58,6 @@ const withMapboxNavigation = (config, options = {}) => {
58
58
  }
59
59
  }
60
60
 
61
- // Add Notifee Maven repo if not already present
62
- // Fixes: "Could not find any matches for app.notifee:core:+"
63
- if (!contents.includes('dl.cloudsmith.io/public/notifee')) {
64
- const notifeeRepo = `
65
- maven { url 'https://dl.cloudsmith.io/public/notifee/notifee/maven/' }`;
66
-
67
- if (contents.includes('allprojects') && contents.includes('repositories')) {
68
- contents = contents.replace(
69
- /allprojects\s*\{[\s\S]*?repositories\s*\{/,
70
- (match) => match + notifeeRepo
71
- );
72
- }
73
- }
74
61
 
75
62
  mod.modResults.contents = contents;
76
63
  return mod;