@jacques_gordon/expo-mapbox-navigation 2.1.0 → 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.
@@ -58,6 +58,7 @@ import expo.modules.kotlin.AppContext
58
58
  import expo.modules.kotlin.viewevent.EventDispatcher
59
59
  import expo.modules.kotlin.views.ExpoView
60
60
  import java.util.Calendar
61
+ import java.util.Date
61
62
  import java.util.Locale
62
63
 
63
64
  private const val TAG = "ExpoMapboxNavigation"
@@ -65,7 +66,7 @@ private const val TAG = "ExpoMapboxNavigation"
65
66
  class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
66
67
  ExpoView(context, appContext) {
67
68
 
68
- // ── EventDispatchers (correct Expo Modules API for View events) ───────────
69
+ // ── EventDispatchers ──────────────────────────────────────────────────────
69
70
  private val onRouteProgressChanged by EventDispatcher()
70
71
  private val onRoutesReady by EventDispatcher()
71
72
  private val onNavigationFinished by EventDispatcher()
@@ -83,7 +84,7 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
83
84
  private var tvDistance: TextView? = null
84
85
  private var etaBar: LinearLayout? = null
85
86
 
86
- // ── Navigation APIs (from official TurnByTurnExperienceActivity) ──────────
87
+ // ── Navigation APIs ───────────────────────────────────────────────────────
87
88
  private lateinit var navigationCamera: NavigationCamera
88
89
  private lateinit var viewportDataSource: MapboxNavigationViewportDataSource
89
90
  private lateinit var routeLineApi: MapboxRouteLineApi
@@ -95,8 +96,6 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
95
96
  private lateinit var routeArrowView: MapboxRouteArrowView
96
97
  private val navigationLocationProvider = NavigationLocationProvider()
97
98
  private var mapboxNavigation: MapboxNavigation? = null
98
-
99
- // ── Night mode ────────────────────────────────────────────────────────────
100
99
  private var isNightMode = false
101
100
 
102
101
  // ── Props ─────────────────────────────────────────────────────────────────
@@ -121,7 +120,7 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
121
120
  }
122
121
 
123
122
  // ─────────────────────────────────────────────────────────────────────────
124
- // 1. Build programmatic UI
123
+ // Build UI
125
124
  // ─────────────────────────────────────────────────────────────────────────
126
125
  private fun buildUI() {
127
126
  val dp = context.resources.displayMetrics.density
@@ -129,41 +128,37 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
129
128
  layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
130
129
  }
131
130
 
132
- // Full-screen MapView
133
131
  root.addView(mapView, FrameLayout.LayoutParams(
134
132
  FrameLayout.LayoutParams.MATCH_PARENT,
135
133
  FrameLayout.LayoutParams.MATCH_PARENT
136
134
  ))
137
135
 
138
- // ManeuverView — top banner
139
- // NOTE: MapboxManeuverView extends ConstraintLayout, cast to View for addView()
136
+ // ManeuverView — top (cast to View: extends ConstraintLayout)
140
137
  val mv = MapboxManeuverView(context)
141
- val mvParams = FrameLayout.LayoutParams(
138
+ root.addView(mv as View, FrameLayout.LayoutParams(
142
139
  FrameLayout.LayoutParams.MATCH_PARENT,
143
140
  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)
141
+ ).also { it.gravity = Gravity.TOP })
142
+ mv.visibility = View.INVISIBLE
148
143
  maneuverView = mv
149
144
 
150
- // SpeedInfoView — bottom-left
145
+ // SpeedInfoView — bottom-left (cast to View)
151
146
  val siv = MapboxSpeedInfoView(context)
152
- val sivParams = FrameLayout.LayoutParams(
147
+ root.addView(siv as View, FrameLayout.LayoutParams(
153
148
  FrameLayout.LayoutParams.WRAP_CONTENT,
154
149
  FrameLayout.LayoutParams.WRAP_CONTENT
155
- )
156
- sivParams.gravity = Gravity.BOTTOM or Gravity.START
157
- sivParams.setMargins((16 * dp).toInt(), 0, 0, (100 * dp).toInt())
150
+ ).also {
151
+ it.gravity = Gravity.BOTTOM or Gravity.START
152
+ it.setMargins((16 * dp).toInt(), 0, 0, (100 * dp).toInt())
153
+ })
158
154
  siv.visibility = View.GONE
159
- root.addView(siv as View, sivParams)
160
155
  speedInfoView = siv
