@jacques_gordon/expo-mapbox-navigation 2.0.4 → 2.0.6

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,49 @@ 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
14
8
  import com.mapbox.maps.Style
15
- import com.mapbox.navigation.base.TimeFormat
16
9
  import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions
17
- import com.mapbox.navigation.base.extensions.applyLanguageAndVoiceUnitOptions
18
- import com.mapbox.navigation.base.formatter.DistanceFormatterOptions
19
- import com.mapbox.navigation.base.formatter.UnitType
20
10
  import com.mapbox.navigation.base.options.NavigationOptions
21
11
  import com.mapbox.navigation.base.route.NavigationRoute
12
+ import com.mapbox.navigation.base.route.NavigationRouterCallback
13
+ import com.mapbox.navigation.base.route.RouterFailure
22
14
  import com.mapbox.navigation.base.route.RouterOrigin
23
15
  import com.mapbox.navigation.core.MapboxNavigation
24
16
  import com.mapbox.navigation.core.MapboxNavigationProvider
25
17
  import com.mapbox.navigation.core.directions.session.RoutesObserver
26
- import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp
27
- import com.mapbox.navigation.core.replay.MapboxReplayer
18
+ import com.mapbox.navigation.core.directions.session.RoutesUpdatedResult
28
19
  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
20
+ import com.mapbox.navigation.ui.maps.camera.NavigationCamera
21
+ import com.mapbox.navigation.ui.maps.camera.data.MapboxNavigationViewportDataSource
22
+ import com.mapbox.navigation.ui.maps.camera.transition.NavigationCameraTransitionOptions
23
+ import com.mapbox.navigation.ui.maps.route.line.api.MapboxRouteLineApi
24
+ import com.mapbox.navigation.ui.maps.route.line.api.MapboxRouteLineView
25
+ import com.mapbox.navigation.ui.maps.route.line.model.MapboxRouteLineApiOptions
26
+ import com.mapbox.navigation.ui.maps.route.line.model.MapboxRouteLineViewOptions
33
27
  import expo.modules.kotlin.AppContext
34
28
  import expo.modules.kotlin.views.ExpoView
35
29
  import java.util.Locale
36
30
 
37
31
  private const val TAG = "ExpoMapboxNavigation"
38
32
 
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
33
  class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
