@jacques_gordon/expo-mapbox-navigation 2.0.9 → 2.1.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.
@@ -13,12 +13,6 @@ android {
13
13
  defaultConfig {
14
14
  minSdkVersion safeExtGet("minSdkVersion", 24)
15
15
  targetSdkVersion safeExtGet("targetSdkVersion", 35)
16
-
17
- externalNativeBuild {
18
- cmake {
19
- arguments "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
20
- }
21
- }
22
16
  ndk {
23
17
  abiFilters "arm64-v8a", "x86_64"
24
18
  }
@@ -63,22 +57,26 @@ dependencies {
63
57
  implementation project(':expo-modules-core')
64
58
  implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${safeExtGet('kotlinVersion', '1.9.0')}"
65
59
 
66
- // Mapbox Navigation SDK v3 core
60
+ // Mapbox Navigation SDK v3
67
61
  implementation "com.mapbox.navigationcore:android:${MAPBOX_NAV_VERSION}"
68
62
 
69
- // Mapbox Maps SDK (pinned to minimum version)
63
+ // Mapbox Maps SDK
70
64
  implementation("com.mapbox.maps:android:${MAPBOX_MAPS_MIN_VERSION}") {
71
65
  transitive = true
72
66
  }
73
67
 
74
- // ── Navigation UI Components (Waze-style UI) ─────────────────────────────
75
- // MapboxManeuverView — turn-by-turn banner
68
+ // Navigation UI Components (ManeuverView, SpeedInfoView)
76
69
  implementation "com.mapbox.navigationcore:ui-components:${MAPBOX_NAV_VERSION}"
77
- // MapboxSpeedInfoView — speed limit + current speed
70
+
71
+ // TripData (ManeuverApi, SpeedInfoApi, TripProgressApi)
78
72
  implementation "com.mapbox.navigationcore:tripdata:${MAPBOX_NAV_VERSION}"
79
- // Trip progress API — ETA, distance, duration
73
+
74
+ // UI Maps (NavigationStyles, NavigationLocationProvider, NavigationCamera)
80
75
  implementation "com.mapbox.navigationcore:ui-maps:${MAPBOX_NAV_VERSION}"
81
76
 
77
+ // ConstraintLayout — required because MapboxManeuverView extends ConstraintLayout
78
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
79
+
82
80
  // AndroidX
83
81
  implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
84
82
  implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
@@ -3,20 +3,19 @@ package expo.modules.mapboxnavigation
3
3
  import android.annotation.SuppressLint
4
4
  import android.content.Context
5
5
  import android.graphics.Color
6
- import android.os.Build
7
6
  import android.util.Log
8
7
  import android.view.Gravity
9
8
  import android.view.View
10
9
  import android.widget.FrameLayout
11
10
  import android.widget.LinearLayout
12
11
  import android.widget.TextView
13
- import androidx.core.content.ContextCompat
14
12
  import com.mapbox.api.directions.v5.models.RouteOptions
13
+ import com.mapbox.common.location.Location
15
14
  import com.mapbox.geojson.Point
16
15
  import com.mapbox.maps.MapView
17
- import com.mapbox.maps.Style
18
16
  import com.mapbox.maps.plugin.animation.camera
19
17
  import com.mapbox.maps.plugin.locationcomponent.location
18
+ import com.mapbox.navigation.base.TimeFormat
20
19
  import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions
21
20
  import com.mapbox.navigation.base.formatter.DistanceFormatterOptions
22
21
  import com.mapbox.navigation.base.options.NavigationOptions
@@ -28,6 +27,7 @@ import com.mapbox.navigation.core.MapboxNavigation
28
27
  import com.mapbox.navigation.core.MapboxNavigationProvider
29
28
  import com.mapbox.navigation.core.directions.session.RoutesObserver
30
29
  import com.mapbox.navigation.core.directions.session.RoutesUpdatedResult
30
+ import com.mapbox.navigation.core.formatter.MapboxDistanceFormatter
31
31
  import com.mapbox.navigation.core.trip.session.LocationMatcherResult
32
32
  import com.mapbox.navigation.core.trip.session.LocationObserver
33
33
  import com.mapbox.navigation.core.trip.session.RouteProgressObserver
@@ -47,6 +47,9 @@ import com.mapbox.navigation.ui.maps.camera.NavigationCamera
47
47
  import com.mapbox.navigation.ui.maps.camera.data.MapboxNavigationViewportDataSource
48
48
  import com.mapbox.navigation.ui.maps.camera.transition.NavigationCameraTransitionOptions
49
49
  import com.mapbox.navigation.ui.maps.location.NavigationLocationProvider
50
+ import com.mapbox.navigation.ui.maps.route.arrow.api.MapboxRouteArrowApi
51
+ import com.mapbox.navigation.ui.maps.route.arrow.api.MapboxRouteArrowView
52
+ import com.mapbox.navigation.ui.maps.route.arrow.model.RouteArrowOptions
50
53
  import com.mapbox.navigation.ui.maps.route.line.api.MapboxRouteLineApi
51
54
  import com.mapbox.navigation.ui.maps.route.line.api.MapboxRouteLineView
52
55
  import com.mapbox.navigation.ui.maps.route.line.model.MapboxRouteLineApiOptions
@@ -55,6 +58,7 @@ import expo.modules.kotlin.AppContext
55
58
  import expo.modules.kotlin.viewevent.EventDispatcher
56
59
  import expo.modules.kotlin.views.ExpoView
57
60
  import java.util.Calendar
61
+ import java.util.Date
58
62
  import java.util.Locale
59
63
 
60
64
  private const val TAG = "ExpoMapboxNavigation"
@@ -70,31 +74,28 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
70
74
  private val onRoutesFailed by EventDispatcher()
71
75
  private val onArrival by EventDispatcher()
72
76
 
73
- // ── Core views ────────────────────────────────────────────────────────────
77
+ // ── Views ─────────────────────────────────────────────────────────────────
74
78
  private val mapView: MapView = MapView(context)
75
-
76
- // ── Overlay UI views (Waze-style) ─────────────────────────────────────────
77
- private lateinit var maneuverView: MapboxManeuverView
78
- private lateinit var speedInfoView: MapboxSpeedInfoView
79
- private lateinit var tripProgressView: MapboxTripProgressView
80
- private lateinit var etaBottomBar: LinearLayout
81
- private lateinit var tvEtaTime: TextView
82
- private lateinit var tvDuration: TextView
83
- private lateinit var tvDistance: TextView
84
- private lateinit var btnCancel: TextView
79
+ private var maneuverView: MapboxManeuverView? = null
80
+ private var speedInfoView: MapboxSpeedInfoView? = null
81
+ private var tripProgressView: MapboxTripProgressView? = null
82
+ private var tvEtaTime: TextView? = null
83
+ private var tvDuration: TextView? = null
84
+ private var tvDistance: TextView? = null
85
+ private var etaBar: LinearLayout? = null
85
86
 
86
87
  // ── Navigation APIs ───────────────────────────────────────────────────────
87
- private var navigationCamera: NavigationCamera? = null
88
- private var viewportDataSource: MapboxNavigationViewportDataSource? = null
89
- private var routeLineApi: MapboxRouteLineApi? = null
90
- private var routeLineView: MapboxRouteLineView? = null
91
- private var mapboxNavigation: MapboxNavigation? = null
92
- private var maneuverApi: MapboxManeuverApi? = null
93
- private var speedInfoApi: MapboxSpeedInfoApi? = null
94
- private var tripProgressApi: MapboxTripProgressApi? = null
88
+ private lateinit var navigationCamera: NavigationCamera
89
+ private lateinit var viewportDataSource: MapboxNavigationViewportDataSource
90
+ private lateinit var routeLineApi: MapboxRouteLineApi
91
+ private lateinit var routeLineView: MapboxRouteLineView
92
+ private lateinit var maneuverApi: MapboxManeuverApi
93
+ private lateinit var tripProgressApi: MapboxTripProgressApi
94
+ private lateinit var speedInfoApi: MapboxSpeedInfoApi
95
+ private val routeArrowApi = MapboxRouteArrowApi()
96
+ private lateinit var routeArrowView: MapboxRouteArrowView
95
97
  private val navigationLocationProvider = NavigationLocationProvider()
96
-
97
- // ── Night mode state ──────────────────────────────────────────────────────
98
+ private var mapboxNavigation: MapboxNavigation? = null
98
99
  private var isNightMode = false
99
100
 
100
101
  // ── Props ─────────────────────────────────────────────────────────────────
@@ -113,134 +114,123 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
113
114
  private var customRasterAboveLayerId: String? = null
114
115
 
115
116
  init {
116
- buildOverlayUI()
117
+ buildUI()
118
+ initAPIs()
117
119
  setupNavigation()
118
120
  }
119
121
 
120
122
  // ─────────────────────────────────────────────────────────────────────────
121
- // Build the Waze-style overlay UI programmatically
123
+ // Build UI
122
124
  // ─────────────────────────────────────────────────────────────────────────
123
- private fun buildOverlayUI() {
125
+ private fun buildUI() {
126
+ val dp = context.resources.displayMetrics.density
124
127
  val root = FrameLayout(context).apply {
125
128
  layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
126
129
  }
127
130
 
128
- // MapView full screen
129
- mapView.layoutParams = FrameLayout.LayoutParams(
131
+ root.addView(mapView, FrameLayout.LayoutParams(
130
132
  FrameLayout.LayoutParams.MATCH_PARENT,
131
133
  FrameLayout.LayoutParams.MATCH_PARENT
132
- )
133
- root.addView(mapView)
134
+ ))
134
135
 
135
- val dp = context.resources.displayMetrics.density
136
+ // ManeuverView top (cast to View: extends ConstraintLayout)
137
+ val mv = MapboxManeuverView(context)
138
+ root.addView(mv as View, FrameLayout.LayoutParams(
139
+ FrameLayout.LayoutParams.MATCH_PARENT,
140
+ FrameLayout.LayoutParams.WRAP_CONTENT
141
+ ).also { it.gravity = Gravity.TOP })
142
+ mv.visibility = View.INVISIBLE
143
+ maneuverView = mv
144
+
145
+ // SpeedInfoView — bottom-left (cast to View)
146
+ val siv = MapboxSpeedInfoView(context)
147
+ root.addView(siv as View, FrameLayout.LayoutParams(
148
+ FrameLayout.LayoutParams.WRAP_CONTENT,
149
+ FrameLayout.LayoutParams.WRAP_CONTENT
150
+ ).also {
151
+ it.gravity = Gravity.BOTTOM or Gravity.START
152
+ it.setMargins((16 * dp).toInt(), 0, 0, (100 * dp).toInt())
153
+ })
154
+ siv.visibility = View.GONE
155
+ speedInfoView = siv
136
156
 
137
- // ── Maneuver banner (top) ─────────────────────────────────────────────
138
- maneuverView = MapboxManeuverView(context).apply {
139
- layoutParams = FrameLayout.LayoutParams(
140
- FrameLayout.LayoutParams.MATCH_PARENT,
141
- FrameLayout.LayoutParams.WRAP_CONTENT
142
- ).also { it.gravity = Gravity.TOP }
143
- visibility = View.GONE
144
- }
145
- root.addView(maneuverView)
146
-
147
- // ── Speed info (bottom-left, Waze style) ──────────────────────────────
148
- speedInfoView = MapboxSpeedInfoView(context).apply {
149
- layoutParams = FrameLayout.LayoutParams(
150
- FrameLayout.LayoutParams.WRAP_CONTENT,
151
- FrameLayout.LayoutParams.WRAP_CONTENT
152
- ).also {
153
- it.gravity = Gravity.BOTTOM or Gravity.START
154
- it.setMargins(
155
- (16 * dp).toInt(), 0,
156
- 0, (120 * dp).toInt()
157
- )
158
- }
159
- visibility = View.GONE
160
- }
161
- root.addView(speedInfoView)
157
+ // TripProgressView (used by the official Mapbox render method)
158
+ val tpv = MapboxTripProgressView(context)
159
+ tripProgressView = tpv
162
160
 
163
- // ── ETA bottom bar ────────────────────────────────────────────────────
164
- etaBottomBar = LinearLayout(context).apply {
161
+ // Bottom ETA bar
162
+ val bar = LinearLayout(context).apply {
165
163
  orientation = LinearLayout.HORIZONTAL
166
164
  setBackgroundColor(Color.WHITE)
167
165
  elevation = 8 * dp
168
- layoutParams = FrameLayout.LayoutParams(
169
- FrameLayout.LayoutParams.MATCH_PARENT,
170
- (80 * dp).toInt()
171
- ).also { it.gravity = Gravity.BOTTOM }
166
+ visibility = View.INVISIBLE
172
167
  gravity = Gravity.CENTER_VERTICAL
173
168
  setPadding(
174
169
  (16 * dp).toInt(), (8 * dp).toInt(),
175
170
  (16 * dp).toInt(), (8 * dp).toInt()
176
171
  )
177
- visibility = View.GONE
178
172
  }
173
+ root.addView(bar, FrameLayout.LayoutParams(
174
+ FrameLayout.LayoutParams.MATCH_PARENT, (80 * dp).toInt()
175
+ ).also { it.gravity = Gravity.BOTTOM })
179
176
 
180
- // ETA time
181
- tvEtaTime = TextView(context).apply {
177
+ val etaTime = TextView(context).apply {
182
178
  textSize = 22f
183
179
  setTextColor(Color.BLACK)
184
180
  typeface = android.graphics.Typeface.DEFAULT_BOLD
185
- layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
186
181
  }
187
- etaBottomBar.addView(tvEtaTime)
182
+ bar.addView(etaTime, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f))
183
+ tvEtaTime = etaTime
188
184
 
189
- // Duration + Distance (center)
190
- val centerLayout = LinearLayout(context).apply {
185
+ val center = LinearLayout(context).apply {
191
186
  orientation = LinearLayout.VERTICAL
192
187
  gravity = Gravity.CENTER
193
- layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 2f)
194
188
  }
195
- tvDuration = TextView(context).apply {
196
- textSize = 16f
197
- setTextColor(Color.DKGRAY)
198
- gravity = Gravity.CENTER
189
+ val dur = TextView(context).apply {
190
+ textSize = 16f; setTextColor(Color.DKGRAY); gravity = Gravity.CENTER
199
191
  }
200
- tvDistance = TextView(context).apply {
201
- textSize = 13f
202
- setTextColor(Color.GRAY)
203
- gravity = Gravity.CENTER
192
+ val dist = TextView(context).apply {
193
+ textSize = 13f; setTextColor(Color.GRAY); gravity = Gravity.CENTER
204
194
  }
205
- centerLayout.addView(tvDuration)
206
- centerLayout.addView(tvDistance)
207
- etaBottomBar.addView(centerLayout)
208
-
209
- // Cancel button
210
- btnCancel = TextView(context).apply {
211
- text = "✕"
212
- textSize = 22f
213
- setTextColor(Color.DKGRAY)
195
+ center.addView(dur)
196
+ center.addView(dist)
197
+ bar.addView(center, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 2f))
198
+ tvDuration = dur
199
+ tvDistance = dist
200
+
201
+ val cancelBtn = TextView(context).apply {
202
+ text = "✕"; textSize = 22f; setTextColor(Color.DKGRAY)
214
203
  gravity = Gravity.END or Gravity.CENTER_VERTICAL
215
- layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
216
204
  setOnClickListener { cancelNavigation() }
217
205
  }
218
- etaBottomBar.addView(btnCancel)
219
- root.addView(etaBottomBar)
206
+ bar.addView(cancelBtn, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f))
207
+ etaBar = bar
220
208
 
221
209
  addView(root)
222
210
  }
223
211
 
224
212
  // ─────────────────────────────────────────────────────────────────────────
225
- // Setup Navigation
213
+ // Init APIs — confirmed from official Mapbox examples
226
214
  // ─────────────────────────────────────────────────────────────────────────
227
- private fun setupNavigation() {
228
- val navigationOptions = NavigationOptions.Builder(context).build()
229
- mapboxNavigation = MapboxNavigationProvider.create(navigationOptions)
230
-
231
- // Auto day/night style
232
- val style = mapStyle ?: getAutoStyle()
233
-
234
- mapView.mapboxMap.loadStyle(style) { _ ->
235
- setupComponents()
236
- registerObservers()
237
- }
238
- }
215
+ private fun initAPIs() {
216
+ val distanceFormatterOptions = DistanceFormatterOptions.Builder(context).build()
217
+
218
+ maneuverApi = MapboxManeuverApi(MapboxDistanceFormatter(distanceFormatterOptions))
219
+
220
+ // TripProgressApi from ShowTripProgressActivity official example
221
+ // PercentDistanceTraveledFormatter is optional — omit if causing issues
222
+ tripProgressApi = MapboxTripProgressApi(
223
+ TripProgressUpdateFormatter.Builder(context)
224
+ .distanceRemainingFormatter(DistanceRemainingFormatter(distanceFormatterOptions))
225
+ .timeRemainingFormatter(TimeRemainingFormatter(context))
226
+ .estimatedTimeToArrivalFormatter(
227
+ EstimatedTimeToArrivalFormatter(context, TimeFormat.NONE_SPECIFIED)
228
+ )
229
+ .build()
230
+ )
239
231
 
240
- private fun setupComponents() {
241
- val mapboxMap = mapView.mapboxMap
232
+ speedInfoApi = MapboxSpeedInfoApi()
242
233
 
243
- // Route line
244
234
  routeLineApi = MapboxRouteLineApi(MapboxRouteLineApiOptions.Builder().build())
245
235
  routeLineView = MapboxRouteLineView(
246
236
  MapboxRouteLineViewOptions.Builder(context)
@@ -248,41 +238,38 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
248
238
  .build()
249
239
  )
250
240
 
251
- // Camera
252
- viewportDataSource = MapboxNavigationViewportDataSource(mapboxMap)
253
- navigationCamera = NavigationCamera(
254
- mapboxMap,
255
- mapView.camera,
256
- viewportDataSource!!
241
+ routeArrowView = MapboxRouteArrowView(RouteArrowOptions.Builder(context).build())
242
+ }
243
+
244
+ // ─────────────────────────────────────────────────────────────────────────
245
+ // Setup Navigation
246
+ // ─────────────────────────────────────────────────────────────────────────
247
+ private fun setupNavigation() {
248
+ mapboxNavigation = MapboxNavigationProvider.create(
249
+ NavigationOptions.Builder(context).build()
257
250
  )
258
251
 
259
- // Speed info API
260
- speedInfoApi = MapboxSpeedInfoApi()
252
+ mapView.mapboxMap.loadStyle(mapStyle ?: getAutoStyle()) { style ->
253
+ routeLineView.initializeLayers(style)
261
254
 
262
- // Maneuver API
263
- val formatter = DistanceFormatterOptions.Builder(context).build()
264
- maneuverApi = MapboxManeuverApi(
265
- com.mapbox.navigation.base.formatter.MapboxDistanceFormatter(formatter)
266
- )
255
+ viewportDataSource = MapboxNavigationViewportDataSource(mapView.mapboxMap)
256
+ navigationCamera = NavigationCamera(
257
+ mapView.mapboxMap,
258
+ mapView.camera,
259
+ viewportDataSource
260
+ )
261
+
262
+ mapView.location.apply {
263
+ setLocationProvider(navigationLocationProvider)
264
+ enabled = true
265
+ }
267
266
 
268
- // Trip progress API
269
- val progressFormatter = TripProgressUpdateFormatter.Builder(context)
270
- .distanceRemainingFormatter(DistanceRemainingFormatter(formatter))
271
- .timeRemainingFormatter(TimeRemainingFormatter(context))
272
- .percentRouteTraveledFormatter(PercentDistanceTraveledFormatter())
273
- .estimatedTimeToArrivalFormatter(EstimatedTimeToArrivalFormatter(context))
274
- .build()
275
- tripProgressApi = MapboxTripProgressApi(progressFormatter)
276
-
277
- // Location puck
278
- mapView.location.apply {
279
- setLocationProvider(navigationLocationProvider)
280
- enabled = true
267
+ registerObservers()
281
268
  }
282
269
  }
283
270
 
284
271
  // ─────────────────────────────────────────────────────────────────────────
285
- // Register all observers
272
+ // Observers
286
273
  // ─────────────────────────────────────────────────────────────────────────
287
274
  private fun registerObservers() {
288
275
  val nav = mapboxNavigation ?: return
@@ -290,73 +277,76 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
290
277
  // Routes observer
291
278
  nav.registerRoutesObserver(object : RoutesObserver {
292
279
  override fun onRoutesChanged(result: RoutesUpdatedResult) {
293
- val routes = result.navigationRoutes
294
- if (routes.isNotEmpty()) {
295
- routeLineApi?.setNavigationRoutes(routes) { value ->
296
- mapView.mapboxMap.style?.let { style ->
297
- routeLineView?.renderRouteDrawData(style, value)
280
+ if (result.navigationRoutes.isNotEmpty()) {
281
+ routeLineApi.setNavigationRoutes(result.navigationRoutes) { value ->
282
+ mapView.mapboxMap.style?.apply {
283
+ routeLineView.renderRouteDrawData(this, value)
298
284
  }
299
285
  }
300
- viewportDataSource?.onRouteChanged(routes.first())
301
- viewportDataSource?.evaluate()
302
- safeCameraFollowing()
303
-
304
- // Show UI overlays
305
- maneuverView.visibility = View.VISIBLE
306
- etaBottomBar.visibility = View.VISIBLE
307
-
286
+ viewportDataSource.onRouteChanged(result.navigationRoutes.first())
287
+ viewportDataSource.evaluate()
288
+ showUI()
308
289
  onRoutesReady(mapOf(
309
- "routeCount" to routes.size,
310
- "distanceMeters" to (routes.first().directionsRoute.distance() ?: 0.0),
311
- "durationSeconds" to (routes.first().directionsRoute.duration() ?: 0.0)
290
+ "routeCount" to result.navigationRoutes.size,
291
+ "distanceMeters" to (result.navigationRoutes.first().directionsRoute.distance() ?: 0.0),
292
+ "durationSeconds" to (result.navigationRoutes.first().directionsRoute.duration() ?: 0.0)
312
293
  ))
313
294
  } else {
314
- routeLineApi?.clearRouteLine { value ->
315
- mapView.mapboxMap.style?.let { style ->
316
- routeLineView?.renderClearRouteLineValue(style, value)
295
+ mapView.mapboxMap.style?.let { style ->
296
+ routeLineApi.clearRouteLine { value ->
297
+ routeLineView.renderClearRouteLineValue(style, value)
317
298
  }
299
+ routeArrowView.render(style, routeArrowApi.clearArrows())
318
300
  }
319
- viewportDataSource?.clearRouteData()
320
- viewportDataSource?.evaluate()
321
- hideNavigationUI()
301
+ viewportDataSource.clearRouteData()
302
+ viewportDataSource.evaluate()
303
+ hideUI()
322
304
  }
323
305
  }
324
306
  })
325
307
 
326
- // Route progress observer — maneuvers + ETA + speed limit
308
+ // Route progress observer
327
309
  nav.registerRouteProgressObserver(RouteProgressObserver { routeProgress ->
328
- viewportDataSource?.onRouteProgressChanged(routeProgress)
329
- viewportDataSource?.evaluate()
310
+ viewportDataSource.onRouteProgressChanged(routeProgress)
311
+ viewportDataSource.evaluate()
330
312
 
331
- // ── Maneuver instructions ──────────────────────────────────────
332
- maneuverApi?.getManeuvers(routeProgress) { maneuverState ->
333
- maneuverView.renderManeuvers(maneuverState)
313
+ // Route arrow
314
+ mapView.mapboxMap.style?.let { style ->
315
+ val arrowResult = routeArrowApi.addUpcomingManeuverArrow(routeProgress)
316
+ routeArrowView.renderManeuverUpdate(style, arrowResult)
334
317
  }
335
318
 
336
- // ── Trip progress (ETA, duration, distance) ────────────────────
337
- tripProgressApi?.getTripProgress(routeProgress)?.let { tripProgress ->
338
- val eta = tripProgress.currentLegTimeRemaining
339
- val cal = Calendar.getInstance().apply {
340
- add(Calendar.SECOND, eta.toInt())
319
+ // ── Maneuver fold() with explicit Unit returns on both branches ──
320
+ // Fix for error: "Argument type mismatch: Unit? but R & Any expected"
321
+ // Solution: use explicit return type annotation on the success lambda
322
+ val maneuvers = maneuverApi.getManeuvers(routeProgress)
323
+ maneuvers.fold(
324
+ { error ->
325
+ Log.w(TAG, "Maneuver error: ${error.errorMessage}")
326
+ Unit
327
+ },
328
+ { _ ->
329
+ maneuverView?.visibility = View.VISIBLE
330
+ maneuverView?.renderManeuvers(maneuvers)
331
+ Unit
341
332
  }
342
- val h = cal.get(Calendar.HOUR_OF_DAY)
343
- val m = cal.get(Calendar.MINUTE)
344
- tvEtaTime.text = String.format("%02d:%02d", h, m)
345
-
346
- val totalMin = (tripProgress.totalTimeRemaining / 60).toInt()
347
- tvDuration.text = if (totalMin >= 60)
348
- "${totalMin / 60}h ${totalMin % 60}min"
349
- else
350
- "${totalMin} min"
351
-
352
- val distKm = tripProgress.distanceRemaining / 1000.0
353
- tvDistance.text = if (distKm >= 1.0)
354
- String.format("%.1f km", distKm)
355
- else
356
- "${tripProgress.distanceRemaining.toInt()} m"
357
- }
333
+ )
334
+
335
+ // ── Trip progress — use getTripProgress exactly as official example ──
336
+ // render() to the MapboxTripProgressView (official pattern)
337
+ // For our custom ETA bar, use the raw fields from TripProgressUpdateValue:
338
+ // .estimatedTimeToArrival Long (Unix timestamp ms of arrival)
339
+ // .currentLegTimeRemaining Double (seconds remaining for current leg)
340
+ // .totalTimeRemaining — Double (seconds remaining total)
341
+ // .distanceRemaining — Double (metres remaining)
342
+ val tripProgress = tripProgressApi.getTripProgress(routeProgress)
343
+ tripProgressView?.render(tripProgress)
344
+ updateEtaBar(
345
+ tripProgress.estimatedTimeToArrival,
346
+ tripProgress.totalTimeRemaining,
347
+ tripProgress.distanceRemaining
348
+ )
358
349
 
359
- // ── Emit progress event ───────────────────────────────────────
360
350
  onRouteProgressChanged(mapOf(
361
351
  "distanceRemaining" to routeProgress.distanceRemaining,
362
352
  "durationRemaining" to routeProgress.durationRemaining,
@@ -368,124 +358,133 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
368
358
  ))
369
359
  })
370
360
 
371
- // Location observer — speed limit + location puck
361
+ // Location observer
372
362
  nav.registerLocationObserver(object : LocationObserver {
373
- override fun onNewRawLocation(rawLocation: com.mapbox.common.location.Location) {}
374
-
375
- override fun onNewLocationMatcherResult(locationMatcherResult: LocationMatcherResult) {
376
- val enhancedLocation = locationMatcherResult.enhancedLocation
377
-
378
- // Update location puck
363
+ override fun onNewRawLocation(rawLocation: Location) {}
364
+ override fun onNewLocationMatcherResult(result: LocationMatcherResult) {
365
+ val loc = result.enhancedLocation
379
366
  navigationLocationProvider.changePosition(
380
- location = enhancedLocation,
381
- keyPoints = locationMatcherResult.keyPoints,
382
- )
383
-
384
- // Update camera
385
- viewportDataSource?.onLocationChanged(enhancedLocation)
386
- viewportDataSource?.evaluate()
387
-
388
- // ── Speed limit (issue — show posted + current speed) ──────
389
- val formatter = DistanceFormatterOptions.Builder(context).build()
390
- val info = speedInfoApi?.updatePostedAndCurrentSpeed(
391
- locationMatcherResult,
392
- formatter
367
+ location = loc,
368
+ keyPoints = result.keyPoints,
393
369
  )
394
- if (info != null) {
395
- speedInfoView.visibility = View.VISIBLE
396
- speedInfoView.render(info)
370
+ viewportDataSource.onLocationChanged(loc)
371
+ viewportDataSource.evaluate()
372
+
373
+ // Speed limit
374
+ val fmtOptions = DistanceFormatterOptions.Builder(context).build()
375
+ val speedInfo = speedInfoApi.updatePostedAndCurrentSpeed(result, fmtOptions)
376
+ if (speedInfo != null) {
377
+ speedInfoView?.visibility = View.VISIBLE
378
+ speedInfoView?.render(speedInfo)
397
379
  } else {
398
- speedInfoView.visibility = View.GONE
380
+ speedInfoView?.visibility = View.GONE
399
381
  }
400
382
 
401
- // ── Auto day/night switch ──────────────────────────────────
402
383
  checkAndSwitchDayNight()
403
384
  }
404
385
  })
405
386
  }
406
387
 
407
388
  // ─────────────────────────────────────────────────────────────────────────
408
- // Auto day/night mode
389
+ // ETA bar — using confirmed TripProgressUpdateValue fields
390
+ // estimatedTimeToArrival: Long — Unix timestamp (ms) of expected arrival
391
+ // totalTimeRemaining: Double — seconds remaining
392
+ // distanceRemaining: Double — metres remaining
393
+ // ─────────────────────────────────────────────────────────────────────────
394
+ private fun updateEtaBar(
395
+ estimatedTimeToArrivalMs: Long,
396
+ totalTimeRemainingSec: Double,
397
+ distanceRemainingMetres: Double
398
+ ) {
399
+ // Arrival clock time from Unix timestamp
400
+ val arrivalCal = Calendar.getInstance().apply {
401
+ timeInMillis = estimatedTimeToArrivalMs
402
+ }
403
+ tvEtaTime?.text = String.format(
404
+ "%02d:%02d",
405
+ arrivalCal.get(Calendar.HOUR_OF_DAY),
406
+ arrivalCal.get(Calendar.MINUTE)
407
+ )
408
+
409
+ // Duration remaining
410
+ val totalMin = (totalTimeRemainingSec / 60).toInt()
411
+ tvDuration?.text = if (totalMin >= 60)
412
+ "${totalMin / 60}h ${totalMin % 60}min"
413
+ else
414
+ "${totalMin} min"
415
+
416
+ // Distance remaining
417
+ tvDistance?.text = if (distanceRemainingMetres >= 1000)
418
+ String.format("%.1f km", distanceRemainingMetres / 1000.0)
419
+ else
420
+ "${distanceRemainingMetres.toInt()} m"
421
+ }
422
+
423
+ // ─────────────────────────────────────────────────────────────────────────
424
+ // Day / Night
409
425
  // ─────────────────────────────────────────────────────────────────────────
410
426
  private fun getAutoStyle(): String {
411
427
  val hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY)
412
- return if (hour in 6..20)
413
- NavigationStyles.NAVIGATION_DAY_STYLE
414
- else
415
- NavigationStyles.NAVIGATION_NIGHT_STYLE
428
+ return if (hour in 6..20) NavigationStyles.NAVIGATION_DAY_STYLE
429
+ else NavigationStyles.NAVIGATION_NIGHT_STYLE
416
430
  }
417
431
 
418
432
  private fun checkAndSwitchDayNight() {
419
433
  val hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY)
420
434
  val shouldBeNight = hour !in 6..20
421
- if (shouldBeNight != isNightMode) {
422
- isNightMode = shouldBeNight
423
- val newStyle = if (shouldBeNight)
424
- NavigationStyles.NAVIGATION_NIGHT_STYLE
425
- else
426
- NavigationStyles.NAVIGATION_DAY_STYLE
427
-
428
- mapView.mapboxMap.loadStyle(newStyle) { _ ->
429
- // Re-render route line on new style
430
- mapView.mapboxMap.style?.let { style ->
431
- routeLineApi?.let { api ->
432
- mapboxNavigation?.getNavigationRoutes()?.let { routes ->
433
- if (routes.isNotEmpty()) {
434
- api.setNavigationRoutes(routes) { value ->
435
- routeLineView?.renderRouteDrawData(style, value)
436
- }
437
- }
438
- }
439
- }
435
+ if (shouldBeNight == isNightMode) return
436
+ isNightMode = shouldBeNight
437
+ val newStyle = if (shouldBeNight) NavigationStyles.NAVIGATION_NIGHT_STYLE
438
+ else NavigationStyles.NAVIGATION_DAY_STYLE
439
+ mapView.mapboxMap.loadStyle(newStyle) { style ->
440
+ routeLineView.initializeLayers(style)
441
+ mapboxNavigation?.getNavigationRoutes()?.takeIf { it.isNotEmpty() }?.let { routes ->
442
+ routeLineApi.setNavigationRoutes(routes) { value ->
443
+ routeLineView.renderRouteDrawData(style, value)
440
444
  }
441
445
  }
442
-
443
- // Switch bottom bar color
444
- etaBottomBar.setBackgroundColor(
445
- if (shouldBeNight) Color.parseColor("#1E2433") else Color.WHITE
446
- )
447
- val textColor = if (shouldBeNight) Color.WHITE else Color.BLACK
448
- tvEtaTime.setTextColor(textColor)
449
- tvDuration.setTextColor(if (shouldBeNight) Color.LTGRAY else Color.DKGRAY)
450
- tvDistance.setTextColor(if (shouldBeNight) Color.GRAY else Color.GRAY)
451
- btnCancel.setTextColor(if (shouldBeNight) Color.LTGRAY else Color.DKGRAY)
452
446
  }
447
+ etaBar?.setBackgroundColor(if (shouldBeNight) Color.parseColor("#1E2433") else Color.WHITE)
448
+ val tc = if (shouldBeNight) Color.WHITE else Color.BLACK
449
+ tvEtaTime?.setTextColor(tc)
450
+ tvDuration?.setTextColor(if (shouldBeNight) Color.LTGRAY else Color.DKGRAY)
451
+ tvDistance?.setTextColor(Color.GRAY)
453
452
  }
454
453
 
455
454
  // ─────────────────────────────────────────────────────────────────────────
456
455
  // Cancel navigation
457
456
  // ─────────────────────────────────────────────────────────────────────────
458
457
  private fun cancelNavigation() {
459
- mapboxNavigation?.setNavigationRoutes(emptyList())
458
+ mapboxNavigation?.setNavigationRoutes(listOf())
460
459
  mapboxNavigation?.stopTripSession()
461
- hideNavigationUI()
460
+ hideUI()
462
461
  onNavigationCancelled(mapOf<String, Any>())
463
462
  }
464
463
 
465
- private fun hideNavigationUI() {
466
- maneuverView.visibility = View.GONE
467
- speedInfoView.visibility = View.GONE
468
- etaBottomBar.visibility = View.GONE
464
+ private fun showUI() {
465
+ maneuverView?.visibility = View.VISIBLE
466
+ etaBar?.visibility = View.VISIBLE
469
467
  }
470
468
 
471
- // ─────────────────────────────────────────────────────────────────────────
472
- // Issue #43: safe camera
473
- // ─────────────────────────────────────────────────────────────────────────
469
+ private fun hideUI() {
470
+ maneuverView?.visibility = View.INVISIBLE
471
+ speedInfoView?.visibility = View.GONE
472
+ etaBar?.visibility = View.INVISIBLE
473
+ }
474
+
475
+ // Issue #43
474
476
  private fun safeCameraFollowing() {
475
477
  try {
476
- navigationCamera?.requestNavigationCameraToFollowing(
478
+ navigationCamera.requestNavigationCameraToFollowing(
477
479
  stateTransitionOptions = NavigationCameraTransitionOptions.Builder()
478
- .maxDuration(0)
479
- .build()
480
+ .maxDuration(0).build()
480
481
  )
481
482
  } catch (e: Exception) {
482
- Log.e(TAG, "Camera transition error: ${e.message}")
483
+ Log.e(TAG, "Camera error: ${e.message}")
483
484
  }
484
485
  }
485
486
 
486
- // ─────────────────────────────────────────────────────────────────────────
487
- // Issue #31: voice units
488
- // ─────────────────────────────────────────────────────────────────────────
487
+ // Issue #31
489
488
  private fun resolveVoiceUnits(): String {
490
489
  return when (voiceUnits?.lowercase()) {
491
490
  "metric" -> "metric"
@@ -497,76 +496,44 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
497
496
  }
498
497
  }
499
498
 
500
- // ─────────────────────────────────────────────────────────────────────────
501
499
  // Route request
502
- // ─────────────────────────────────────────────────────────────────────────
503
500
  @SuppressLint("MissingPermission")
504
501
  private fun fetchRoutes() {
505
502
  val nav = mapboxNavigation ?: return
506
503
  if (coordinates.size < 2) return
507
-
508
- val points = coordinates.map { coord ->
509
- Point.fromLngLat(coord["longitude"] ?: 0.0, coord["latitude"] ?: 0.0)
510
- }
504
+ val points = coordinates.map { Point.fromLngLat(it["longitude"] ?: 0.0, it["latitude"] ?: 0.0) }
511
505
  val locale = language?.let { Locale.forLanguageTag(it) } ?: Locale.getDefault()
512
-
513
506
  val builder = RouteOptions.builder()
514
507
  .applyDefaultNavigationOptions()
515
508
  .language(locale.toLanguageTag())
516
509
  .voiceUnits(resolveVoiceUnits())
517
510
  .coordinatesList(points)
518
- // maxspeed annotation required for speed limit display
519
511
  .annotations("maxspeed,congestion,duration")
520
-
521
512
  waypointIndices?.let { builder.waypointIndicesList(it) }
522
- navigationProfile?.let {
523
- builder.profile(if (it.startsWith("mapbox/")) it else "mapbox/$it")
524
- }
525
- excludeTypes?.takeIf { it.isNotEmpty() }?.let {
526
- builder.exclude(it.joinToString(","))
527
- }
513
+ navigationProfile?.let { builder.profile(if (it.startsWith("mapbox/")) it else "mapbox/$it") }
514
+ excludeTypes?.takeIf { it.isNotEmpty() }?.let { builder.exclude(it.joinToString(",")) }
528
515
  maxHeight?.let { builder.maxHeight(it) }
529
516
  maxWidth?.let { builder.maxWidth(it) }
530
-
531
- nav.requestRoutes(
532
- builder.build(),
533
- object : NavigationRouterCallback {
534
- override fun onRoutesReady(
535
- routes: List<NavigationRoute>,
536
- @RouterOrigin routerOrigin: String
537
- ) {
538
- if (routes.isEmpty()) {
539
- onRoutesFailed(mapOf("message" to "No routes returned"))
540
- return
541
- }
542
- nav.setNavigationRoutes(routes)
543
- nav.startTripSession()
544
- }
545
-
546
- override fun onFailure(
547
- reasons: List<RouterFailure>,
548
- routeOptions: RouteOptions
549
- ) {
550
- val msg = reasons.firstOrNull()?.message ?: "Unknown error"
551
- Log.e(TAG, "Route request failed: $msg")
552
- onRoutesFailed(mapOf("message" to msg))
553
- }
554
-
555
- override fun onCanceled(
556
- routeOptions: RouteOptions,
557
- @RouterOrigin routerOrigin: String
558
- ) {
559
- Log.d(TAG, "Route request cancelled")
560
- }
517
+ nav.requestRoutes(builder.build(), object : NavigationRouterCallback {
518
+ override fun onRoutesReady(routes: List<NavigationRoute>, @RouterOrigin routerOrigin: String) {
519
+ if (routes.isEmpty()) { onRoutesFailed(mapOf("message" to "No routes returned")); return }
520
+ nav.setNavigationRoutes(routes)
521
+ nav.startTripSession()
522
+ safeCameraFollowing()
561
523
  }
562
- )
524
+ override fun onFailure(reasons: List<RouterFailure>, routeOptions: RouteOptions) {
525
+ val msg = reasons.firstOrNull()?.message ?: "Unknown error"
526
+ Log.e(TAG, "Route failed: $msg")
527
+ onRoutesFailed(mapOf("message" to msg))
528
+ }
529
+ override fun onCanceled(routeOptions: RouteOptions, @RouterOrigin routerOrigin: String) {
530
+ Log.d(TAG, "Route cancelled")
531
+ }
532
+ })
563
533
  }
564
534
 
565
- // ── Prop setters ──────────────────────────────────────────────────────────
566
- fun setCoordinates(coords: List<Map<String, Double>>) {
567
- coordinates = coords
568
- if (coords.size >= 2) fetchRoutes()
569
- }
535
+ // Prop setters
536
+ fun setCoordinates(coords: List<Map<String, Double>>) { coordinates = coords; if (coords.size >= 2) fetchRoutes() }
570
537
  fun setWaypointIndices(indices: List<Int>?) { waypointIndices = indices }
571
538
  fun setLanguage(lang: String?) { language = lang }
572
539
  fun setVoiceUnits(units: String?) { voiceUnits = units }
@@ -580,12 +547,11 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
580
547
  fun setCustomRasterTileUrl(url: String?) { customRasterTileUrl = url }
581
548
  fun setCustomRasterAboveLayerId(layerId: String?) { customRasterAboveLayerId = layerId }
582
549
 
583
- // ── Lifecycle ─────────────────────────────────────────────────────────────
584
550
  override fun onDetachedFromWindow() {
585
551
  super.onDetachedFromWindow()
586
- maneuverApi?.cancel()
587
- routeLineApi?.cancel()
588
- routeLineView?.cancel()
552
+ maneuverApi.cancel()
553
+ routeLineApi.cancel()
554
+ routeLineView.cancel()
589
555
  mapboxNavigation?.stopTripSession()
590
556
  MapboxNavigationProvider.destroy()
591
557
  mapView.onStop()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jacques_gordon/expo-mapbox-navigation",
3
- "version": "2.0.9",
3
+ "version": "2.1.1",
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",
@@ -23,14 +23,14 @@
23
23
  ],
24
24
  "repository": {
25
25
  "type": "git",
26
- "url": "https://github.com/jacques_gordon/expo-mapbox-navigation.git"
26
+ "url": "https://github.com/jacquesngomeheffa/expo-mapbox-navigation.git"
27
27
  },
28
28
  "bugs": {
29
- "url": "https://github.com/jacques_gordon/expo-mapbox-navigation/issues"
29
+ "url": "https://github.com/jacquesngomeheffa/expo-mapbox-navigation/issues"
30
30
  },
31
31
  "author": "Jacques Gordon",
32
32
  "license": "MIT",
33
- "homepage": "https://github.com/jacques_gordon/expo-mapbox-navigation#readme",
33
+ "homepage": "https://github.com/jacquesngomeheffa/expo-mapbox-navigation#readme",
34
34
  "peerDependencies": {
35
35
  "expo": ">=53.0.0",
36
36
  "react": "*",