@jacques_gordon/expo-mapbox-navigation 2.1.1 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,6 +2,7 @@ package expo.modules.mapboxnavigation
2
2
 
3
3
  import android.annotation.SuppressLint
4
4
  import android.content.Context
5
+ import android.content.res.Resources
5
6
  import android.graphics.Color
6
7
  import android.util.Log
7
8
  import android.view.Gravity
@@ -12,6 +13,7 @@ import android.widget.TextView
12
13
  import com.mapbox.api.directions.v5.models.RouteOptions
13
14
  import com.mapbox.common.location.Location
14
15
  import com.mapbox.geojson.Point
16
+ import com.mapbox.maps.EdgeInsets
15
17
  import com.mapbox.maps.MapView
16
18
  import com.mapbox.maps.plugin.animation.camera
17
19
  import com.mapbox.maps.plugin.locationcomponent.location
@@ -35,7 +37,6 @@ import com.mapbox.navigation.tripdata.maneuver.api.MapboxManeuverApi
35
37
  import com.mapbox.navigation.tripdata.progress.api.MapboxTripProgressApi
36
38
  import com.mapbox.navigation.tripdata.progress.model.DistanceRemainingFormatter
37
39
  import com.mapbox.navigation.tripdata.progress.model.EstimatedTimeToArrivalFormatter
38
- import com.mapbox.navigation.tripdata.progress.model.PercentDistanceTraveledFormatter
39
40
  import com.mapbox.navigation.tripdata.progress.model.TimeRemainingFormatter
40
41
  import com.mapbox.navigation.tripdata.progress.model.TripProgressUpdateFormatter
41
42
  import com.mapbox.navigation.tripdata.speedlimit.api.MapboxSpeedInfoApi
@@ -58,7 +59,6 @@ import expo.modules.kotlin.AppContext
58
59
  import expo.modules.kotlin.viewevent.EventDispatcher
59
60
  import expo.modules.kotlin.views.ExpoView
60
61
  import java.util.Calendar
61
- import java.util.Date
62
62
  import java.util.Locale
63
63
 
64
64
  private const val TAG = "ExpoMapboxNavigation"
@@ -98,6 +98,29 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
98
98
  private var mapboxNavigation: MapboxNavigation? = null
99
99
  private var isNightMode = false
100
100
 
101
+ // ── FIX: Track whether first location has been received ───────────────────
102
+ // Without this, camera never enters following mode on first GPS fix
103
+ private var firstLocationReceived = false
104
+
105
+ // ── Viewport padding (from official TurnByTurnExperienceActivity) ─────────
106
+ private val pixelDensity = Resources.getSystem().displayMetrics.density
107
+ private val followingPadding by lazy {
108
+ EdgeInsets(
109
+ 180.0 * pixelDensity, // top — room for maneuver banner
110
+ 40.0 * pixelDensity,
111
+ 150.0 * pixelDensity, // bottom — room for ETA bar
112
+ 40.0 * pixelDensity
113
+ )
114
+ }
115
+ private val overviewPadding by lazy {
116
+ EdgeInsets(
117
+ 140.0 * pixelDensity,
118
+ 40.0 * pixelDensity,
119
+ 120.0 * pixelDensity,
120
+ 40.0 * pixelDensity
121
+ )
122
+ }
123
+
101
124
  // ── Props ─────────────────────────────────────────────────────────────────
102
125
  private var coordinates: List<Map<String, Double>> = emptyList()
103
126
  private var waypointIndices: List<Int>? = null
@@ -128,12 +151,14 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
128
151
  layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
129
152
  }
130
153
 
154
+ // Full screen map
131
155
  root.addView(mapView, FrameLayout.LayoutParams(
132
156
  FrameLayout.LayoutParams.MATCH_PARENT,
133
157
  FrameLayout.LayoutParams.MATCH_PARENT
134
158
  ))
135
159
 
136
- // ManeuverView — top (cast to View: extends ConstraintLayout)
160
+ // ── ManeuverView — top banner ──────────────────────────────────────────
161
+ // extends ConstraintLayout → must cast to View for addView()
137
162
  val mv = MapboxManeuverView(context)