161
156
 
162
- // TripProgressView embedded in bottom bar
157
+ // TripProgressView (used by the official Mapbox render method)
163
158
  val tpv = MapboxTripProgressView(context)
164
159
  tripProgressView = tpv
165
160
 
166
- // ETA bottom bar
161
+ // Bottom ETA bar
167
162
  val bar = LinearLayout(context).apply {
168
163
  orientation = LinearLayout.HORIZONTAL
169
164
  setBackgroundColor(Color.WHITE)
@@ -175,11 +170,9 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
175
170
  (16 * dp).toInt(), (8 * dp).toInt()
176
171
  )
177
172
  }
178
- val barParams = FrameLayout.LayoutParams(
179
- FrameLayout.LayoutParams.MATCH_PARENT,
180
- (80 * dp).toInt()
181
- )
182
- barParams.gravity = Gravity.BOTTOM
173
+ root.addView(bar, FrameLayout.LayoutParams(
174
+ FrameLayout.LayoutParams.MATCH_PARENT, (80 * dp).toInt()
175
+ ).also { it.gravity = Gravity.BOTTOM })
183
176
 
184
177
  val etaTime = TextView(context).apply {
185
178
  textSize = 22f
@@ -193,8 +186,12 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
193
186
  orientation = LinearLayout.VERTICAL
194
187
  gravity = Gravity.CENTER
195
188
  }
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 }
189
+ val dur = TextView(context).apply {
190
+ textSize = 16f; setTextColor(Color.DKGRAY); gravity = Gravity.CENTER
191
+ }
192
+ val dist = TextView(context).apply {
193
+ textSize = 13f; setTextColor(Color.GRAY); gravity = Gravity.CENTER
194
+ }
198
195
  center.addView(dur)
199
196
  center.addView(dist)
200
197
  bar.addView(center, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 2f))
@@ -202,35 +199,30 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
202
199
  tvDistance = dist
203
200
 
204
201
  val cancelBtn = TextView(context).apply {
205
- text = "✕"
206
- textSize = 22f
207
- setTextColor(Color.DKGRAY)
202
+ text = "✕"; textSize = 22f; setTextColor(Color.DKGRAY)
208
203
  gravity = Gravity.END or Gravity.CENTER_VERTICAL
209
204
  setOnClickListener { cancelNavigation() }
210
205
  }
211
206
  bar.addView(cancelBtn, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f))
212
207
  etaBar = bar
213
- root.addView(bar, barParams)
214
208
 
215
209
  addView(root)
216
210
  }
217
211
 
218
212
  // ─────────────────────────────────────────────────────────────────────────
219
- // 2. Init APIs (from official example — exact same pattern)
213
+ // Init APIs — confirmed from official Mapbox examples
220
214
  // ─────────────────────────────────────────────────────────────────────────