55
34
  ExpoView(context, appContext) {
56
35
 
57
- private var navigationView: NavigationView? = null
36
+ private val mapView: MapView = MapView(context)
37
+ private var navigationCamera: NavigationCamera? = null
38
+ private var viewportDataSource: MapboxNavigationViewportDataSource? = null
39
+ private var routeLineApi: MapboxRouteLineApi? = null
40
+ private var routeLineView: MapboxRouteLineView? = null
58
41
  private var mapboxNavigation: MapboxNavigation? = null
59
42
 
60
43
  // Props
61
44
  private var coordinates: List<Map<String, Double>> = emptyList()
62
45
  private var waypointIndices: List<Int>? = null
63
46
  private var language: String? = null
64
- private var voiceUnits: String? = null // "metric" | "imperial" | null
47
+ private var voiceUnits: String? = null
65
48
  private var navigationProfile: String? = null
66
49
  private var excludeTypes: List<String>? = null
67
50
  private var mapStyle: String? = null
@@ -73,230 +56,191 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
73
56
  private var customRasterAboveLayerId: String? = null
74
57
 
75
58
  init {
76
- setupNavigationView()
59
+ mapView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
60
+ addView(mapView)
61
+ setupNavigation()
77
62
  }
78
63
 
79
- private fun setupNavigationView() {
80
- val accessToken = getAccessToken() ?: run {
81
- Log.e(TAG, "Mapbox access token not found. Set it via the config plugin.")
82
- return
83
- }
64
+ private fun setupNavigation() {
65
+ val navigationOptions = NavigationOptions.Builder(context).build()
66
+ mapboxNavigation = MapboxNavigationProvider.create(navigationOptions)
84
67
 
85
- navigationView = NavigationView(context, accessToken).also { navView ->
86
- navView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
87
- addView(navView)
68
+ mapView.getMapboxMap().loadStyle(mapStyle ?: Style.MAPBOX_STREETS) { style ->
69
+ routeLineApi = MapboxRouteLineApi(MapboxRouteLineApiOptions.Builder().build())
70
+ routeLineView = MapboxRouteLineView(MapboxRouteLineViewOptions.Builder(context).build())
88
71
 
89
- navView.addNavigationViewListener(object : NavigationViewListener() {
90
- override fun onDestinationPreviewOptionsVisible(visible: Boolean) {}
72
+ viewportDataSource = MapboxNavigationViewportDataSource(mapView.getMapboxMap())
73
+ navigationCamera = NavigationCamera(
74
+ mapView.getMapboxMap(),
75
+ mapView.camera,
76
+ viewportDataSource!!
77
+ )
78
+ registerObservers()
79
+ }
80
+ }
91
81
 
92
- override fun onFreeDrive() {
93
- sendEvent("onNavigationCancelled", mapOf<String, Any>())
94
- }
82
+ private fun registerObservers() {
83
+ val nav = mapboxNavigation ?: return
95
84
 
96
- override fun onArrival() {
97
- sendEvent("onArrival", mapOf<String, Any>())
85
+ nav.registerRoutesObserver(object : RoutesObserver {
86
+ override fun onRoutesChanged(result: RoutesUpdatedResult) {
87
+ val routes = result.navigationRoutes
88
+ if (routes.isNotEmpty()) {
89
+ routeLineApi?.setNavigationRoutes(routes) { value ->
90
+ mapView.getMapboxMap().getStyle()?.let { style ->
91
+ routeLineView?.renderRouteDrawData(style, value)
92
+ }
93
+ }
94
+ safeCameraFollowing()
95
+ emit("onRoutesReady", mapOf(
96
+ "routeCount" to routes.size,
97
+ "distanceMeters" to (routes.firstOrNull()?.directionsRoute?.distance() ?: 0.0),
98
+ "durationSeconds" to (routes.firstOrNull()?.directionsRoute?.duration() ?: 0.0)
99
+ ))
98
100
  }
101
+ }
102
+ })
99
103
 
100
- override fun onActiveNavigation() {}
101
- override fun onPassiveNavigation() {}
102
- override fun onPreview() {}
103
- override fun onRoutePreview() {}
104
- })
105
- }
104
+ nav.registerRouteProgressObserver(RouteProgressObserver { progress ->
105
+ emit("onRouteProgressChanged", mapOf(
106
+ "distanceRemaining" to progress.distanceRemaining,
107
+ "durationRemaining" to progress.durationRemaining,
108
+ "distanceTraveled" to progress.distanceTraveled,
109
+ "fractionTraveled" to progress.fractionTraveled,
110
+ "currentStepDistanceRemaining" to
111
+ (progress.currentLegProgress?.currentStepProgress?.distanceRemaining ?: 0f)
112
+ ))
113
+ })
106
114
  }
107
115
 
108
- // ─────────────────────────────────────────────────────────────────────────
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.
113
- // ─────────────────────────────────────────────────────────────────────────
114
- private fun safeCameraFollowing(mapboxNavigation: MapboxNavigation) {
116
+ // ── Issue #43: safe camera transition ────────────────────────────────────
117
+ private fun safeCameraFollowing() {
115
118
  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())
119
+ navigationCamera?.requestNavigationCameraToFollowing(
120
+ stateTransitionOptions = NavigationCameraTransitionOptions.Builder()
121
+ .maxDuration(0)
122
+ .build()
123
+ )
124
124
  } catch (e: Exception) {
125
- Log.e(TAG, "Unexpected error requesting navigation camera to following: ${e.message}")
125
+ Log.e(TAG, "Camera transition error (issue #43): ${e.message}")
126
126
  }
127
127
  }
128
128
 
129
- // ─────────────────────────────────────────────────────────────────────────
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.
133
- // ─────────────────────────────────────────────────────────────────────────
134
- private fun resolveUnitType(): UnitType {
129
+ // ── Issue #31: voice units ────────────────────────────────────────────────
130
+ private fun resolveUnitType(): String {
135
131
  return when (voiceUnits?.lowercase()) {
136
- "metric" -> UnitType.METRIC
137
- "imperial" -> UnitType.IMPERIAL
132
+ "metric" -> "metric"
133
+ "imperial" -> "imperial"
138
134
  else -> {
139
- // Auto-detect from device locale (default behaviour)
140
135
  val locale = language?.let { Locale.forLanguageTag(it) } ?: Locale.getDefault()
141
- // Countries that primarily use imperial: US, UK, Liberia, Myanmar
142
136
  val imperialCountries = setOf("US", "GB", "LR", "MM")
143
- if (locale.country in imperialCountries) UnitType.IMPERIAL else UnitType.METRIC
137
+ if (locale.country in imperialCountries) "imperial" else "metric"
144
138
  }
145
139
  }
146
140
  }
147
141
 
148
- private fun buildRouteOptions(): RouteOptions? {
149
- if (coordinates.size < 2) return null
142
+ private fun fetchRoutes() {
143
+ val nav = mapboxNavigation ?: return
144
+ if (coordinates.size < 2) return
150
145
 
151
146
  val points = coordinates.map { coord ->
152
147
  Point.fromLngLat(coord["longitude"] ?: 0.0, coord["latitude"] ?: 0.0)
153
148
  }
154
149
 
155
150
  val locale = language?.let { Locale.forLanguageTag(it) } ?: Locale.getDefault()
156
- val unitType = resolveUnitType()
157
151
 
158
152
  val builder = RouteOptions.builder()
159
153
  .applyDefaultNavigationOptions()
160
- .applyLanguageAndVoiceUnitOptions(context)
161
154
  .language(locale.toLanguageTag())
162
- .voiceUnits(unitType.value) // Issue #31 fix
155
+ .voiceUnits(resolveUnitType())
163
156
  .coordinatesList(points)
164
157
 
165
- // Waypoints
166
- waypointIndices?.let { indices ->
167
- builder.waypointIndicesList(indices)
168
- }
169
-
170
- // Navigation profile
158
+ waypointIndices?.let { builder.waypointIndicesList(it) }
171
159
  navigationProfile?.let {
172
- val profile = if (it.startsWith("mapbox/")) it else "mapbox/$it"
173
- builder.profile(profile)
174
- }
175
-
176
- // Exclude road types
177
- excludeTypes?.takeIf { it.isNotEmpty() }?.let {
178
- builder.exclude(it.joinToString(","))
160
+ builder.profile(if (it.startsWith("mapbox/")) it else "mapbox/$it")
179
161
  }
180
-
181
- // Vehicle restrictions
162
+ excludeTypes?.takeIf { it.isNotEmpty() }?.let { builder.exclude(it.joinToString(",")) }
182
163
  maxHeight?.let { builder.maxHeight(it) }
183
164
  maxWidth?.let { builder.maxWidth(it) }
184
165
 
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
166
+ // Nav SDK v3: NavigationRouterCallback uses @RouterOrigin String (not enum)
167
+ nav.requestRoutes(
168
+ builder.build(),
169
+ object : NavigationRouterCallback {
170
+ override fun onRoutesReady(
171
+ routes: List<NavigationRoute>,
172
+ @RouterOrigin routerOrigin: String
173
+ ) {
174
+ if (routes.isEmpty()) {
175
+ emit("onRoutesFailed", mapOf("message" to "No routes returned"))
176
+ return
177
+ }
178
+ nav.setNavigationRoutes(routes)
179
+ nav.startTripSession()
197
180
  }
198
181
 
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
- }
182
+ override fun onFailure(
183
+ reasons: List<RouterFailure>,
184
+ routeOptions: RouteOptions
185
+ ) {
186
+ val msg = reasons.firstOrNull()?.message ?: "Unknown error"
187
+ Log.e(TAG, "Route request failed: $msg")
188
+ emit("onRoutesFailed", mapOf("message" to msg))
189
+ }
210
190
 
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")))
191
+ override fun onCanceled(
192
+ routeOptions: RouteOptions,
193
+ @RouterOrigin routerOrigin: String
194
+ ) {
195
+ Log.d(TAG, "Route request cancelled")
196
+ }
214
197
  }
215
- })
198
+ )
216
199
  }
217
200
 
218
- // ─────────────────────────────────────────────────────────────────────────
219
- // Prop setters
220
- // ─────────────────────────────────────────────────────────────────────────
221
-
201
+ // ── Prop setters ──────────────────────────────────────────────────────────
222
202
  fun setCoordinates(coords: List<Map<String, Double>>) {
223
203
  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
233
- }
234
-
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
- }
278
-
279
- // ─────────────────────────────────────────────────────────────────────────
280
- // Helpers
281
- // ─────────────────────────────────────────────────────────────────────────
282
-
204
+ if (coords.size >= 2) fetchRoutes()
205
+ }
206
+ fun setWaypointIndices(indices: List<Int>?) { waypointIndices = indices }
207
+ fun setLanguage(lang: String?) { language = lang }
208
+ fun setVoiceUnits(units: String?) { voiceUnits = units }
209
+ fun setNavigationProfile(profile: String?) { navigationProfile = profile }
210
+ fun setExcludeTypes(types: List<String>?) { excludeTypes = types }
211
+ fun setMapStyle(style: String?) { mapStyle = style }
212
+ fun setMute(shouldMute: Boolean) { mute = shouldMute }
213
+ fun setMaxHeight(height: Double?) { maxHeight = height }
214
+ fun setMaxWidth(width: Double?) { maxWidth = width }
215
+ fun setUseMapMatching(use: Boolean) { useMapMatching = use }
216
+ fun setCustomRasterTileUrl(url: String?) { customRasterTileUrl = url }
217
+ fun setCustomRasterAboveLayerId(layerId: String?) { customRasterAboveLayerId = layerId }
218
+
219
+ // ── Helpers ───────────────────────────────────────────────────────────────
283
220
  private fun getAccessToken(): String? {
284
221
  return try {
285
222
  val resId = context.resources.getIdentifier(
286
223
  "mapbox_access_token", "string", context.packageName
287
224
  )
288
225
  if (resId != 0) context.getString(resId) else null
289
- } catch (e: Exception) {
290
- null
291
- }
226
+ } catch (e: Exception) { null }
292
227
  }
293
228
 
294
- private fun sendEvent(name: String, data: Map<String, Any>) {
295
- appContext.eventEmitter?.emit(name, data)
229
+ // Correct Expo SDK v53 event emitter API
230
+ private fun emit(name: String, data: Map<String, Any>) {
231
+ try {
232
+ appContext.eventEmitter?.emit(name, data)
233
+ } catch (e: Exception) {
234
+ Log.e(TAG, "Failed to emit event $name: ${e.message}")
235
+ }
296
236
  }
297
237
 
298
238
  override fun onDetachedFromWindow() {
299
239
  super.onDetachedFromWindow()
300
- mapboxNavigation?.onDestroy()
240
+ mapboxNavigation?.stopTripSession()
241
+ MapboxNavigationProvider.destroy()
242
+ routeLineApi?.cancel()
243
+ mapView.onStop()
244
+ mapView.onDestroy()
301
245
  }
302
246
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jacques_gordon/expo-mapbox-navigation",
3
- "version": "2.0.4",
3
+ "version": "2.0.6",
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",