138
163
  root.addView(mv as View, FrameLayout.LayoutParams(
139
164
  FrameLayout.LayoutParams.MATCH_PARENT,
@@ -142,83 +167,103 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
142
167
  mv.visibility = View.INVISIBLE
143
168
  maneuverView = mv
144
169
 
145
- // SpeedInfoView — bottom-left (cast to View)
170
+ // ── SpeedInfoView — bottom-left ────────────────────────────────────────
171
+ // Shows posted speed limit + current vehicle speed
146
172
  val siv = MapboxSpeedInfoView(context)
147
173
  root.addView(siv as View, FrameLayout.LayoutParams(
148
174
  FrameLayout.LayoutParams.WRAP_CONTENT,
149
175
  FrameLayout.LayoutParams.WRAP_CONTENT
150
176
  ).also {
151
177
  it.gravity = Gravity.BOTTOM or Gravity.START
152
- it.setMargins((16 * dp).toInt(), 0, 0, (100 * dp).toInt())
178
+ // Position above the ETA bar (80dp) + margin
179
+ it.setMargins((16 * dp).toInt(), 0, 0, (96 * dp).toInt())
153
180
  })
154
181
  siv.visibility = View.GONE
155
182
  speedInfoView = siv
156
183
 
157
- // TripProgressView (used by the official Mapbox render method)
158
- val tpv = MapboxTripProgressView(context)
159
- tripProgressView = tpv
160
-
161
- // Bottom ETA bar
184
+ // ── ETA bottom bar ─────────────────────────────────────────────────────
162
185
  val bar = LinearLayout(context).apply {
163
186
  orientation = LinearLayout.HORIZONTAL
164
- setBackgroundColor(Color.WHITE)
187
+ setBackgroundColor(Color.parseColor("#1E2433")) // dark nav style
165
188
  elevation = 8 * dp
166
189
  visibility = View.INVISIBLE
167
190
  gravity = Gravity.CENTER_VERTICAL
168
191
  setPadding(
169
- (16 * dp).toInt(), (8 * dp).toInt(),
170
- (16 * dp).toInt(), (8 * dp).toInt()
192
+ (16 * dp).toInt(), (12 * dp).toInt(),
193
+ (16 * dp).toInt(), (12 * dp).toInt()
171
194
  )
172
195
  }
173
196
  root.addView(bar, FrameLayout.LayoutParams(
174
- FrameLayout.LayoutParams.MATCH_PARENT, (80 * dp).toInt()
197
+ FrameLayout.LayoutParams.MATCH_PARENT,
198
+ FrameLayout.LayoutParams.WRAP_CONTENT
175
199
  ).also { it.gravity = Gravity.BOTTOM })
176
200
 
201
+ // ETA arrival time
177
202
  val etaTime = TextView(context).apply {
178
- textSize = 22f
179
- setTextColor(Color.BLACK)
203
+ textSize = 24f
204
+ setTextColor(Color.WHITE)
180
205
  typeface = android.graphics.Typeface.DEFAULT_BOLD
181
206
  }
182
- bar.addView(etaTime, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f))
207
+ bar.addView(etaTime, LinearLayout.LayoutParams(
208
+ 0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f
209
+ ))
183
210
  tvEtaTime = etaTime
184
211
 
212
+ // Duration + distance (center)
185
213
  val center = LinearLayout(context).apply {
186
214
  orientation = LinearLayout.VERTICAL
187
215
  gravity = Gravity.CENTER
188
216
  }
189
217
  val dur = TextView(context).apply {
190
- textSize = 16f; setTextColor(Color.DKGRAY); gravity = Gravity.CENTER
218
+ textSize = 18f
219
+ setTextColor(Color.WHITE)
220
+ typeface = android.graphics.Typeface.DEFAULT_BOLD
221
+ gravity = Gravity.CENTER
191
222
  }
192
223
  val dist = TextView(context).apply {
193
- textSize = 13f; setTextColor(Color.GRAY); gravity = Gravity.CENTER
224
+ textSize = 13f
225
+ setTextColor(Color.parseColor("#AAAAAA"))
226
+ gravity = Gravity.CENTER
194
227
  }
195
228
  center.addView(dur)
196
229
  center.addView(dist)
197
- bar.addView(center, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 2f))
230
+ bar.addView(center, LinearLayout.LayoutParams(
231
+ 0, LinearLayout.LayoutParams.WRAP_CONTENT, 2f
232
+ ))
198
233
  tvDuration = dur
199
234
  tvDistance = dist
200
235
 
