@jacques_gordon/expo-mapbox-navigation 2.0.9 → 2.1.0

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
@@ -62,7 +65,7 @@ private const val TAG = "ExpoMapboxNavigation"
62
65
  class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
63
66
  ExpoView(context, appContext) {
64
67
 
65
- // ── EventDispatchers ──────────────────────────────────────────────────────
68
+ // ── EventDispatchers (correct Expo Modules API for View events) ───────────
66
69
  private val onRouteProgressChanged by EventDispatcher()
67
70
  private val onRoutesReady by EventDispatcher()
68
71
  private val onNavigationFinished by EventDispatcher()
@@ -70,31 +73,30 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
70
73
  private val onRoutesFailed by EventDispatcher()
71
74
  private val onArrival by EventDispatcher()
72
75
 
73
- // ── Core views ────────────────────────────────────────────────────────────
76
+ // ── Views ─────────────────────────────────────────────────────────────────
74
77
  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
85
-
86
- // ── 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
78
+ private var maneuverView: MapboxManeuverView? = null
79
+ private var speedInfoView: MapboxSpeedInfoView? = null
80
+ private var tripProgressView: MapboxTripProgressView? = null
81
+ private var tvEtaTime: TextView? = null
82
+ private var tvDuration: TextView? = null
83
+ private var tvDistance: TextView? = null
84
+ private var etaBar: LinearLayout? = null
85
+
86
+ // ── Navigation APIs (from official TurnByTurnExperienceActivity) ──────────
87
+ private lateinit var navigationCamera: NavigationCamera
88
+ private lateinit var viewportDataSource: MapboxNavigationViewportDataSource
89
+ private lateinit var routeLineApi: MapboxRouteLineApi
90
+ private lateinit var routeLineView: MapboxRouteLineView
91
+ private lateinit var maneuverApi: MapboxManeuverApi
92
+ private lateinit var tripProgressApi: MapboxTripProgressApi
93
+ private lateinit var speedInfoApi: MapboxSpeedInfoApi
94
+ private val routeArrowApi = MapboxRouteArrowApi()
95
+ private lateinit var routeArrowView: MapboxRouteArrowView
95
96
  private val navigationLocationProvider = NavigationLocationProvider()
97
+ private var mapboxNavigation: MapboxNavigation? = null
96
98
 
97
- // ── Night mode state ──────────────────────────────────────────────────────
99
+ // ── Night mode ────────────────────────────────────────────────────────────
98
100
  private var isNightMode = false
99
101
 
100
102
  // ── Props ─────────────────────────────────────────────────────────────────
@@ -113,250 +115,250 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
113
115
  private var customRasterAboveLayerId: String? = null
114
116
 
115
117
  init {
116
- buildOverlayUI()
118
+ buildUI()
119
+ initAPIs()
117
120
  setupNavigation()
118
121
  }
119
122
 
120
123
  // ─────────────────────────────────────────────────────────────────────────
121
- // Build the Waze-style overlay UI programmatically
124
+ // 1. Build programmatic UI
122
125
  // ─────────────────────────────────────────────────────────────────────────
123
- private fun buildOverlayUI() {
126
+ private fun buildUI() {
127
+ val dp = context.resources.displayMetrics.density
124
128
  val root = FrameLayout(context).apply {
125
129
  layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
126
130
  }
127
131
 
128
- // MapView full screen
129
- mapView.layoutParams = FrameLayout.LayoutParams(
132
+ // Full-screen MapView
133
+ root.addView(mapView, FrameLayout.LayoutParams(
130
134
  FrameLayout.LayoutParams.MATCH_PARENT,
131
135
  FrameLayout.LayoutParams.MATCH_PARENT
132
- )
133
- root.addView(mapView)
134
-
135
- val dp = context.resources.displayMetrics.density
136
-
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)
136
+ ))
162
137
 
163
- // ── ETA bottom bar ────────────────────────────────────────────────────
164
- etaBottomBar = LinearLayout(context).apply {
138
+ // ManeuverView top banner
139
+ // NOTE: MapboxManeuverView extends ConstraintLayout, cast to View for addView()
140
+ val mv = MapboxManeuverView(context)
141
+ val mvParams = FrameLayout.LayoutParams(
142
+ FrameLayout.LayoutParams.MATCH_PARENT,
143
+ FrameLayout.LayoutParams.WRAP_CONTENT
144
+ )
145
+ mvParams.gravity = Gravity.TOP
146
+ mv.visibility = View.INVISIBLE // matches official example (INVISIBLE not GONE)
147
+ root.addView(mv as View, mvParams)
148
+ maneuverView = mv
149
+
150
+ // SpeedInfoView — bottom-left
151
+ val siv = MapboxSpeedInfoView(context)
152
+ val sivParams = FrameLayout.LayoutParams(
153
+ FrameLayout.LayoutParams.WRAP_CONTENT,
154
+ FrameLayout.LayoutParams.WRAP_CONTENT
155
+ )
156
+ sivParams.gravity = Gravity.BOTTOM or Gravity.START
157
+ sivParams.setMargins((16 * dp).toInt(), 0, 0, (100 * dp).toInt())
158
+ siv.visibility = View.GONE
159
+ root.addView(siv as View, sivParams)
160
+ speedInfoView = siv
161
+
162
+ // TripProgressView — embedded in bottom bar
163
+ val tpv = MapboxTripProgressView(context)
164
+ tripProgressView = tpv
165
+
166
+ // ETA bottom bar
167
+ val bar = LinearLayout(context).apply {
165
168
  orientation = LinearLayout.HORIZONTAL
166
169
  setBackgroundColor(Color.WHITE)
167
170
  elevation = 8 * dp
168
- layoutParams = FrameLayout.LayoutParams(
169
- FrameLayout.LayoutParams.MATCH_PARENT,
170
- (80 * dp).toInt()
171
- ).also { it.gravity = Gravity.BOTTOM }
171
+ visibility = View.INVISIBLE
172
172
  gravity = Gravity.CENTER_VERTICAL
173
173
  setPadding(
174
174
  (16 * dp).toInt(), (8 * dp).toInt(),
175
175
  (16 * dp).toInt(), (8 * dp).toInt()
176
176
  )
177
- visibility = View.GONE
178
177
  }
178
+ val barParams = FrameLayout.LayoutParams(
179
+ FrameLayout.LayoutParams.MATCH_PARENT,
180
+ (80 * dp).toInt()
181
+ )
182
+ barParams.gravity = Gravity.BOTTOM
179
183
 
180
- // ETA time
181
- tvEtaTime = TextView(context).apply {
184
+ val etaTime = TextView(context).apply {
182
185
  textSize = 22f
183
186
  setTextColor(Color.BLACK)
184
187
  typeface = android.graphics.Typeface.DEFAULT_BOLD
185
- layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
186
188
  }
187
- etaBottomBar.addView(tvEtaTime)
189
+ bar.addView(etaTime, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f))
190
+ tvEtaTime = etaTime
188
191
 
189
- // Duration + Distance (center)
190
- val centerLayout = LinearLayout(context).apply {
192
+ val center = LinearLayout(context).apply {
191
193
  orientation = LinearLayout.VERTICAL
192
194
  gravity = Gravity.CENTER
193
- layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 2f)
194
- }
195
- tvDuration = TextView(context).apply {
196
- textSize = 16f
197
- setTextColor(Color.DKGRAY)
198
- gravity = Gravity.CENTER
199
195
  }
200
- tvDistance = TextView(context).apply {
201
- textSize = 13f
202
- setTextColor(Color.GRAY)
203
- gravity = Gravity.CENTER
204
- }
205
- centerLayout.addView(tvDuration)
206
- centerLayout.addView(tvDistance)
207
- etaBottomBar.addView(centerLayout)
208
-
209
- // Cancel button
210
- btnCancel = TextView(context).apply {
196
+ val dur = TextView(context).apply { textSize = 16f; setTextColor(Color.DKGRAY); gravity = Gravity.CENTER }
197
+ val dist = TextView(context).apply { textSize = 13f; setTextColor(Color.GRAY); gravity = Gravity.CENTER }
198
+ center.addView(dur)
199
+ center.addView(dist)
200
+ bar.addView(center, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 2f))
201
+ tvDuration = dur
202
+ tvDistance = dist
203
+
204
+ val cancelBtn = TextView(context).apply {
211
205
  text = "✕"
212
206
  textSize = 22f
213
207
  setTextColor(Color.DKGRAY)
214
208
  gravity = Gravity.END or Gravity.CENTER_VERTICAL
215
- layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
216
209
  setOnClickListener { cancelNavigation() }
217
210
  }
218
- etaBottomBar.addView(btnCancel)
219
- root.addView(etaBottomBar)
211
+ bar.addView(cancelBtn, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f))
212
+ etaBar = bar
213
+ root.addView(bar, barParams)
220
214
 
221
215
  addView(root)
222
216
  }
223
217
 
224
218
  // ─────────────────────────────────────────────────────────────────────────
225
- // Setup Navigation
219
+ // 2. Init APIs (from official example — exact same pattern)
226
220
  // ─────────────────────────────────────────────────────────────────────────
227
- private fun setupNavigation() {
228
- val navigationOptions = NavigationOptions.Builder(context).build()
229
- mapboxNavigation = MapboxNavigationProvider.create(navigationOptions)
221
+ private fun initAPIs() {
222
+ val distanceFormatterOptions = DistanceFormatterOptions.Builder(context).build()
230
223
 
231
- // Auto day/night style
232
- val style = mapStyle ?: getAutoStyle()
224
+ // EXACT same instantiation as TurnByTurnExperienceActivity
225
+ maneuverApi = MapboxManeuverApi(
226
+ MapboxDistanceFormatter(distanceFormatterOptions)
227
+ )
233
228
 
234
- mapView.mapboxMap.loadStyle(style) { _ ->
235
- setupComponents()
236
- registerObservers()
237
- }
238
- }
229
+ tripProgressApi = MapboxTripProgressApi(
230
+ TripProgressUpdateFormatter.Builder(context)
231
+ .distanceRemainingFormatter(DistanceRemainingFormatter(distanceFormatterOptions))
232
+ .timeRemainingFormatter(TimeRemainingFormatter(context))
233
+ .percentRouteTraveledFormatter(PercentDistanceTraveledFormatter())
234
+ .estimatedTimeToArrivalFormatter(
235
+ EstimatedTimeToArrivalFormatter(context, TimeFormat.NONE_SPECIFIED)
236
+ )
237
+ .build()
238
+ )
239
239
 
240
- private fun setupComponents() {
241
- val mapboxMap = mapView.mapboxMap
240
+ speedInfoApi = MapboxSpeedInfoApi()
242
241
 
243
- // Route line
242
+ val routeLineViewOptions = MapboxRouteLineViewOptions.Builder(context)
243
+ .routeLineBelowLayerId("road-label-navigation")
244
+ .build()
244
245
  routeLineApi = MapboxRouteLineApi(MapboxRouteLineApiOptions.Builder().build())
245
- routeLineView = MapboxRouteLineView(
246
- MapboxRouteLineViewOptions.Builder(context)
247
- .routeLineBelowLayerId("road-label-navigation")
248
- .build()
249
- )
246
+ routeLineView = MapboxRouteLineView(routeLineViewOptions)
247
+
248
+ val routeArrowOptions = RouteArrowOptions.Builder(context).build()
249
+ routeArrowView = MapboxRouteArrowView(routeArrowOptions)
250
+ }
250
251
 
251
- // Camera
252
- viewportDataSource = MapboxNavigationViewportDataSource(mapboxMap)
253
- navigationCamera = NavigationCamera(
254
- mapboxMap,
255
- mapView.camera,
256
- viewportDataSource!!
252
+ // ─────────────────────────────────────────────────────────────────────────
253
+ // 3. Setup Navigation
254
+ // ─────────────────────────────────────────────────────────────────────────
255
+ private fun setupNavigation() {
256
+ mapboxNavigation = MapboxNavigationProvider.create(
257
+ NavigationOptions.Builder(context).build()
257
258
  )
258
259
 
259
- // Speed info API
260
- speedInfoApi = MapboxSpeedInfoApi()
260
+ val style = mapStyle ?: getAutoStyle()
261
+ mapView.mapboxMap.loadStyle(style) { mapStyle ->
261
262
 
262
- // Maneuver API
263
- val formatter = DistanceFormatterOptions.Builder(context).build()
264
- maneuverApi = MapboxManeuverApi(
265
- com.mapbox.navigation.base.formatter.MapboxDistanceFormatter(formatter)
266
- )
263
+ // Route line layers must be initialized first
264
+ routeLineView.initializeLayers(mapStyle)
267
265
 
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)
266
+ // Camera setup — EXACT same as official example
267
+ viewportDataSource = MapboxNavigationViewportDataSource(mapView.mapboxMap)
268
+ navigationCamera = NavigationCamera(
269
+ mapView.mapboxMap,
270
+ mapView.camera,
271
+ viewportDataSource
272
+ )
273
+
274
+ // Location puck
275
+ mapView.location.apply {
276
+ setLocationProvider(navigationLocationProvider)
277
+ enabled = true
278
+ }
276
279
 
277
- // Location puck
278
- mapView.location.apply {
279
- setLocationProvider(navigationLocationProvider)
280
- enabled = true
280
+ registerObservers()
281
281
  }
282
282
  }
283
283
 
284
284
  // ─────────────────────────────────────────────────────────────────────────
285
- // Register all observers
285
+ // 4. Observers — based 100% on official TurnByTurnExperienceActivity
286
286
  // ─────────────────────────────────────────────────────────────────────────
287
287
  private fun registerObservers() {
288
288
  val nav = mapboxNavigation ?: return
289
289
 
290
- // Routes observer
290
+ // ── Routes observer ───────────────────────────────────────────────────
291
291
  nav.registerRoutesObserver(object : RoutesObserver {
292
292
  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)
293
+ if (result.navigationRoutes.isNotEmpty()) {
294
+ // Draw route line
295
+ routeLineApi.setNavigationRoutes(result.navigationRoutes) { value ->
296
+ mapView.mapboxMap.style?.apply {
297
+ routeLineView.renderRouteDrawData(this, value)
298
298
  }
299
299
  }
300
- viewportDataSource?.onRouteChanged(routes.first())
301
- viewportDataSource?.evaluate()
302
- safeCameraFollowing()
300
+ // Update camera viewport
301
+ viewportDataSource.onRouteChanged(result.navigationRoutes.first())
302
+ viewportDataSource.evaluate()
303
303
 
304
- // Show UI overlays
305
- maneuverView.visibility = View.VISIBLE
306
- etaBottomBar.visibility = View.VISIBLE
304
+ // Show navigation UI
305
+ maneuverView?.visibility = View.VISIBLE
306
+ etaBar?.visibility = View.VISIBLE
307
307
 
308
308
  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)
309
+ "routeCount" to result.navigationRoutes.size,
310
+ "distanceMeters" to (result.navigationRoutes.first().directionsRoute.distance() ?: 0.0),
311
+ "durationSeconds" to (result.navigationRoutes.first().directionsRoute.duration() ?: 0.0)
312
312
  ))
313
313
  } else {
314
- routeLineApi?.clearRouteLine { value ->
315
- mapView.mapboxMap.style?.let { style ->
316
- routeLineView?.renderClearRouteLineValue(style, value)
314
+ // Clear route line
315
+ val style = mapView.mapboxMap.style
316
+ if (style != null) {
317
+ routeLineApi.clearRouteLine { value ->
318
+ routeLineView.renderClearRouteLineValue(style, value)
317
319
  }
320
+ routeArrowView.render(style, routeArrowApi.clearArrows())
318
321
  }
319
- viewportDataSource?.clearRouteData()
320
- viewportDataSource?.evaluate()
321
- hideNavigationUI()
322
+ viewportDataSource.clearRouteData()
323
+ viewportDataSource.evaluate()
324
+ hideUI()
322
325
  }
323
326
  }
324
327
  })
325
328
 
326
- // Route progress observer — maneuvers + ETA + speed limit
329
+ // ── Route progress observer ───────────────────────────────────────────
330
+ // EXACT same pattern as TurnByTurnExperienceActivity.routeProgressObserver
327
331
  nav.registerRouteProgressObserver(RouteProgressObserver { routeProgress ->
328
- viewportDataSource?.onRouteProgressChanged(routeProgress)
329
- viewportDataSource?.evaluate()
330
-
331
- // ── Maneuver instructions ──────────────────────────────────────
332
- maneuverApi?.getManeuvers(routeProgress) { maneuverState ->
333
- maneuverView.renderManeuvers(maneuverState)
332
+ // Update camera
333
+ viewportDataSource.onRouteProgressChanged(routeProgress)
334
+ viewportDataSource.evaluate()
335
+
336
+ // Draw maneuver arrow
337
+ val style = mapView.mapboxMap.style
338
+ if (style != null) {
339
+ val maneuverArrowResult = routeArrowApi.addUpcomingManeuverArrow(routeProgress)
340
+ routeArrowView.renderManeuverUpdate(style, maneuverArrowResult)
334
341
  }
335
342
 
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())
343
+ // Maneuver banner EXACT same fold() pattern as official example
344
+ val maneuvers = maneuverApi.getManeuvers(routeProgress)
345
+ maneuvers.fold(
346
+ { error -> Log.w(TAG, "Maneuver error: ${error.errorMessage}") },
347
+ {
348
+ maneuverView?.visibility = View.VISIBLE
349
+ maneuverView?.renderManeuvers(maneuvers)
341
350
  }
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
- }
351
+ )
352
+
353
+ // Trip progress EXACT same as official example
354
+ // getTripProgress returns TripProgressUpdateValue directly (not nullable)
355
+ val tripProgress = tripProgressApi.getTripProgress(routeProgress)
356
+ tripProgressView?.render(tripProgress)
357
+
358
+ // Update our custom ETA bar from tripProgress fields
359
+ updateEtaBar(tripProgress)
358
360
 
359
- // ── Emit progress event ───────────────────────────────────────
361
+ // Emit event
360
362
  onRouteProgressChanged(mapOf(
361
363
  "distanceRemaining" to routeProgress.distanceRemaining,
362
364
  "durationRemaining" to routeProgress.durationRemaining,
@@ -368,9 +370,10 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
368
370
  ))
369
371
  })
