@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
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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.
|
|
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(), (
|
|
170
|
-
(16 * 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,
|
|
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 =
|
|
179
|
-
setTextColor(Color.
|
|
203
|
+
textSize = 24f
|
|
204
|
+
setTextColor(Color.WHITE)
|
|
180
205
|
typeface = android.graphics.Typeface.DEFAULT_BOLD
|
|
181
206
|
}
|
|
182
|
-
bar.addView(etaTime, LinearLayout.LayoutParams(
|
|
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 =
|
|
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
|
|
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(
|
|
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 = "✕"
|
|
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(
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
//
|
|
453
|
+
// ETA bar update
|
|
454
|
+
// Fields confirmed from TripProgressUpdateValue docs:
|
|
455
|
+
// estimatedTimeToArrival: Long — Unix 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
|
-
//
|
|
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
|
-
//
|
|
476
|
-
|
|
477
|
-
|
|
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
|
-
|
|
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 {
|
|
514
|
-
|
|
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(
|
|
519
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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>>) {
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
fun
|
|
541
|
-
fun
|
|
542
|
-
fun
|
|
543
|
-
fun
|
|
544
|
-
fun
|
|
545
|
-
fun
|
|
546
|
-
fun
|
|
547
|
-
fun
|
|
548
|
-
fun
|
|
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.
|
|
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",
|