236
+ // Cancel button
201
237
  val cancelBtn = TextView(context).apply {
202
- text = "✕"; textSize = 22f; setTextColor(Color.DKGRAY)
238
+ text = "✕"
239
+ textSize = 22f
240
+ setTextColor(Color.parseColor("#AAAAAA"))
203
241
  gravity = Gravity.END or Gravity.CENTER_VERTICAL
204
242
  setOnClickListener { cancelNavigation() }
205
243
  }
206
- bar.addView(cancelBtn, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f))
244
+ bar.addView(cancelBtn, LinearLayout.LayoutParams(
245
+ 0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f
246
+ ))
207
247
  etaBar = bar
208
248
 
249
+ // ── TripProgressView — hidden native view (used for render() API) ──────
250
+ // FIX: was created but never added to root — now added but invisible
251
+ val tpv = MapboxTripProgressView(context)
252
+ root.addView(tpv as View, FrameLayout.LayoutParams(1, 1)) // 1x1 hidden
253
+ tpv.visibility = View.GONE
254
+ tripProgressView = tpv
255
+
209
256
  addView(root)
210
257
  }
211
258
 
212
259
  // ─────────────────────────────────────────────────────────────────────────
213
- // Init APIs — confirmed from official Mapbox examples
260
+ // Init APIs
214
261
  // ─────────────────────────────────────────────────────────────────────────