370
372
 
371
- // Location observer — speed limit + location puck
373
+ // ── Location observer ─────────────────────────────────────────────────
374
+ // EXACT same as official example
372
375
  nav.registerLocationObserver(object : LocationObserver {
373
- override fun onNewRawLocation(rawLocation: com.mapbox.common.location.Location) {}
376
+ override fun onNewRawLocation(rawLocation: Location) {}
374
377
 
375
378
  override fun onNewLocationMatcherResult(locationMatcherResult: LocationMatcherResult) {
376
379
  val enhancedLocation = locationMatcherResult.enhancedLocation
@@ -382,98 +385,117 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
382
385
  )
383
386
 
384
387
  // Update camera
385
- viewportDataSource?.onLocationChanged(enhancedLocation)
386
- viewportDataSource?.evaluate()
388
+ viewportDataSource.onLocationChanged(enhancedLocation)
389
+ viewportDataSource.evaluate()
387
390
 
388
- // ── Speed limit (issue — show posted + current speed) ──────
389
- val formatter = DistanceFormatterOptions.Builder(context).build()
390
- val info = speedInfoApi?.updatePostedAndCurrentSpeed(
391
+ // Speed limit display
392
+ val formatterOptions = DistanceFormatterOptions.Builder(context).build()
393
+ val speedInfo = speedInfoApi.updatePostedAndCurrentSpeed(
391
394
  locationMatcherResult,
392
- formatter
395
+ formatterOptions
393
396
  )
394
- if (info != null) {
395
- speedInfoView.visibility = View.VISIBLE
396
- speedInfoView.render(info)
397
+ if (speedInfo != null) {
398
+ speedInfoView?.visibility = View.VISIBLE
399
+ speedInfoView?.render(speedInfo)
397
400
  } else {
398
- speedInfoView.visibility = View.GONE
401
+ speedInfoView?.visibility = View.GONE
399
402
  }
400
403
 
401
- // ── Auto day/night switch ──────────────────────────────────
404
+ // Auto day/night switch
402
405
  checkAndSwitchDayNight()
403
406
  }
404
407
  })
405
408
  }
