@jacques_gordon/expo-mapbox-navigation 2.1.0 → 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
|
|
@@ -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
|
|
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
|
|
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,10 +96,31 @@ 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
|
|
|
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
|
+
|
|
102
124
|
// ── Props ─────────────────────────────────────────────────────────────────
|
|
103
125
|
private var coordinates: List<Map<String, Double>> = emptyList()
|
|
104
126
|
private var waypointIndices: List<Int>? = null
|
|
@@ -121,7 +143,7 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
121
143
|
}
|
|
122
144
|
|
|
123
145
|
// ─────────────────────────────────────────────────────────────────────────
|
|
124
|
-
//
|
|
146
|
+
// Build UI
|
|
125
147
|
// ─────────────────────────────────────────────────────────────────────────
|
|
126
148
|
private fun buildUI() {
|
|
127
149
|
val dp = context.resources.displayMetrics.density
|
|
@@ -129,108 +151,123 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
129
151
|
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
|
130
152
|
}
|
|
131
153
|
|
|
132
|
-
// Full
|
|
154
|
+
// Full screen map
|
|
133
155
|
root.addView(mapView, FrameLayout.LayoutParams(
|
|
134
156
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
135
157
|
FrameLayout.LayoutParams.MATCH_PARENT
|
|
136
158
|
))
|
|
137
159
|
|
|
138
|
-
// ManeuverView — top banner
|
|
139
|
-
//
|
|
160
|
+
// ── ManeuverView — top banner ──────────────────────────────────────────
|
|
161
|
+
// extends ConstraintLayout → must cast to View for addView()
|
|
140
162
|
val mv = MapboxManeuverView(context)
|
|
141
|
-
|
|
163
|
+
root.addView(mv as View, FrameLayout.LayoutParams(
|
|
142
164
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
143
165
|
FrameLayout.LayoutParams.WRAP_CONTENT
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
mv.visibility = View.INVISIBLE // matches official example (INVISIBLE not GONE)
|
|
147
|
-
root.addView(mv as View, mvParams)
|
|
166
|
+
).also { it.gravity = Gravity.TOP })
|
|
167
|
+
mv.visibility = View.INVISIBLE
|
|
148
168
|
maneuverView = mv
|
|
149
169
|
|
|
150
|
-
// SpeedInfoView — bottom-left
|
|
170
|
+
// ── SpeedInfoView — bottom-left ────────────────────────────────────────
|
|
171
|
+
// Shows posted speed limit + current vehicle speed
|
|
151
172
|
val siv = MapboxSpeedInfoView(context)
|
|
152
|
-
|
|
173
|
+
root.addView(siv as View, FrameLayout.LayoutParams(
|
|
153
174
|
FrameLayout.LayoutParams.WRAP_CONTENT,
|
|
154
175
|
FrameLayout.LayoutParams.WRAP_CONTENT
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
|
|
176
|
+
).also {
|
|
177
|
+
it.gravity = Gravity.BOTTOM or Gravity.START
|
|
178
|
+
// Position above the ETA bar (80dp) + margin
|
|
179
|
+
it.setMargins((16 * dp).toInt(), 0, 0, (96 * dp).toInt())
|
|
180
|
+
})
|
|
158
181
|
siv.visibility = View.GONE
|
|
159
|
-
root.addView(siv as View, sivParams)
|
|
160
182
|
speedInfoView = siv
|
|
161
183
|
|
|
162
|
-
//
|
|
163
|
-
val tpv = MapboxTripProgressView(context)
|
|
164
|
-
tripProgressView = tpv
|
|
165
|
-
|
|
166
|
-
// ETA bottom bar
|
|
184
|
+
// ── ETA bottom bar ─────────────────────────────────────────────────────
|
|
167
185
|
val bar = LinearLayout(context).apply {
|
|
168
186
|
orientation = LinearLayout.HORIZONTAL
|
|
169
|
-
setBackgroundColor(Color.
|
|
187
|
+
setBackgroundColor(Color.parseColor("#1E2433")) // dark nav style
|
|
170
188
|
elevation = 8 * dp
|
|
171
189
|
visibility = View.INVISIBLE
|
|
172
190
|
gravity = Gravity.CENTER_VERTICAL
|
|
173
191
|
setPadding(
|
|
174
|
-
(16 * dp).toInt(), (
|
|
175
|
-
(16 * dp).toInt(), (
|
|
192
|
+
(16 * dp).toInt(), (12 * dp).toInt(),
|
|
193
|
+
(16 * dp).toInt(), (12 * dp).toInt()
|
|
176
194
|
)
|
|
177
195
|
}
|
|
178
|
-
|
|
196
|
+
root.addView(bar, FrameLayout.LayoutParams(
|
|
179
197
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
180
|
-
|
|
181
|
-
)
|
|
182
|
-
barParams.gravity = Gravity.BOTTOM
|
|
198
|
+
FrameLayout.LayoutParams.WRAP_CONTENT
|
|
199
|
+
).also { it.gravity = Gravity.BOTTOM })
|
|
183
200
|
|
|
201
|
+
// ETA arrival time
|
|
184
202
|
val etaTime = TextView(context).apply {
|
|
185
|
-
textSize =
|
|
186
|
-
setTextColor(Color.
|
|
203
|
+
textSize = 24f
|
|
204
|
+
setTextColor(Color.WHITE)
|
|
187
205
|
typeface = android.graphics.Typeface.DEFAULT_BOLD
|
|
188
206
|
}
|
|
189
|
-
bar.addView(etaTime, LinearLayout.LayoutParams(
|
|
207
|
+
bar.addView(etaTime, LinearLayout.LayoutParams(
|
|
208
|
+
0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f
|
|
209
|
+
))
|
|
190
210
|
tvEtaTime = etaTime
|
|
191
211
|
|
|
212
|
+
// Duration + distance (center)
|
|
192
213
|
val center = LinearLayout(context).apply {
|
|
193
214
|
orientation = LinearLayout.VERTICAL
|
|
194
215
|
gravity = Gravity.CENTER
|
|
195
216
|
}
|
|
196
|
-
val dur = TextView(context).apply {
|
|
197
|
-
|
|
217
|
+
val dur = TextView(context).apply {
|
|
218
|
+
textSize = 18f
|
|
219
|
+
setTextColor(Color.WHITE)
|
|
220
|
+
typeface = android.graphics.Typeface.DEFAULT_BOLD
|
|
221
|
+
gravity = Gravity.CENTER
|
|
222
|
+
}
|
|
223
|
+
val dist = TextView(context).apply {
|
|
224
|
+
textSize = 13f
|
|
225
|
+
setTextColor(Color.parseColor("#AAAAAA"))
|
|
226
|
+
gravity = Gravity.CENTER
|
|
227
|
+
}
|
|
198
228
|
center.addView(dur)
|
|
199
229
|
center.addView(dist)
|
|
200
|
-
bar.addView(center, LinearLayout.LayoutParams(
|
|
230
|
+
bar.addView(center, LinearLayout.LayoutParams(
|
|
231
|
+
0, LinearLayout.LayoutParams.WRAP_CONTENT, 2f
|
|
232
|
+
))
|
|
201
233
|
tvDuration = dur
|
|
202
234
|
tvDistance = dist
|
|
203
235
|
|
|
236
|
+
// Cancel button
|
|
204
237
|
val cancelBtn = TextView(context).apply {
|
|
205
238
|
text = "✕"
|
|
206
239
|
textSize = 22f
|
|
207
|
-
setTextColor(Color.
|
|
240
|
+
setTextColor(Color.parseColor("#AAAAAA"))
|
|
208
241
|
gravity = Gravity.END or Gravity.CENTER_VERTICAL
|
|
209
242
|
setOnClickListener { cancelNavigation() }
|
|
210
243
|
}
|
|
211
|
-
bar.addView(cancelBtn, LinearLayout.LayoutParams(
|
|
244
|
+
bar.addView(cancelBtn, LinearLayout.LayoutParams(
|
|
245
|
+
0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f
|
|
246
|
+
))
|
|
212
247
|
etaBar = bar
|
|
213
|
-
|
|
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
|
|
214
255
|
|
|
215
256
|
addView(root)
|
|
216
257
|
}
|
|
217
258
|
|
|
218
259
|
// ─────────────────────────────────────────────────────────────────────────
|
|
219
|
-
//
|
|
260
|
+
// Init APIs
|
|
220
261
|
// ─────────────────────────────────────────────────────────────────────────
|
|
221
262
|
private fun initAPIs() {
|
|
222
263
|
val distanceFormatterOptions = DistanceFormatterOptions.Builder(context).build()
|
|
223
264
|
|
|
224
|
-
|
|
225
|
-
maneuverApi = MapboxManeuverApi(
|
|
226
|
-
MapboxDistanceFormatter(distanceFormatterOptions)
|
|
227
|
-
)
|
|
265
|
+
maneuverApi = MapboxManeuverApi(MapboxDistanceFormatter(distanceFormatterOptions))
|
|
228
266
|
|
|
229
267
|
tripProgressApi = MapboxTripProgressApi(
|
|
230
268
|
TripProgressUpdateFormatter.Builder(context)
|
|
231
269
|
.distanceRemainingFormatter(DistanceRemainingFormatter(distanceFormatterOptions))
|
|
232
270
|
.timeRemainingFormatter(TimeRemainingFormatter(context))
|
|
233
|
-
.percentRouteTraveledFormatter(PercentDistanceTraveledFormatter())
|
|
234
271
|
.estimatedTimeToArrivalFormatter(
|
|
235
272
|
EstimatedTimeToArrivalFormatter(context, TimeFormat.NONE_SPECIFIED)
|
|
236
273
|
)
|
|
@@ -239,32 +276,35 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
239
276
|
|
|
240
277
|
speedInfoApi = MapboxSpeedInfoApi()
|
|
241
278
|
|
|
242
|
-
val routeLineViewOptions = MapboxRouteLineViewOptions.Builder(context)
|
|
243
|
-
.routeLineBelowLayerId("road-label-navigation")
|
|
244
|
-
.build()
|
|
245
279
|
routeLineApi = MapboxRouteLineApi(MapboxRouteLineApiOptions.Builder().build())
|
|
246
|
-
routeLineView = MapboxRouteLineView(
|
|
280
|
+
routeLineView = MapboxRouteLineView(
|
|
281
|
+
MapboxRouteLineViewOptions.Builder(context)
|
|
282
|
+
.routeLineBelowLayerId("road-label-navigation")
|
|
283
|
+
.build()
|
|
284
|
+
)
|
|
247
285
|
|
|
248
|
-
|
|
249
|
-
routeArrowView = MapboxRouteArrowView(routeArrowOptions)
|
|
286
|
+
routeArrowView = MapboxRouteArrowView(RouteArrowOptions.Builder(context).build())
|
|
250
287
|
}
|
|
251
288
|
|
|
252
289
|
// ─────────────────────────────────────────────────────────────────────────
|
|
253
|
-
//
|
|
290
|
+
// Setup Navigation
|
|
254
291
|
// ─────────────────────────────────────────────────────────────────────────
|
|
255
292
|
private fun setupNavigation() {
|
|
256
293
|
mapboxNavigation = MapboxNavigationProvider.create(
|
|
257
294
|
NavigationOptions.Builder(context).build()
|
|
258
295
|
)
|
|
259
296
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
// Route line layers must be initialized first
|
|
264
|
-
routeLineView.initializeLayers(mapStyle)
|
|
297
|
+
mapView.mapboxMap.loadStyle(mapStyle ?: getAutoStyle()) { style ->
|
|
298
|
+
routeLineView.initializeLayers(style)
|
|
265
299
|
|
|
266
|
-
// Camera
|
|
300
|
+
// Camera viewport data source
|
|
267
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
|
+
|
|
268
308
|
navigationCamera = NavigationCamera(
|
|
269
309
|
mapView.mapboxMap,
|
|
270
310
|
mapView.camera,
|
|
@@ -282,7 +322,7 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
282
322
|
}
|
|
283
323
|
|
|
284
324
|
// ─────────────────────────────────────────────────────────────────────────
|
|
285
|
-
//
|
|
325
|
+
// Observers
|
|
286
326
|
// ─────────────────────────────────────────────────────────────────────────
|
|
287
327
|
private fun registerObservers() {
|
|
288
328
|
val nav = mapboxNavigation ?: return
|
|
@@ -291,19 +331,18 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
291
331
|
nav.registerRoutesObserver(object : RoutesObserver {
|
|
292
332
|
override fun onRoutesChanged(result: RoutesUpdatedResult) {
|
|
293
333
|
if (result.navigationRoutes.isNotEmpty()) {
|
|
294
|
-
// Draw route line
|
|
295
334
|
routeLineApi.setNavigationRoutes(result.navigationRoutes) { value ->
|
|
296
335
|
mapView.mapboxMap.style?.apply {
|
|
297
336
|
routeLineView.renderRouteDrawData(this, value)
|
|
298
337
|
}
|
|
299
338
|
}
|
|
300
|
-
// Update camera viewport
|
|
301
339
|
viewportDataSource.onRouteChanged(result.navigationRoutes.first())
|
|
302
340
|
viewportDataSource.evaluate()
|
|
303
341
|
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
|
|
342
|
+
// FIX: transition to following mode when route is set
|
|
343
|
+
// This triggers the camera to zoom/pitch/center correctly
|
|
344
|
+
safeCameraFollowing()
|
|
345
|
+
showUI()
|
|
307
346
|
|
|
308
347
|
onRoutesReady(mapOf(
|
|
309
348
|
"routeCount" to result.navigationRoutes.size,
|
|
@@ -311,9 +350,7 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
311
350
|
"durationSeconds" to (result.navigationRoutes.first().directionsRoute.duration() ?: 0.0)
|
|
312
351
|
))
|
|
313
352
|
} else {
|
|
314
|
-
|
|
315
|
-
val style = mapView.mapboxMap.style
|
|
316
|
-
if (style != null) {
|
|
353
|
+
mapView.mapboxMap.style?.let { style ->
|
|
317
354
|
routeLineApi.clearRouteLine { value ->
|
|
318
355
|
routeLineView.renderClearRouteLineValue(style, value)
|
|
319
356
|
}
|
|
@@ -327,38 +364,40 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
327
364
|
})
|
|
328
365
|
|
|
329
366
|
// ── Route progress observer ───────────────────────────────────────────
|
|
330
|
-
// EXACT same pattern as TurnByTurnExperienceActivity.routeProgressObserver
|
|
331
367
|
nav.registerRouteProgressObserver(RouteProgressObserver { routeProgress ->
|
|
332
|
-
// Update camera
|
|
368
|
+
// Update viewport with progress (camera follows route progress)
|
|
333
369
|
viewportDataSource.onRouteProgressChanged(routeProgress)
|
|
334
370
|
viewportDataSource.evaluate()
|
|
335
371
|
|
|
336
372
|
// Draw maneuver arrow
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
routeArrowView.renderManeuverUpdate(style, maneuverArrowResult)
|
|
373
|
+
mapView.mapboxMap.style?.let { style ->
|
|
374
|
+
val arrowResult = routeArrowApi.addUpcomingManeuverArrow(routeProgress)
|
|
375
|
+
routeArrowView.renderManeuverUpdate(style, arrowResult)
|
|
341
376
|
}
|
|
342
377
|
|
|
343
|
-
// Maneuver banner
|
|
378
|
+
// Maneuver banner
|
|
344
379
|
val maneuvers = maneuverApi.getManeuvers(routeProgress)
|
|
345
380
|
maneuvers.fold(
|
|
346
|
-
{ error ->
|
|
347
|
-
|
|
381
|
+
{ error ->
|
|
382
|
+
Log.w(TAG, "Maneuver error: ${error.errorMessage}")
|
|
383
|
+
Unit
|
|
384
|
+
},
|
|
385
|
+
{ _ ->
|
|
348
386
|
maneuverView?.visibility = View.VISIBLE
|
|
349
387
|
maneuverView?.renderManeuvers(maneuvers)
|
|
388
|
+
Unit
|
|
350
389
|
}
|
|
351
390
|
)
|
|
352
391
|
|
|
353
|
-
// Trip progress
|
|
354
|
-
// getTripProgress returns TripProgressUpdateValue directly (not nullable)
|
|
392
|
+
// Trip progress
|
|
355
393
|
val tripProgress = tripProgressApi.getTripProgress(routeProgress)
|
|
356
394
|
tripProgressView?.render(tripProgress)
|
|
395
|
+
updateEtaBar(
|
|
396
|
+
tripProgress.estimatedTimeToArrival,
|
|
397
|
+
tripProgress.totalTimeRemaining,
|
|
398
|
+
tripProgress.distanceRemaining
|
|
399
|
+
)
|
|
357
400
|
|
|
358
|
-
// Update our custom ETA bar from tripProgress fields
|
|
359
|
-
updateEtaBar(tripProgress)
|
|
360
|
-
|
|
361
|
-
// Emit event
|
|
362
401
|
onRouteProgressChanged(mapOf(
|
|
363
402
|
"distanceRemaining" to routeProgress.distanceRemaining,
|
|
364
403
|
"durationRemaining" to routeProgress.durationRemaining,
|
|
@@ -371,29 +410,33 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
371
410
|
})
|
|
372
411
|
|
|
373
412
|
// ── Location observer ─────────────────────────────────────────────────
|
|
374
|
-
// EXACT same as official example
|
|
375
413
|
nav.registerLocationObserver(object : LocationObserver {
|
|
376
414
|
override fun onNewRawLocation(rawLocation: Location) {}
|
|
377
415
|
|
|
378
|
-
override fun onNewLocationMatcherResult(
|
|
379
|
-
val
|
|
416
|
+
override fun onNewLocationMatcherResult(result: LocationMatcherResult) {
|
|
417
|
+
val loc = result.enhancedLocation
|
|
380
418
|
|
|
381
|
-
// Update location puck
|
|
419
|
+
// Update location puck position
|
|
382
420
|
navigationLocationProvider.changePosition(
|
|
383
|
-
location =
|
|
384
|
-
keyPoints =
|
|
421
|
+
location = loc,
|
|
422
|
+
keyPoints = result.keyPoints,
|
|
385
423
|
)
|
|
386
424
|
|
|
387
|
-
// Update
|
|
388
|
-
|
|
425
|
+
// FIX: Update viewport data source with location AND bearing
|
|
426
|
+
// This is what drives the camera to follow the vehicle's direction
|
|
427
|
+
viewportDataSource.onLocationChanged(loc)
|
|
389
428
|
viewportDataSource.evaluate()
|
|
390
429
|
|
|
391
|
-
//
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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 ────────────────────────────────────────
|
|
438
|
+
val fmtOptions = DistanceFormatterOptions.Builder(context).build()
|
|
439
|
+
val speedInfo = speedInfoApi.updatePostedAndCurrentSpeed(result, fmtOptions)
|
|
397
440
|
if (speedInfo != null) {
|
|
398
441
|
speedInfoView?.visibility = View.VISIBLE
|
|
399
442
|
speedInfoView?.render(speedInfo)
|
|
@@ -401,45 +444,46 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
401
444
|
speedInfoView?.visibility = View.GONE
|
|
402
445
|
}
|
|
403
446
|
|
|
404
|
-
// Auto day/night switch
|
|
405
447
|
checkAndSwitchDayNight()
|
|
406
448
|
}
|
|
407
449
|
})
|
|
408
450
|
}
|
|
409
451
|
|
|
410
452
|
// ─────────────────────────────────────────────────────────────────────────
|
|
411
|
-
//
|
|
412
|
-
//
|
|
453
|
+
// ETA bar update
|
|
454
|
+
// Fields confirmed from TripProgressUpdateValue docs:
|
|
455
|
+
// estimatedTimeToArrival: Long — Unix timestamp ms
|
|
456
|
+
// totalTimeRemaining: Double — seconds
|
|
457
|
+
// distanceRemaining: Double — metres
|
|
413
458
|
// ─────────────────────────────────────────────────────────────────────────
|
|
414
|
-
private fun updateEtaBar(
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
val
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
459
|
+
private fun updateEtaBar(
|
|
460
|
+
estimatedTimeToArrivalMs: Long,
|
|
461
|
+
totalTimeRemainingSec: Double,
|
|
462
|
+
distanceRemainingMetres: Double
|
|
463
|
+
) {
|
|
464
|
+
val arrivalCal = Calendar.getInstance().apply {
|
|
465
|
+
timeInMillis = estimatedTimeToArrivalMs
|
|
466
|
+
}
|
|
467
|
+
tvEtaTime?.text = String.format(
|
|
468
|
+
"%02d:%02d",
|
|
469
|
+
arrivalCal.get(Calendar.HOUR_OF_DAY),
|
|
470
|
+
arrivalCal.get(Calendar.MINUTE)
|
|
424
471
|
)
|
|
425
472
|
|
|
426
|
-
|
|
427
|
-
val totalMin = (tripProgress.totalTimeRemaining / 60).toInt()
|
|
473
|
+
val totalMin = (totalTimeRemainingSec / 60).toInt()
|
|
428
474
|
tvDuration?.text = if (totalMin >= 60)
|
|
429
475
|
"${totalMin / 60}h ${totalMin % 60}min"
|
|
430
476
|
else
|
|
431
477
|
"${totalMin} min"
|
|
432
478
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
tvDistance?.text = if (distM >= 1000)
|
|
436
|
-
String.format("%.1f km", distM / 1000.0)
|
|
479
|
+
tvDistance?.text = if (distanceRemainingMetres >= 1000)
|
|
480
|
+
String.format("%.1f km", distanceRemainingMetres / 1000.0)
|
|
437
481
|
else
|
|
438
|
-
"${
|
|
482
|
+
"${distanceRemainingMetres.toInt()} m"
|
|
439
483
|
}
|
|
440
484
|
|
|
441
485
|
// ─────────────────────────────────────────────────────────────────────────
|
|
442
|
-
//
|
|
486
|
+
// Day / Night auto switch
|
|
443
487
|
// ─────────────────────────────────────────────────────────────────────────
|
|
444
488
|
private fun getAutoStyle(): String {
|
|
445
489
|
val hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY)
|
|
@@ -452,10 +496,8 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
452
496
|
val shouldBeNight = hour !in 6..20
|
|
453
497
|
if (shouldBeNight == isNightMode) return
|
|
454
498
|
isNightMode = shouldBeNight
|
|
455
|
-
|
|
456
499
|
val newStyle = if (shouldBeNight) NavigationStyles.NAVIGATION_NIGHT_STYLE
|
|
457
500
|
else NavigationStyles.NAVIGATION_DAY_STYLE
|
|
458
|
-
|
|
459
501
|
mapView.mapboxMap.loadStyle(newStyle) { style ->
|
|
460
502
|
routeLineView.initializeLayers(style)
|
|
461
503
|
mapboxNavigation?.getNavigationRoutes()?.takeIf { it.isNotEmpty() }?.let { routes ->
|
|
@@ -464,26 +506,40 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
464
506
|
}
|
|
465
507
|
}
|
|
466
508
|
}
|
|
509
|
+
}
|
|
467
510
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
511
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
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
|
+
}
|
|
475
525
|
}
|
|
476
526
|
|
|
477
527
|
// ─────────────────────────────────────────────────────────────────────────
|
|
478
|
-
//
|
|
528
|
+
// Cancel navigation — also stops foreground service (fixes notification crash)
|
|
479
529
|
// ─────────────────────────────────────────────────────────────────────────
|
|
480
530
|
private fun cancelNavigation() {
|
|
481
531
|
mapboxNavigation?.setNavigationRoutes(listOf())
|
|
482
532
|
mapboxNavigation?.stopTripSession()
|
|
533
|
+
firstLocationReceived = false
|
|
483
534
|
hideUI()
|
|
484
535
|
onNavigationCancelled(mapOf<String, Any>())
|
|
485
536
|
}
|
|
486
537
|
|
|
538
|
+
private fun showUI() {
|
|
539
|
+
maneuverView?.visibility = View.VISIBLE
|
|
540
|
+
etaBar?.visibility = View.VISIBLE
|
|
541
|
+
}
|
|
542
|
+
|
|
487
543
|
private fun hideUI() {
|
|
488
544
|
maneuverView?.visibility = View.INVISIBLE
|
|
489
545
|
speedInfoView?.visibility = View.GONE
|
|
@@ -491,22 +547,7 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
491
547
|
}
|
|
492
548
|
|
|
493
549
|
// ─────────────────────────────────────────────────────────────────────────
|
|
494
|
-
//
|
|
495
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
496
|
-
private fun safeCameraFollowing() {
|
|
497
|
-
try {
|
|
498
|
-
navigationCamera.requestNavigationCameraToFollowing(
|
|
499
|
-
stateTransitionOptions = NavigationCameraTransitionOptions.Builder()
|
|
500
|
-
.maxDuration(0)
|
|
501
|
-
.build()
|
|
502
|
-
)
|
|
503
|
-
} catch (e: Exception) {
|
|
504
|
-
Log.e(TAG, "Camera transition error: ${e.message}")
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
509
|
-
// 9. Issue #31 — Voice units
|
|
550
|
+
// Issue #31 — Voice units
|
|
510
551
|
// ─────────────────────────────────────────────────────────────────────────
|
|
511
552
|
private fun resolveVoiceUnits(): String {
|
|
512
553
|
return when (voiceUnits?.lowercase()) {
|
|
@@ -520,15 +561,15 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
520
561
|
}
|
|
521
562
|
|
|
522
563
|
// ─────────────────────────────────────────────────────────────────────────
|
|
523
|
-
//
|
|
564
|
+
// Route request
|
|
524
565
|
// ─────────────────────────────────────────────────────────────────────────
|
|
525
566
|
@SuppressLint("MissingPermission")
|
|
526
567
|
private fun fetchRoutes() {
|
|
527
568
|
val nav = mapboxNavigation ?: return
|
|
528
569
|
if (coordinates.size < 2) return
|
|
529
570
|
|
|
530
|
-
val points = coordinates.map {
|
|
531
|
-
Point.fromLngLat(
|
|
571
|
+
val points = coordinates.map {
|
|
572
|
+
Point.fromLngLat(it["longitude"] ?: 0.0, it["latitude"] ?: 0.0)
|
|
532
573
|
}
|
|
533
574
|
val locale = language?.let { Locale.forLanguageTag(it) } ?: Locale.getDefault()
|
|
534
575
|
|
|
@@ -537,46 +578,47 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
537
578
|
.language(locale.toLanguageTag())
|
|
538
579
|
.voiceUnits(resolveVoiceUnits())
|
|
539
580
|
.coordinatesList(points)
|
|
581
|
+
// maxspeed annotation required for speed limit display
|
|
540
582
|
.annotations("maxspeed,congestion,duration")
|
|
541
583
|
|
|
542
584
|
waypointIndices?.let { builder.waypointIndicesList(it) }
|
|
543
585
|
navigationProfile?.let {
|
|
544
586
|
builder.profile(if (it.startsWith("mapbox/")) it else "mapbox/$it")
|
|
545
587
|
}
|
|
546
|
-
excludeTypes?.takeIf { it.isNotEmpty() }?.let {
|
|
588
|
+
excludeTypes?.takeIf { it.isNotEmpty() }?.let {
|
|
589
|
+
builder.exclude(it.joinToString(","))
|
|
590
|
+
}
|
|
547
591
|
maxHeight?.let { builder.maxHeight(it) }
|
|
548
592
|
maxWidth?.let { builder.maxWidth(it) }
|
|
549
593
|
|
|
550
|
-
nav.requestRoutes(
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
onRoutesFailed(mapOf("message" to "No routes returned"))
|
|
559
|
-
return
|
|
560
|
-
}
|
|
561
|
-
nav.setNavigationRoutes(routes)
|
|
562
|
-
nav.startTripSession()
|
|
563
|
-
safeCameraFollowing()
|
|
594
|
+
nav.requestRoutes(builder.build(), object : NavigationRouterCallback {
|
|
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
|
|
564
602
|
}
|
|
603
|
+
nav.setNavigationRoutes(routes)
|
|
604
|
+
nav.startTripSession()
|
|
605
|
+
// Camera will follow automatically via onNewLocationMatcherResult
|
|
606
|
+
// + onRoutesChanged observer
|
|
607
|
+
}
|
|
565
608
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
609
|
+
override fun onFailure(reasons: List<RouterFailure>, routeOptions: RouteOptions) {
|
|
610
|
+
val msg = reasons.firstOrNull()?.message ?: "Unknown error"
|
|
611
|
+
Log.e(TAG, "Route failed: $msg")
|
|
612
|
+
onRoutesFailed(mapOf("message" to msg))
|
|
613
|
+
}
|
|
571
614
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
}
|
|
615
|
+
override fun onCanceled(
|
|
616
|
+
routeOptions: RouteOptions,
|
|
617
|
+
@RouterOrigin routerOrigin: String
|
|
618
|
+
) {
|
|
619
|
+
Log.d(TAG, "Route cancelled")
|
|
578
620
|
}
|
|
579
|
-
)
|
|
621
|
+
})
|
|
580
622
|
}
|
|
581
623
|
|
|
582
624
|
// ── Prop setters ──────────────────────────────────────────────────────────
|
|
@@ -584,18 +626,18 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
584
626
|
coordinates = coords
|
|
585
627
|
if (coords.size >= 2) fetchRoutes()
|
|
586
628
|
}
|
|
587
|
-
fun setWaypointIndices(
|
|
588
|
-
fun setLanguage(
|
|
589
|
-
fun setVoiceUnits(
|
|
590
|
-
fun setNavigationProfile(
|
|
591
|
-
fun setExcludeTypes(
|
|
592
|
-
fun setMapStyle(
|
|
593
|
-
fun setMute(
|
|
594
|
-
fun setMaxHeight(
|
|
595
|
-
fun setMaxWidth(
|
|
596
|
-
fun setUseMapMatching(
|
|
597
|
-
fun setCustomRasterTileUrl(
|
|
598
|
-
fun setCustomRasterAboveLayerId(
|
|
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 }
|
|
599
641
|
|
|
600
642
|
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
|
601
643
|
override fun onDetachedFromWindow() {
|
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",
|