215
262
  private fun initAPIs() {
216
263
  val distanceFormatterOptions = DistanceFormatterOptions.Builder(context).build()
217
264
 
218
265
  maneuverApi = MapboxManeuverApi(MapboxDistanceFormatter(distanceFormatterOptions))
219
266
 
220
- // TripProgressApi — from ShowTripProgressActivity official example
221
- // PercentDistanceTraveledFormatter is optional — omit if causing issues
222
267
  tripProgressApi = MapboxTripProgressApi(
223
268
  TripProgressUpdateFormatter.Builder(context)
224
269
  .distanceRemainingFormatter(DistanceRemainingFormatter(distanceFormatterOptions))
@@ -252,13 +297,21 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
252
297
  mapView.mapboxMap.loadStyle(mapStyle ?: getAutoStyle()) { style ->
253
298
  routeLineView.initializeLayers(style)
254
299
 
300
+ // Camera viewport data source
255
301
  viewportDataSource = MapboxNavigationViewportDataSource(mapView.mapboxMap)
302
+
303
+ // FIX: Set padding BEFORE creating NavigationCamera
304
+ // Without this, the camera doesn't account for the maneuver banner and ETA bar
305
+ viewportDataSource.followingPadding = followingPadding
306
+ viewportDataSource.overviewPadding = overviewPadding
307
+
256
308
  navigationCamera = NavigationCamera(
257
309
  mapView.mapboxMap,
258
310
  mapView.camera,
259
311
  viewportDataSource
260
312
  )
261
313
 
314
+ // Location puck
262
315
  mapView.location.apply {
263
316
  setLocationProvider(navigationLocationProvider)
264
317
  enabled = true
@@ -274,7 +327,7 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
274
327
  private fun registerObservers() {
275
328
  val nav = mapboxNavigation ?: return
276
329
 
277
- // Routes observer
330
+ // ── Routes observer ───────────────────────────────────────────────────
278
331
  nav.registerRoutesObserver(object : RoutesObserver {
279
332
  override fun onRoutesChanged(result: RoutesUpdatedResult) {
280
333
  if (result.navigationRoutes.isNotEmpty()) {
@@ -285,7 +338,12 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
285
338
  }
286
339
  viewportDataSource.onRouteChanged(result.navigationRoutes.first())
287
340
  viewportDataSource.evaluate()
341
+
342
+ // FIX: transition to following mode when route is set
343
+ // This triggers the camera to zoom/pitch/center correctly
344
+ safeCameraFollowing()
288
345
  showUI()
346
+
289
347
  onRoutesReady(mapOf(
290
348
  "routeCount" to result.navigationRoutes.size,
291
349
  "distanceMeters" to (result.navigationRoutes.first().directionsRoute.distance() ?: 0.0),
@@ -305,20 +363,19 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
305
363
  }
306
364
  })
307
365
 
308
- // Route progress observer
366
+ // ── Route progress observer ───────────────────────────────────────────
309
367
  nav.registerRouteProgressObserver(RouteProgressObserver { routeProgress ->
368
+ // Update viewport with progress (camera follows route progress)
310
369
  viewportDataSource.onRouteProgressChanged(routeProgress)
311
370
  viewportDataSource.evaluate()
312
371
 
313
- // Route arrow
372
+ // Draw maneuver arrow
314
373
  mapView.mapboxMap.style?.let { style ->
315
374
  val arrowResult = routeArrowApi.addUpcomingManeuverArrow(routeProgress)
316
375
  routeArrowView.renderManeuverUpdate(style, arrowResult)
317
376
  }
318
377
 
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
378
+ // Maneuver banner
322
379
  val maneuvers = maneuverApi.getManeuvers(routeProgress)
323
380
  maneuvers.fold(
324
381
  { error ->
@@ -332,13 +389,7 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
332
389
  }
333
390
  )
334
391
 
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)
392
+ // Trip progress
342
393
  val tripProgress = tripProgressApi.getTripProgress(routeProgress)
343
394
  tripProgressView?.render(tripProgress)
344
395
  updateEtaBar(
@@ -358,19 +409,32 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
358
409
  ))
359
410
  })
360
411
 
361
- // Location observer
412
+ // ── Location observer ─────────────────────────────────────────────────
362
413
  nav.registerLocationObserver(object : LocationObserver {
363
414
  override fun onNewRawLocation(rawLocation: Location) {}
415
+
364
416
  override fun onNewLocationMatcherResult(result: LocationMatcherResult) {
365
417
  val loc = result.enhancedLocation
418
+
419
+ // Update location puck position
366
420
  navigationLocationProvider.changePosition(
367
421
  location = loc,
368
422
  keyPoints = result.keyPoints,
369
423
  )
424
+
425
+ // FIX: Update viewport data source with location AND bearing
426
+ // This is what drives the camera to follow the vehicle's direction
370
427
  viewportDataSource.onLocationChanged(loc)
371
428
  viewportDataSource.evaluate()
372
429
 
373
- // Speed limit
430
+ // FIX: On first location fix, immediately enter following mode
431
+ // This ensures the camera centers on the user as soon as GPS is available
432
+ if (!firstLocationReceived) {
433
+ firstLocationReceived = true
434
+ safeCameraFollowing()
435
+ }
436
+
437
+ // ── Speed limit display ────────────────────────────────────────
374
438
  val fmtOptions = DistanceFormatterOptions.Builder(context).build()
375
439
  val speedInfo = speedInfoApi.updatePostedAndCurrentSpeed(result, fmtOptions)
376
440
  if (speedInfo != null) {
@@ -386,17 +450,17 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
386
450
  }
387
451
 
388
452
  // ─────────────────────────────────────────────────────────────────────────
389
- // ETA bar — using confirmed TripProgressUpdateValue fields
390
- // estimatedTimeToArrival: Long Unix timestamp (ms) of expected arrival
391
- // totalTimeRemaining: Doubleseconds remaining
392
- // distanceRemaining: Double — metres remaining
453
+ // ETA bar update
454
+ // Fields confirmed from TripProgressUpdateValue docs:
455
+ // estimatedTimeToArrival: LongUnix timestamp ms
456
+ // totalTimeRemaining: Double — seconds
457
+ // distanceRemaining: Double — metres
393
458
  // ─────────────────────────────────────────────────────────────────────────
394
459
  private fun updateEtaBar(
395
460
  estimatedTimeToArrivalMs: Long,
396
461
  totalTimeRemainingSec: Double,
397
462
  distanceRemainingMetres: Double
398
463
  ) {
399
- // Arrival clock time from Unix timestamp
400
464
  val arrivalCal = Calendar.getInstance().apply {
401
465
  timeInMillis = estimatedTimeToArrivalMs
402
466
  }
@@ -406,14 +470,12 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
406
470
  arrivalCal.get(Calendar.MINUTE)
407
471
  )
408
472
 
409
- // Duration remaining
410
473
  val totalMin = (totalTimeRemainingSec / 60).toInt()
411
474
  tvDuration?.text = if (totalMin >= 60)
412
475
  "${totalMin / 60}h ${totalMin % 60}min"
413
476
  else
414
477
  "${totalMin} min"
415
478
 
416
- // Distance remaining
417
479
  tvDistance?.text = if (distanceRemainingMetres >= 1000)
418
480
  String.format("%.1f km", distanceRemainingMetres / 1000.0)
419
481
  else
@@ -421,7 +483,7 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
421
483
  }
422
484
 
423
485
  // ─────────────────────────────────────────────────────────────────────────
424
- // Day / Night
486
+ // Day / Night auto switch
425
487
  // ─────────────────────────────────────────────────────────────────────────
426
488
  private fun getAutoStyle(): String {
427
489
  val hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY)
@@ -444,19 +506,31 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
444
506
  }
445
507
  }
446
508
  }
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)
452
509
  }
453
510
 
454
511
  // ─────────────────────────────────────────────────────────────────────────
455
- // Cancel navigation
512
+ // Camera following — safe wrapper (issue #43)
513
+ // FIX: maxDuration(0) = instant transition, no animation delay
514
+ // ─────────────────────────────────────────────────────────────────────────
515
+ private fun safeCameraFollowing() {
516
+ try {
517
+ navigationCamera.requestNavigationCameraToFollowing(
518
+ stateTransitionOptions = NavigationCameraTransitionOptions.Builder()
519
+ .maxDuration(0)
520
+ .build()
521
+ )
522
+ } catch (e: Exception) {
523
+ Log.e(TAG, "Camera following error: ${e.message}")
524
+ }
525
+ }
526
+
527
+ // ─────────────────────────────────────────────────────────────────────────
528
+ // Cancel navigation — also stops foreground service (fixes notification crash)
456
529
  // ─────────────────────────────────────────────────────────────────────────
457
530
  private fun cancelNavigation() {
458
531
  mapboxNavigation?.setNavigationRoutes(listOf())
459
532
  mapboxNavigation?.stopTripSession()
533
+ firstLocationReceived = false
460
534
  hideUI()
461
535
  onNavigationCancelled(mapOf<String, Any>())
462
536
  }
@@ -472,19 +546,9 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
472
546
  etaBar?.visibility = View.INVISIBLE
473
547
  }
474
548
 
475
- // Issue #43
476
- private fun safeCameraFollowing() {
477
- try {
478
- navigationCamera.requestNavigationCameraToFollowing(
479
- stateTransitionOptions = NavigationCameraTransitionOptions.Builder()
480
- .maxDuration(0).build()
481
- )
482
- } catch (e: Exception) {
483
- Log.e(TAG, "Camera error: ${e.message}")
484
- }
485
- }
486
-
487
- // Issue #31
549
+ // ─────────────────────────────────────────────────────────────────────────
550
+ // Issue #31 — Voice units
551
+ // ─────────────────────────────────────────────────────────────────────────
488
552
  private fun resolveVoiceUnits(): String {
489
553
  return when (voiceUnits?.lowercase()) {
490
554
  "metric" -> "metric"
@@ -496,57 +560,86 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
496
560
  }
497
561
  }
498
562
 
563
+ // ─────────────────────────────────────────────────────────────────────────
499
564
  // Route request
565
+ // ─────────────────────────────────────────────────────────────────────────
500
566
  @SuppressLint("MissingPermission")
501
567
  private fun fetchRoutes() {
502
568
  val nav = mapboxNavigation ?: return
503
569
  if (coordinates.size < 2) return
504
- val points = coordinates.map { Point.fromLngLat(it["longitude"] ?: 0.0, it["latitude"] ?: 0.0) }
570
+
571
+ val points = coordinates.map {
572
+ Point.fromLngLat(it["longitude"] ?: 0.0, it["latitude"] ?: 0.0)
573
+ }
505
574
  val locale = language?.let { Locale.forLanguageTag(it) } ?: Locale.getDefault()
575
+
506
576
  val builder = RouteOptions.builder()
507
577
  .applyDefaultNavigationOptions()
508
578
  .language(locale.toLanguageTag())
509
579
  .voiceUnits(resolveVoiceUnits())
510
580
  .coordinatesList(points)
581
+ // maxspeed annotation required for speed limit display
511
582
  .annotations("maxspeed,congestion,duration")
583
+
512
584
  waypointIndices?.let { builder.waypointIndicesList(it) }
513
- navigationProfile?.let { builder.profile(if (it.startsWith("mapbox/")) it else "mapbox/$it") }
514
- excludeTypes?.takeIf { it.isNotEmpty() }?.let { builder.exclude(it.joinToString(",")) }
585
+ navigationProfile?.let {
586
+ builder.profile(if (it.startsWith("mapbox/")) it else "mapbox/$it")
587
+ }
588
+ excludeTypes?.takeIf { it.isNotEmpty() }?.let {
589
+ builder.exclude(it.joinToString(","))
590
+ }
515
591
  maxHeight?.let { builder.maxHeight(it) }
516
592
  maxWidth?.let { builder.maxWidth(it) }
593
+
517
594
  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 }
595
+ override fun onRoutesReady(
596
+ routes: List<NavigationRoute>,
597
+ @RouterOrigin routerOrigin: String
598
+ ) {
599
+ if (routes.isEmpty()) {
600
+ onRoutesFailed(mapOf("message" to "No routes returned"))
601
+ return
602
+ }
520
603
  nav.setNavigationRoutes(routes)
521
604
  nav.startTripSession()
522
- safeCameraFollowing()
605
+ // Camera will follow automatically via onNewLocationMatcherResult
606
+ // + onRoutesChanged observer
523
607
  }
608
+
524
609
  override fun onFailure(reasons: List<RouterFailure>, routeOptions: RouteOptions) {
525
610
  val msg = reasons.firstOrNull()?.message ?: "Unknown error"
526
611
  Log.e(TAG, "Route failed: $msg")
527
612
  onRoutesFailed(mapOf("message" to msg))
528
613
  }
529
- override fun onCanceled(routeOptions: RouteOptions, @RouterOrigin routerOrigin: String) {
614
+
615
+ override fun onCanceled(
616
+ routeOptions: RouteOptions,
617
+ @RouterOrigin routerOrigin: String
618
+ ) {
530
619
  Log.d(TAG, "Route cancelled")
531
620
  }
532
621
  })