221
215
  private fun initAPIs() {
222
216
  val distanceFormatterOptions = DistanceFormatterOptions.Builder(context).build()
223
217
 
224
- // EXACT same instantiation as TurnByTurnExperienceActivity
225
- maneuverApi = MapboxManeuverApi(
226
- MapboxDistanceFormatter(distanceFormatterOptions)
227
- )
218
+ maneuverApi = MapboxManeuverApi(MapboxDistanceFormatter(distanceFormatterOptions))
228
219
 
220
+ // TripProgressApi — from ShowTripProgressActivity official example
221
+ // PercentDistanceTraveledFormatter is optional — omit if causing issues
229
222
  tripProgressApi = MapboxTripProgressApi(
230
223
  TripProgressUpdateFormatter.Builder(context)
231
224
  .distanceRemainingFormatter(DistanceRemainingFormatter(distanceFormatterOptions))
232
225
  .timeRemainingFormatter(TimeRemainingFormatter(context))
233
- .percentRouteTraveledFormatter(PercentDistanceTraveledFormatter())
234
226
  .estimatedTimeToArrivalFormatter(
235
227
  EstimatedTimeToArrivalFormatter(context, TimeFormat.NONE_SPECIFIED)
236
228
  )
@@ -239,31 +231,27 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
239
231
 
240
232
  speedInfoApi = MapboxSpeedInfoApi()
241
233
 
242
- val routeLineViewOptions = MapboxRouteLineViewOptions.Builder(context)
243
- .routeLineBelowLayerId("road-label-navigation")
244
- .build()
245
234
  routeLineApi = MapboxRouteLineApi(MapboxRouteLineApiOptions.Builder().build())
246
- routeLineView = MapboxRouteLineView(routeLineViewOptions)
235
+ routeLineView = MapboxRouteLineView(
236
+ MapboxRouteLineViewOptions.Builder(context)
237
+ .routeLineBelowLayerId("road-label-navigation")
238
+ .build()
239
+ )
247
240
 
248
- val routeArrowOptions = RouteArrowOptions.Builder(context).build()
249
- routeArrowView = MapboxRouteArrowView(routeArrowOptions)
241
+ routeArrowView = MapboxRouteArrowView(RouteArrowOptions.Builder(context).build())
250
242
  }
251
243
 
252
244
  // ─────────────────────────────────────────────────────────────────────────
253
- // 3. Setup Navigation
245
+ // Setup Navigation
254
246
  // ─────────────────────────────────────────────────────────────────────────
255
247
  private fun setupNavigation() {
256
248
  mapboxNavigation = MapboxNavigationProvider.create(
257
249
  NavigationOptions.Builder(context).build()
258
250
  )
259
251
 
260
- val style = mapStyle ?: getAutoStyle()
261
- mapView.mapboxMap.loadStyle(style) { mapStyle ->
262
-
263
- // Route line layers must be initialized first
264
- routeLineView.initializeLayers(mapStyle)
252
+ mapView.mapboxMap.loadStyle(mapStyle ?: getAutoStyle()) { style ->
253
+ routeLineView.initializeLayers(style)
265
254
 
266
- // Camera setup — EXACT same as official example
267
255
  viewportDataSource = MapboxNavigationViewportDataSource(mapView.mapboxMap)
268
256
  navigationCamera = NavigationCamera(
269
257
  mapView.mapboxMap,
@@ -271,7 +259,6 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
271
259
  viewportDataSource
272
260
  )
273
261
 
274
- // Location puck
275
262
  mapView.location.apply {
276
263
  setLocationProvider(navigationLocationProvider)
277
264
  enabled = true
@@ -282,38 +269,30 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
282
269
  }
283
270
 
284
271
  // ─────────────────────────────────────────────────────────────────────────
285
- // 4. Observers — based 100% on official TurnByTurnExperienceActivity
272
+ // Observers
286
273
  // ─────────────────────────────────────────────────────────────────────────
287
274
  private fun registerObservers() {
288
275
  val nav = mapboxNavigation ?: return
289
276
 
290
- // ── Routes observer ───────────────────────────────────────────────────
277
+ // Routes observer
291
278
  nav.registerRoutesObserver(object : RoutesObserver {
292
279
  override fun onRoutesChanged(result: RoutesUpdatedResult) {
293
280
  if (result.navigationRoutes.isNotEmpty()) {
294
- // Draw route line
295
281
  routeLineApi.setNavigationRoutes(result.navigationRoutes) { value ->
296
282
  mapView.mapboxMap.style?.apply {
297
283
  routeLineView.renderRouteDrawData(this, value)
298
284
  }
299
285
  }
300
- // Update camera viewport
301
286
  viewportDataSource.onRouteChanged(result.navigationRoutes.first())
302
287
  viewportDataSource.evaluate()
303
-
304
- // Show navigation UI
305
- maneuverView?.visibility = View.VISIBLE
306
- etaBar?.visibility = View.VISIBLE
307
-
288
+ showUI()
308
289
  onRoutesReady(mapOf(
309
290
  "routeCount" to result.navigationRoutes.size,
310
291
  "distanceMeters" to (result.navigationRoutes.first().directionsRoute.distance() ?: 0.0),
311
292
  "durationSeconds" to (result.navigationRoutes.first().directionsRoute.duration() ?: 0.0)
312
293
  ))
313
294
  } else {
314
- // Clear route line
315
- val style = mapView.mapboxMap.style
316
- if (style != null) {
295
+ mapView.mapboxMap.style?.let { style ->
317
296
  routeLineApi.clearRouteLine { value ->
318
297
  routeLineView.renderClearRouteLineValue(style, value)
319
298
  }
@@ -326,39 +305,48 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
326
305
  }
327
306
  })
328
307
 
329
- // ── Route progress observer ───────────────────────────────────────────
330
- // EXACT same pattern as TurnByTurnExperienceActivity.routeProgressObserver
308
+ // Route progress observer
331
309
  nav.registerRouteProgressObserver(RouteProgressObserver { routeProgress ->
332
- // Update camera
333
310
  viewportDataSource.onRouteProgressChanged(routeProgress)
334
311
  viewportDataSource.evaluate()
335
312
 
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)
313
+ // Route arrow
314
+ mapView.mapboxMap.style?.let { style ->
315
+ val arrowResult = routeArrowApi.addUpcomingManeuverArrow(routeProgress)
316
+ routeArrowView.renderManeuverUpdate(style, arrowResult)
341
317
  }
342
318
 
343
- // Maneuver banner EXACT same fold() pattern as official example
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
344
322
  val maneuvers = maneuverApi.getManeuvers(routeProgress)
345
323
  maneuvers.fold(
346
- { error -> Log.w(TAG, "Maneuver error: ${error.errorMessage}") },
347
- {
324
+ { error ->
325
+ Log.w(TAG, "Maneuver error: ${error.errorMessage}")
326
+ Unit
327
+ },
328
+ { _ ->
348
329
  maneuverView?.visibility = View.VISIBLE
349
330
  maneuverView?.renderManeuvers(maneuvers)
331
+ Unit
350
332
  }
351
333
  )
352
334
 
353
- // Trip progress — EXACT same as official example
354
- // getTripProgress returns TripProgressUpdateValue directly (not nullable)
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)
355
342
  val tripProgress = tripProgressApi.getTripProgress(routeProgress)
356
343
  tripProgressView?.render(tripProgress)
344
+ updateEtaBar(
345
+ tripProgress.estimatedTimeToArrival,
346
+ tripProgress.totalTimeRemaining,
347
+ tripProgress.distanceRemaining
348
+ )
357
349
 
358
- // Update our custom ETA bar from tripProgress fields
359
- updateEtaBar(tripProgress)
360
-
361
- // Emit event
362
350
  onRouteProgressChanged(mapOf(
363
351
  "distanceRemaining" to routeProgress.distanceRemaining,
364
352
  "durationRemaining" to routeProgress.durationRemaining,
@@ -370,30 +358,21 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
370
358
  ))
371
359
  })
372
360
 
373
- // ── Location observer ─────────────────────────────────────────────────
374
- // EXACT same as official example
361
+ // Location observer
375
362
  nav.registerLocationObserver(object : LocationObserver {
376
363
  override fun onNewRawLocation(rawLocation: Location) {}
377
-
378
- override fun onNewLocationMatcherResult(locationMatcherResult: LocationMatcherResult) {
379
- val enhancedLocation = locationMatcherResult.enhancedLocation
380
-
381
- // Update location puck
364
+ override fun onNewLocationMatcherResult(result: LocationMatcherResult) {
365
+ val loc = result.enhancedLocation
382
366
  navigationLocationProvider.changePosition(
383
- location = enhancedLocation,
384
- keyPoints = locationMatcherResult.keyPoints,
367
+ location = loc,
368
+ keyPoints = result.keyPoints,
385
369
  )
386
-
387
- // Update camera
388
- viewportDataSource.onLocationChanged(enhancedLocation)
370
+ viewportDataSource.onLocationChanged(loc)
389
371
  viewportDataSource.evaluate()
390
372
 
391
- // Speed limit display
392
- val formatterOptions = DistanceFormatterOptions.Builder(context).build()
393
- val speedInfo = speedInfoApi.updatePostedAndCurrentSpeed(
394
- locationMatcherResult,
395
- formatterOptions
396
- )
373
+ // Speed limit
374
+ val fmtOptions = DistanceFormatterOptions.Builder(context).build()
375
+ val speedInfo = speedInfoApi.updatePostedAndCurrentSpeed(result, fmtOptions)
397
376
  if (speedInfo != null) {
398
377
  speedInfoView?.visibility = View.VISIBLE
399
378
  speedInfoView?.render(speedInfo)
@@ -401,45 +380,48 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
401
380
  speedInfoView?.visibility = View.GONE
402
381
  }
403
382
 
404
- // Auto day/night switch
405
383
  checkAndSwitchDayNight()
406
384
  }
407
385
  })
408
386
  }
409
387
 
410
388
  // ─────────────────────────────────────────────────────────────────────────
411
- // 5. ETA bar update
412
- // TripProgressUpdateValue fields confirmed from official tripdata API
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
413
393
  // ─────────────────────────────────────────────────────────────────────────
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)
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)
424
407
  )
425
408
 
426
409
  // Duration remaining
427
- val totalMin = (tripProgress.totalTimeRemaining / 60).toInt()
410
+ val totalMin = (totalTimeRemainingSec / 60).toInt()
428
411
  tvDuration?.text = if (totalMin >= 60)
429
412
  "${totalMin / 60}h ${totalMin % 60}min"
430
413
  else
431
414
  "${totalMin} min"
432
415
 
433
416
  // Distance remaining
434
- val distM = tripProgress.distanceRemaining
435
- tvDistance?.text = if (distM >= 1000)
436
- String.format("%.1f km", distM / 1000.0)
417
+ tvDistance?.text = if (distanceRemainingMetres >= 1000)
418
+ String.format("%.1f km", distanceRemainingMetres / 1000.0)
437
419
  else
438
- "${distM.toInt()} m"
420
+ "${distanceRemainingMetres.toInt()} m"
439
421
  }
440
422
 
441
423
  // ─────────────────────────────────────────────────────────────────────────
442
- // 6. Day / Night auto switch
424
+ // Day / Night
443
425
  // ─────────────────────────────────────────────────────────────────────────
444
426
  private fun getAutoStyle(): String {
445
427
  val hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY)
@@ -452,10 +434,8 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
452
434
  val shouldBeNight = hour !in 6..20
453
435
  if (shouldBeNight == isNightMode) return
454
436
  isNightMode = shouldBeNight
455
-
456
437
  val newStyle = if (shouldBeNight) NavigationStyles.NAVIGATION_NIGHT_STYLE
457
438
  else NavigationStyles.NAVIGATION_DAY_STYLE
458
-
459
439
  mapView.mapboxMap.loadStyle(newStyle) { style ->
460
440
  routeLineView.initializeLayers(style)
461
441
  mapboxNavigation?.getNavigationRoutes()?.takeIf { it.isNotEmpty() }?.let { routes ->
@@ -464,10 +444,7 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
464
444
  }
465
445
  }
466
446
  }
467
-
468
- etaBar?.setBackgroundColor(
469
- if (shouldBeNight) Color.parseColor("#1E2433") else Color.WHITE
470
- )
447
+ etaBar?.setBackgroundColor(if (shouldBeNight) Color.parseColor("#1E2433") else Color.WHITE)
471
448
  val tc = if (shouldBeNight) Color.WHITE else Color.BLACK
472
449
  tvEtaTime?.setTextColor(tc)
473
450
  tvDuration?.setTextColor(if (shouldBeNight) Color.LTGRAY else Color.DKGRAY)
@@ -475,7 +452,7 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
475
452
  }
476
453
 
477
454
  // ─────────────────────────────────────────────────────────────────────────
478
- // 7. Cancel navigation
455
+ // Cancel navigation
479
456
  // ─────────────────────────────────────────────────────────────────────────
480
457
  private fun cancelNavigation() {
481
458
  mapboxNavigation?.setNavigationRoutes(listOf())
@@ -484,30 +461,30 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
484
461
  onNavigationCancelled(mapOf<String, Any>())
485
462
  }
486
463
 
464
+ private fun showUI() {
465
+ maneuverView?.visibility = View.VISIBLE
466
+ etaBar?.visibility = View.VISIBLE
467
+ }
468
+
487
469
  private fun hideUI() {
488
470
  maneuverView?.visibility = View.INVISIBLE
489
471
  speedInfoView?.visibility = View.GONE
490
472
  etaBar?.visibility = View.INVISIBLE
491
473
  }
492
474
 
493
- // ─────────────────────────────────────────────────────────────────────────
494
- // 8. Issue #43 — safe camera following
495
- // ─────────────────────────────────────────────────────────────────────────
475
+ // Issue #43
496
476
  private fun safeCameraFollowing() {
497
477
  try {
498
478
  navigationCamera.requestNavigationCameraToFollowing(
499
479
  stateTransitionOptions = NavigationCameraTransitionOptions.Builder()
500
- .maxDuration(0)
501
- .build()
480
+ .maxDuration(0).build()
502
481
  )
503
482
  } catch (e: Exception) {
504
- Log.e(TAG, "Camera transition error: ${e.message}")
483
+ Log.e(TAG, "Camera error: ${e.message}")
505
484
  }
506
485
  }
507
486
 
508
- // ─────────────────────────────────────────────────────────────────────────
509
- // 9. Issue #31 — Voice units
510
- // ─────────────────────────────────────────────────────────────────────────
487
+ // Issue #31
511
488
  private fun resolveVoiceUnits(): String {
512
489
  return when (voiceUnits?.lowercase()) {
513
490
  "metric" -> "metric"
@@ -519,71 +496,44 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
519
496
  }
520
497
  }
521
498
 
522
- // ─────────────────────────────────────────────────────────────────────────
523
- // 10. Route request
524
- // ─────────────────────────────────────────────────────────────────────────
499
+ // Route request
525
500
  @SuppressLint("MissingPermission")
526
501
  private fun fetchRoutes() {
527
502
  val nav = mapboxNavigation ?: return
528
503
  if (coordinates.size < 2) return
529
-
530
- val points = coordinates.map { coord ->
531
- Point.fromLngLat(coord["longitude"] ?: 0.0, coord["latitude"] ?: 0.0)
532
- }
504
+ val points = coordinates.map { Point.fromLngLat(it["longitude"] ?: 0.0, it["latitude"] ?: 0.0) }
533
505
  val locale = language?.let { Locale.forLanguageTag(it) } ?: Locale.getDefault()
534
-
535
506
  val builder = RouteOptions.builder()
536
507
  .applyDefaultNavigationOptions()
537
508
  .language(locale.toLanguageTag())
538
509
  .voiceUnits(resolveVoiceUnits())
539
510
  .coordinatesList(points)
540
511
  .annotations("maxspeed,congestion,duration")
541
-
542
512
  waypointIndices?.let { builder.waypointIndicesList(it) }
543
- navigationProfile?.let {
544
- builder.profile(if (it.startsWith("mapbox/")) it else "mapbox/$it")
545
- }
513
+ navigationProfile?.let { builder.profile(if (it.startsWith("mapbox/")) it else "mapbox/$it") }
546
514
  excludeTypes?.takeIf { it.isNotEmpty() }?.let { builder.exclude(it.joinToString(",")) }
547
515
  maxHeight?.let { builder.maxHeight(it) }
548
516
  maxWidth?.let { builder.maxWidth(it) }
549
-
550
- nav.requestRoutes(
551
- builder.build(),
552
- object : NavigationRouterCallback {
553
- override fun onRoutesReady(
554
- routes: List<NavigationRoute>,
555
- @RouterOrigin routerOrigin: String
556
- ) {
557
- if (routes.isEmpty()) {
558
- onRoutesFailed(mapOf("message" to "No routes returned"))
559
- return
560
- }
561
- nav.setNavigationRoutes(routes)
562
- nav.startTripSession()
563
- safeCameraFollowing()
564
- }
565
-
566
- override fun onFailure(reasons: List<RouterFailure>, routeOptions: RouteOptions) {
567
- val msg = reasons.firstOrNull()?.message ?: "Unknown error"
568
- Log.e(TAG, "Route request failed: $msg")
569
- onRoutesFailed(mapOf("message" to msg))
570
- }
571
-
572
- override fun onCanceled(
573
- routeOptions: RouteOptions,
574
- @RouterOrigin routerOrigin: String
575
- ) {
576
- Log.d(TAG, "Route request cancelled")
577
- }
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()
578
523
  }
579
- )
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
+ })
580
533
  }
581
534
 
582
- // ── Prop setters ──────────────────────────────────────────────────────────
583
- fun setCoordinates(coords: List<Map<String, Double>>) {
584
- coordinates = coords
585
- if (coords.size >= 2) fetchRoutes()
586
- }
535
+ // Prop setters
536
+ fun setCoordinates(coords: List<Map<String, Double>>) { coordinates = coords; if (coords.size >= 2) fetchRoutes() }
587
537
  fun setWaypointIndices(indices: List<Int>?) { waypointIndices = indices }
588
538
  fun setLanguage(lang: String?) { language = lang }
589
539
  fun setVoiceUnits(units: String?) { voiceUnits = units }
@@ -597,7 +547,6 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
597
547
  fun setCustomRasterTileUrl(url: String?) { customRasterTileUrl = url }
598
548
  fun setCustomRasterAboveLayerId(layerId: String?) { customRasterAboveLayerId = layerId }
599
549
 
600
- // ── Lifecycle ─────────────────────────────────────────────────────────────
601
550
  override fun onDetachedFromWindow() {
602
551
  super.onDetachedFromWindow()
603
552
  maneuverApi.cancel()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jacques_gordon/expo-mapbox-navigation",
3
- "version": "2.1.0",
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",