406
409
 
407
410
  // ─────────────────────────────────────────────────────────────────────────
408
- // Auto day/night mode
411
+ // 5. ETA bar update
412
+ // TripProgressUpdateValue fields confirmed from official tripdata API
413
+ // ─────────────────────────────────────────────────────────────────────────
414
+ private fun updateEtaBar(tripProgress: com.mapbox.navigation.tripdata.progress.model.TripProgressUpdateValue) {
415
+ // ETA arrival time
416
+ val etaSec = tripProgress.formatter.timeRemainingFormatter
417
+ .formatTime(tripProgress).toString()
418
+ // Calculate arrival clock time
419
+ val totalSec = tripProgress.totalTimeRemaining.toLong()
420
+ val cal = Calendar.getInstance().apply { add(Calendar.SECOND, totalSec.toInt()) }
421
+ tvEtaTime?.text = String.format("%02d:%02d",
422
+ cal.get(Calendar.HOUR_OF_DAY),
423
+ cal.get(Calendar.MINUTE)
424
+ )
425
+
426
+ // Duration remaining
427
+ val totalMin = (tripProgress.totalTimeRemaining / 60).toInt()
428
+ tvDuration?.text = if (totalMin >= 60)
429
+ "${totalMin / 60}h ${totalMin % 60}min"
430
+ else
431
+ "${totalMin} min"
432
+
433
+ // Distance remaining
434
+ val distM = tripProgress.distanceRemaining
435
+ tvDistance?.text = if (distM >= 1000)
436
+ String.format("%.1f km", distM / 1000.0)
437
+ else
438
+ "${distM.toInt()} m"
439
+ }
440
+
441
+ // ─────────────────────────────────────────────────────────────────────────
442
+ // 6. Day / Night auto switch
409
443
  // ─────────────────────────────────────────────────────────────────────────