533
622
  }
534
623
 
535
- // Prop setters
536
- fun setCoordinates(coords: List<Map<String, Double>>) { coordinates = coords; if (coords.size >= 2) fetchRoutes() }
537
- fun setWaypointIndices(indices: List<Int>?) { waypointIndices = indices }
538
- fun setLanguage(lang: String?) { language = lang }
539
- fun setVoiceUnits(units: String?) { voiceUnits = units }
540
- fun setNavigationProfile(profile: String?) { navigationProfile = profile }
541
- fun setExcludeTypes(types: List<String>?) { excludeTypes = types }
542
- fun setMapStyle(style: String?) { mapStyle = style }
543
- fun setMute(shouldMute: Boolean) { mute = shouldMute }
544
- fun setMaxHeight(height: Double?) { maxHeight = height }
545
- fun setMaxWidth(width: Double?) { maxWidth = width }
546
- fun setUseMapMatching(use: Boolean) { useMapMatching = use }
547
- fun setCustomRasterTileUrl(url: String?) { customRasterTileUrl = url }
548
- fun setCustomRasterAboveLayerId(layerId: String?) { customRasterAboveLayerId = layerId }
549
-
624
+ // ── Prop setters ──────────────────────────────────────────────────────────
625
+ fun setCoordinates(coords: List<Map<String, Double>>) {
626
+ coordinates = coords
627
+ if (coords.size >= 2) fetchRoutes()
628
+ }
629
+ fun setWaypointIndices(i: List<Int>?) { waypointIndices = i }
630
+ fun setLanguage(l: String?) { language = l }
631
+ fun setVoiceUnits(u: String?) { voiceUnits = u }
632
+ fun setNavigationProfile(p: String?) { navigationProfile = p }
633
+ fun setExcludeTypes(t: List<String>?) { excludeTypes = t }
634
+ fun setMapStyle(s: String?) { mapStyle = s }
635
+ fun setMute(m: Boolean) { mute = m }
636
+ fun setMaxHeight(h: Double?) { maxHeight = h }
637
+ fun setMaxWidth(w: Double?) { maxWidth = w }
638
+ fun setUseMapMatching(u: Boolean) { useMapMatching = u }
639
+ fun setCustomRasterTileUrl(u: String?) { customRasterTileUrl = u }
640
+ fun setCustomRasterAboveLayerId(l: String?) { customRasterAboveLayerId = l }
641
+
642
+ // ── Lifecycle ─────────────────────────────────────────────────────────────
550
643
  override fun onDetachedFromWindow() {
551
644
  super.onDetachedFromWindow()
552
645
  maneuverApi.cancel()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jacques_gordon/expo-mapbox-navigation",
3
- "version": "2.1.1",
3
+ "version": "2.1.2",
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",