410
444
  private fun getAutoStyle(): String {
411
445
  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
446
+ return if (hour in 6..20) NavigationStyles.NAVIGATION_DAY_STYLE
447
+ else NavigationStyles.NAVIGATION_NIGHT_STYLE
416
448
  }
417
449
 
418
450
  private fun checkAndSwitchDayNight() {
419
451
  val hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY)
420
452
  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
- }
453
+ if (shouldBeNight == isNightMode) return
454
+ isNightMode = shouldBeNight
455
+
456
+ val newStyle = if (shouldBeNight) NavigationStyles.NAVIGATION_NIGHT_STYLE
457
+ else NavigationStyles.NAVIGATION_DAY_STYLE
458
+
459
+ mapView.mapboxMap.loadStyle(newStyle) { style ->
460
+ routeLineView.initializeLayers(style)
461
+ mapboxNavigation?.getNavigationRoutes()?.takeIf { it.isNotEmpty() }?.let { routes ->
462
+ routeLineApi.setNavigationRoutes(routes) { value ->
463
+ routeLineView.renderRouteDrawData(style, value)
440
464
  }
441
465
  }
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
466
  }
467
+
468
+ etaBar?.setBackgroundColor(
469
+ if (shouldBeNight) Color.parseColor("#1E2433") else Color.WHITE
470
+ )
471
+ val tc = if (shouldBeNight) Color.WHITE else Color.BLACK
472
+ tvEtaTime?.setTextColor(tc)
473
+ tvDuration?.setTextColor(if (shouldBeNight) Color.LTGRAY else Color.DKGRAY)
474
+ tvDistance?.setTextColor(Color.GRAY)
453
475
  }
454
476
 
455
477
  // ─────────────────────────────────────────────────────────────────────────
456
- // Cancel navigation
478
+ // 7. Cancel navigation
457
479
  // ─────────────────────────────────────────────────────────────────────────
458
480
  private fun cancelNavigation() {
459
- mapboxNavigation?.setNavigationRoutes(emptyList())
481
+ mapboxNavigation?.setNavigationRoutes(listOf())
460
482
  mapboxNavigation?.stopTripSession()
461
- hideNavigationUI()
483
+ hideUI()
462
484
  onNavigationCancelled(mapOf<String, Any>())
463
485
  }
464
486
 
465
- private fun hideNavigationUI() {
466
- maneuverView.visibility = View.GONE
467
- speedInfoView.visibility = View.GONE
468
- etaBottomBar.visibility = View.GONE
487
+ private fun hideUI() {
488
+ maneuverView?.visibility = View.INVISIBLE
489
+ speedInfoView?.visibility = View.GONE
490
+ etaBar?.visibility = View.INVISIBLE
469
491
  }
470
492
 
471
493
  // ─────────────────────────────────────────────────────────────────────────
472
- // Issue #43: safe camera
494
+ // 8. Issue #43 safe camera following
473
495
  // ─────────────────────────────────────────────────────────────────────────
474
496
  private fun safeCameraFollowing() {
475
497
  try {
476
- navigationCamera?.requestNavigationCameraToFollowing(
498
+ navigationCamera.requestNavigationCameraToFollowing(
477
499
  stateTransitionOptions = NavigationCameraTransitionOptions.Builder()
478
500
  .maxDuration(0)
479
501
  .build()
@@ -484,7 +506,7 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
484
506
  }
485
507
 
486
508
  // ─────────────────────────────────────────────────────────────────────────
487
- // Issue #31: voice units
509
+ // 9. Issue #31 Voice units
488
510
  // ─────────────────────────────────────────────────────────────────────────
489
511
  private fun resolveVoiceUnits(): String {
490
512
  return when (voiceUnits?.lowercase()) {
@@ -498,7 +520,7 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
498
520
  }
499
521
 
500
522
  // ─────────────────────────────────────────────────────────────────────────
501
- // Route request
523
+ // 10. Route request
502
524
  // ─────────────────────────────────────────────────────────────────────────
503
525
  @SuppressLint("MissingPermission")
504
526
  private fun fetchRoutes() {
@@ -515,16 +537,13 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
515
537
  .language(locale.toLanguageTag())
516
538
  .voiceUnits(resolveVoiceUnits())
517
539
  .coordinatesList(points)
518
- // maxspeed annotation required for speed limit display
519
540
  .annotations("maxspeed,congestion,duration")
520
541
 
521
542
  waypointIndices?.let { builder.waypointIndicesList(it) }
522
543
  navigationProfile?.let {
523
544
  builder.profile(if (it.startsWith("mapbox/")) it else "mapbox/$it")
524
545
  }
525
- excludeTypes?.takeIf { it.isNotEmpty() }?.let {
526
- builder.exclude(it.joinToString(","))
527
- }
546
+ excludeTypes?.takeIf { it.isNotEmpty() }?.let { builder.exclude(it.joinToString(",")) }
528
547
  maxHeight?.let { builder.maxHeight(it) }
529
548
  maxWidth?.let { builder.maxWidth(it) }
530
549
 
@@ -541,12 +560,10 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
541
560
  }
542
561
  nav.setNavigationRoutes(routes)
543
562
  nav.startTripSession()
563
+ safeCameraFollowing()
544
564
  }
545
565
 
546
- override fun onFailure(
547
- reasons: List<RouterFailure>,
548
- routeOptions: RouteOptions
549
- ) {
566
+ override fun onFailure(reasons: List<RouterFailure>, routeOptions: RouteOptions) {
550
567
  val msg = reasons.firstOrNull()?.message ?: "Unknown error"
551
568
  Log.e(TAG, "Route request failed: $msg")
552
569
  onRoutesFailed(mapOf("message" to msg))
@@ -583,9 +600,9 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
583
600
  // ── Lifecycle ─────────────────────────────────────────────────────────────
584
601
  override fun onDetachedFromWindow() {
585
602
  super.onDetachedFromWindow()
586
- maneuverApi?.cancel()
587
- routeLineApi?.cancel()
588
- routeLineView?.cancel()
603
+ maneuverApi.cancel()
604
+ routeLineApi.cancel()
605
+ routeLineView.cancel()
589
606
  mapboxNavigation?.stopTripSession()
590
607
  MapboxNavigationProvider.destroy()
591
608
  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.0",
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": "*",