@jacques_gordon/expo-mapbox-navigation 2.1.1 → 2.1.3
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,19 +2,27 @@ 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
|
|
7
|
+
import android.graphics.drawable.GradientDrawable
|
|
6
8
|
import android.util.Log
|
|
7
9
|
import android.view.Gravity
|
|
8
10
|
import android.view.View
|
|
9
11
|
import android.widget.FrameLayout
|
|
12
|
+
import android.widget.ImageView
|
|
10
13
|
import android.widget.LinearLayout
|
|
11
14
|
import android.widget.TextView
|
|
12
15
|
import com.mapbox.api.directions.v5.models.RouteOptions
|
|
13
16
|
import com.mapbox.common.location.Location
|
|
14
17
|
import com.mapbox.geojson.Point
|
|
18
|
+
import com.mapbox.maps.EdgeInsets
|
|
19
|
+
import com.mapbox.maps.ImageHolder
|
|
15
20
|
import com.mapbox.maps.MapView
|
|
21
|
+
import com.mapbox.maps.plugin.LocationPuck2D
|
|
16
22
|
import com.mapbox.maps.plugin.animation.camera
|
|
17
23
|
import com.mapbox.maps.plugin.locationcomponent.location
|
|
24
|
+
import com.mapbox.maps.plugin.locationcomponent.OnIndicatorBearingChangedListener
|
|
25
|
+
import com.mapbox.maps.plugin.locationcomponent.OnIndicatorPositionChangedListener
|
|
18
26
|
import com.mapbox.navigation.base.TimeFormat
|
|
19
27
|
import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions
|
|
20
28
|
import com.mapbox.navigation.base.formatter.DistanceFormatterOptions
|
|
@@ -35,7 +43,6 @@ import com.mapbox.navigation.tripdata.maneuver.api.MapboxManeuverApi
|
|
|
35
43
|
import com.mapbox.navigation.tripdata.progress.api.MapboxTripProgressApi
|
|
36
44
|
import com.mapbox.navigation.tripdata.progress.model.DistanceRemainingFormatter
|
|
37
45
|
import com.mapbox.navigation.tripdata.progress.model.EstimatedTimeToArrivalFormatter
|
|
38
|
-
import com.mapbox.navigation.tripdata.progress.model.PercentDistanceTraveledFormatter
|
|
39
46
|
import com.mapbox.navigation.tripdata.progress.model.TimeRemainingFormatter
|
|
40
47
|
import com.mapbox.navigation.tripdata.progress.model.TripProgressUpdateFormatter
|
|
41
48
|
import com.mapbox.navigation.tripdata.speedlimit.api.MapboxSpeedInfoApi
|
|
@@ -58,7 +65,6 @@ import expo.modules.kotlin.AppContext
|
|
|
58
65
|
import expo.modules.kotlin.viewevent.EventDispatcher
|
|
59
66
|
import expo.modules.kotlin.views.ExpoView
|
|
60
67
|
import java.util.Calendar
|
|
61
|
-
import java.util.Date
|
|
62
68
|
import java.util.Locale
|
|
63
69
|
|
|
64
70
|
private const val TAG = "ExpoMapboxNavigation"
|
|
@@ -84,6 +90,12 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
84
90
|
private var tvDistance: TextView? = null
|
|
85
91
|
private var etaBar: LinearLayout? = null
|
|
86
92
|
|
|
93
|
+
// ── Side action buttons (mute, overview, recenter) ────────────────────────
|
|
94
|
+
private var btnMute: TextView? = null
|
|
95
|
+
private var btnOverview: TextView? = null
|
|
96
|
+
private var btnRecenter: TextView? = null
|
|
97
|
+
private var sideButtons: LinearLayout? = null
|
|
98
|
+
|
|
87
99
|
// ── Navigation APIs ───────────────────────────────────────────────────────
|
|
88
100
|
private lateinit var navigationCamera: NavigationCamera
|
|
89
101
|
private lateinit var viewportDataSource: MapboxNavigationViewportDataSource
|
|
@@ -96,7 +108,36 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
96
108
|
private lateinit var routeArrowView: MapboxRouteArrowView
|
|
97
109
|
private val navigationLocationProvider = NavigationLocationProvider()
|
|
98
110
|
private var mapboxNavigation: MapboxNavigation? = null
|
|
111
|
+
|
|
112
|
+
// ── State ─────────────────────────────────────────────────────────────────
|
|
99
113
|
private var isNightMode = false
|
|
114
|
+
private var isMuted = false
|
|
115
|
+
private var isOverviewMode = false
|
|
116
|
+
private var firstLocationReceived = false
|
|
117
|
+
|
|
118
|
+
// ── Pixel density ─────────────────────────────────────────────────────────
|
|
119
|
+
private val dp = context.resources.displayMetrics.density
|
|
120
|
+
|
|
121
|
+
// ── Viewport padding (Waze-style: vehicle at ~30% from bottom) ────────────
|
|
122
|
+
// top=180dp → room for maneuver banner
|
|
123
|
+
// bottom=300dp → pushes vehicle UP toward 30% from bottom (Waze style)
|
|
124
|
+
// Without large bottom padding, vehicle sits at screen center
|
|
125
|
+
private val followingPadding by lazy {
|
|
126
|
+
EdgeInsets(
|
|
127
|
+
180.0 * dp, // top (maneuver banner height)
|
|
128
|
+
40.0 * dp, // left
|
|
129
|
+
300.0 * dp, // bottom — large value pushes vehicle up like Waze
|
|
130
|
+
40.0 * dp // right
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
private val overviewPadding by lazy {
|
|
134
|
+
EdgeInsets(
|
|
135
|
+
120.0 * dp,
|
|
136
|
+
40.0 * dp,
|
|
137
|
+
120.0 * dp,
|
|
138
|
+
40.0 * dp
|
|
139
|
+
)
|
|
140
|
+
}
|
|
100
141
|
|
|
101
142
|
// ── Props ─────────────────────────────────────────────────────────────────
|
|
102
143
|
private var coordinates: List<Map<String, Double>> = emptyList()
|
|
@@ -120,20 +161,20 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
120
161
|
}
|
|
121
162
|
|
|
122
163
|
// ─────────────────────────────────────────────────────────────────────────
|
|
123
|
-
// Build UI
|
|
164
|
+
// Build Waze-style UI
|
|
124
165
|
// ─────────────────────────────────────────────────────────────────────────
|
|
125
166
|
private fun buildUI() {
|
|
126
|
-
val dp = context.resources.displayMetrics.density
|
|
127
167
|
val root = FrameLayout(context).apply {
|
|
128
168
|
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
|
129
169
|
}
|
|
130
170
|
|
|
171
|
+
// Full-screen map
|
|
131
172
|
root.addView(mapView, FrameLayout.LayoutParams(
|
|
132
173
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
133
174
|
FrameLayout.LayoutParams.MATCH_PARENT
|
|
134
175
|
))
|
|
135
176
|
|
|
136
|
-
// ManeuverView — top
|
|
177
|
+
// ── ManeuverView — top banner ──────────────────────────────────────────
|
|
137
178
|
val mv = MapboxManeuverView(context)
|
|
138
179
|
root.addView(mv as View, FrameLayout.LayoutParams(
|
|
139
180
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
@@ -142,55 +183,97 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
142
183
|
mv.visibility = View.INVISIBLE
|
|
143
184
|
maneuverView = mv
|
|
144
185
|
|
|
145
|
-
// SpeedInfoView — bottom-left
|
|
186
|
+
// ── SpeedInfoView — bottom-left, above ETA bar ────────────────────────
|
|
146
187
|
val siv = MapboxSpeedInfoView(context)
|
|
147
188
|
root.addView(siv as View, FrameLayout.LayoutParams(
|
|
148
|
-
|
|
149
|
-
|
|
189
|
+
(72 * dp).toInt(),
|
|
190
|
+
(72 * dp).toInt()
|
|
150
191
|
).also {
|
|
151
192
|
it.gravity = Gravity.BOTTOM or Gravity.START
|
|
152
|
-
it.setMargins((
|
|
193
|
+
it.setMargins((12 * dp).toInt(), 0, 0, (88 * dp).toInt())
|
|
153
194
|
})
|
|
154
195
|
siv.visibility = View.GONE
|
|
155
196
|
speedInfoView = siv
|
|
156
197
|
|
|
157
|
-
//
|
|
158
|
-
val
|
|
159
|
-
|
|
198
|
+
// ── Side action buttons (right side, Waze-style) ──────────────────────
|
|
199
|
+
val sideCol = LinearLayout(context).apply {
|
|
200
|
+
orientation = LinearLayout.VERTICAL
|
|
201
|
+
gravity = Gravity.CENTER_HORIZONTAL
|
|
202
|
+
visibility = View.INVISIBLE
|
|
203
|
+
}
|
|
204
|
+
root.addView(sideCol, FrameLayout.LayoutParams(
|
|
205
|
+
FrameLayout.LayoutParams.WRAP_CONTENT,
|
|
206
|
+
FrameLayout.LayoutParams.WRAP_CONTENT
|
|
207
|
+
).also {
|
|
208
|
+
it.gravity = Gravity.END or Gravity.BOTTOM
|
|
209
|
+
it.setMargins(0, 0, (12 * dp).toInt(), (96 * dp).toInt())
|
|
210
|
+
})
|
|
211
|
+
sideButtons = sideCol
|
|
212
|
+
|
|
213
|
+
// Mute button
|
|
214
|
+
val muteBtn = makeCircleButton("🔊")
|
|
215
|
+
muteBtn.setOnClickListener { toggleMute() }
|
|
216
|
+
sideCol.addView(muteBtn, circleButtonParams())
|
|
217
|
+
btnMute = muteBtn
|
|
160
218
|
|
|
161
|
-
|
|
219
|
+
sideCol.addView(View(context), LinearLayout.LayoutParams(1, (10 * dp).toInt()))
|
|
220
|
+
|
|
221
|
+
// Overview button
|
|
222
|
+
val overviewBtn = makeCircleButton("⊕")
|
|
223
|
+
overviewBtn.setOnClickListener { toggleOverview() }
|
|
224
|
+
sideCol.addView(overviewBtn, circleButtonParams())
|
|
225
|
+
btnOverview = overviewBtn
|
|
226
|
+
|
|
227
|
+
sideCol.addView(View(context), LinearLayout.LayoutParams(1, (10 * dp).toInt()))
|
|
228
|
+
|
|
229
|
+
// Recenter button (shown only in overview mode)
|
|
230
|
+
val recenterBtn = makeCircleButton("◎")
|
|
231
|
+
recenterBtn.setOnClickListener { recenterCamera() }
|
|
232
|
+
recenterBtn.visibility = View.GONE
|
|
233
|
+
sideCol.addView(recenterBtn, circleButtonParams())
|
|
234
|
+
btnRecenter = recenterBtn
|
|
235
|
+
|
|
236
|
+
// ── ETA bottom bar ─────────────────────────────────────────────────────
|
|
162
237
|
val bar = LinearLayout(context).apply {
|
|
163
238
|
orientation = LinearLayout.HORIZONTAL
|
|
164
|
-
setBackgroundColor(Color.
|
|
239
|
+
setBackgroundColor(Color.parseColor("#1E2433"))
|
|
165
240
|
elevation = 8 * dp
|
|
166
241
|
visibility = View.INVISIBLE
|
|
167
242
|
gravity = Gravity.CENTER_VERTICAL
|
|
168
243
|
setPadding(
|
|
169
|
-
(16 * dp).toInt(), (
|
|
170
|
-
(16 * dp).toInt(), (
|
|
244
|
+
(16 * dp).toInt(), (12 * dp).toInt(),
|
|
245
|
+
(16 * dp).toInt(), (12 * dp).toInt()
|
|
171
246
|
)
|
|
172
247
|
}
|
|
173
248
|
root.addView(bar, FrameLayout.LayoutParams(
|
|
174
|
-
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
249
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
250
|
+
FrameLayout.LayoutParams.WRAP_CONTENT
|
|
175
251
|
).also { it.gravity = Gravity.BOTTOM })
|
|
176
252
|
|
|
253
|
+
// ETA arrival time
|
|
177
254
|
val etaTime = TextView(context).apply {
|
|
178
|
-
textSize =
|
|
179
|
-
setTextColor(Color.
|
|
255
|
+
textSize = 24f
|
|
256
|
+
setTextColor(Color.WHITE)
|
|
180
257
|
typeface = android.graphics.Typeface.DEFAULT_BOLD
|
|
181
258
|
}
|
|
182
259
|
bar.addView(etaTime, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f))
|
|
183
260
|
tvEtaTime = etaTime
|
|
184
261
|
|
|
262
|
+
// Duration + distance (center)
|
|
185
263
|
val center = LinearLayout(context).apply {
|
|
186
264
|
orientation = LinearLayout.VERTICAL
|
|
187
265
|
gravity = Gravity.CENTER
|
|
188
266
|
}
|
|
189
267
|
val dur = TextView(context).apply {
|
|
190
|
-
textSize =
|
|
268
|
+
textSize = 18f
|
|
269
|
+
setTextColor(Color.WHITE)
|
|
270
|
+
typeface = android.graphics.Typeface.DEFAULT_BOLD
|
|
271
|
+
gravity = Gravity.CENTER
|
|
191
272
|
}
|
|
192
273
|
val dist = TextView(context).apply {
|
|
193
|
-
textSize = 13f
|
|
274
|
+
textSize = 13f
|
|
275
|
+
setTextColor(Color.parseColor("#AAAAAA"))
|
|
276
|
+
gravity = Gravity.CENTER
|
|
194
277
|
}
|
|
195
278
|
center.addView(dur)
|
|
196
279
|
center.addView(dist)
|
|
@@ -198,27 +281,55 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
198
281
|
tvDuration = dur
|
|
199
282
|
tvDistance = dist
|
|
200
283
|
|
|
284
|
+
// Cancel button
|
|
201
285
|
val cancelBtn = TextView(context).apply {
|
|
202
|
-
text = "✕"
|
|
286
|
+
text = "✕"
|
|
287
|
+
textSize = 22f
|
|
288
|
+
setTextColor(Color.parseColor("#AAAAAA"))
|
|
203
289
|
gravity = Gravity.END or Gravity.CENTER_VERTICAL
|
|
204
290
|
setOnClickListener { cancelNavigation() }
|
|
205
291
|
}
|
|
206
292
|
bar.addView(cancelBtn, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f))
|
|
207
293
|
etaBar = bar
|
|
208
294
|
|
|
295
|
+
// TripProgressView — 1×1 hidden (required for render() API)
|
|
296
|
+
val tpv = MapboxTripProgressView(context)
|
|
297
|
+
root.addView(tpv as View, FrameLayout.LayoutParams(1, 1))
|
|
298
|
+
tpv.visibility = View.GONE
|
|
299
|
+
tripProgressView = tpv
|
|
300
|
+
|
|
209
301
|
addView(root)
|
|
210
302
|
}
|
|
211
303
|
|
|
304
|
+
// Helper: creates a white circle button (Waze style)
|
|
305
|
+
private fun makeCircleButton(text: String): TextView {
|
|
306
|
+
return TextView(context).apply {
|
|
307
|
+
this.text = text
|
|
308
|
+
textSize = 20f
|
|
309
|
+
gravity = Gravity.CENTER
|
|
310
|
+
setTextColor(Color.BLACK)
|
|
311
|
+
background = GradientDrawable().apply {
|
|
312
|
+
shape = GradientDrawable.OVAL
|
|
313
|
+
setColor(Color.WHITE)
|
|
314
|
+
// Drop shadow via elevation
|
|
315
|
+
}
|
|
316
|
+
elevation = 6 * dp
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private fun circleButtonParams(): LinearLayout.LayoutParams {
|
|
321
|
+
val size = (52 * dp).toInt()
|
|
322
|
+
return LinearLayout.LayoutParams(size, size)
|
|
323
|
+
}
|
|
324
|
+
|
|
212
325
|
// ─────────────────────────────────────────────────────────────────────────
|
|
213
|
-
// Init APIs
|
|
326
|
+
// Init APIs
|
|
214
327
|
// ─────────────────────────────────────────────────────────────────────────
|
|
215
328
|
private fun initAPIs() {
|
|
216
329
|
val distanceFormatterOptions = DistanceFormatterOptions.Builder(context).build()
|
|
217
330
|
|
|
218
331
|
maneuverApi = MapboxManeuverApi(MapboxDistanceFormatter(distanceFormatterOptions))
|
|
219
332
|
|
|
220
|
-
// TripProgressApi — from ShowTripProgressActivity official example
|
|
221
|
-
// PercentDistanceTraveledFormatter is optional — omit if causing issues
|
|
222
333
|
tripProgressApi = MapboxTripProgressApi(
|
|
223
334
|
TripProgressUpdateFormatter.Builder(context)
|
|
224
335
|
.distanceRemainingFormatter(DistanceRemainingFormatter(distanceFormatterOptions))
|
|
@@ -252,16 +363,35 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
252
363
|
mapView.mapboxMap.loadStyle(mapStyle ?: getAutoStyle()) { style ->
|
|
253
364
|
routeLineView.initializeLayers(style)
|
|
254
365
|
|
|
366
|
+
// Camera viewport
|
|
255
367
|
viewportDataSource = MapboxNavigationViewportDataSource(mapView.mapboxMap)
|
|
368
|
+
viewportDataSource.followingPadding = followingPadding
|
|
369
|
+
viewportDataSource.overviewPadding = overviewPadding
|
|
370
|
+
|
|
256
371
|
navigationCamera = NavigationCamera(
|
|
257
372
|
mapView.mapboxMap,
|
|
258
373
|
mapView.camera,
|
|
259
374
|
viewportDataSource
|
|
260
375
|
)
|
|
261
376
|
|
|
377
|
+
// ── Location puck — Waze-style arrow ─────────────────────────────
|
|
378
|
+
// FIX: use mapbox_navigation_puck_icon (built-in arrow)
|
|
379
|
+
// + PuckBearing.COURSE to orient it in direction of travel
|
|
262
380
|
mapView.location.apply {
|
|
263
381
|
setLocationProvider(navigationLocationProvider)
|
|
264
|
-
|
|
382
|
+
updateSettings {
|
|
383
|
+
// Built-in navigation arrow (chevron) from Nav SDK
|
|
384
|
+
locationPuck = LocationPuck2D(
|
|
385
|
+
bearingImage = ImageHolder.from(
|
|
386
|
+
com.mapbox.navigation.R.drawable.mapbox_navigation_puck_icon
|
|
387
|
+
)
|
|
388
|
+
)
|
|
389
|
+
// COURSE = direction of movement (not compass heading)
|
|
390
|
+
puckBearingEnabled = true
|
|
391
|
+
enabled = true
|
|
392
|
+
}
|
|
393
|
+
// Must set puckBearing separately — confirmed from migration guide
|
|
394
|
+
puckBearing = com.mapbox.maps.plugin.PuckBearing.COURSE
|
|
265
395
|
}
|
|
266
396
|
|
|
267
397
|
registerObservers()
|
|
@@ -285,6 +415,8 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
285
415
|
}
|
|
286
416
|
viewportDataSource.onRouteChanged(result.navigationRoutes.first())
|
|
287
417
|
viewportDataSource.evaluate()
|
|
418
|
+
isOverviewMode = false
|
|
419
|
+
safeCameraFollowing()
|
|
288
420
|
showUI()
|
|
289
421
|
onRoutesReady(mapOf(
|
|
290
422
|
"routeCount" to result.navigationRoutes.size,
|
|
@@ -310,21 +442,16 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
310
442
|
viewportDataSource.onRouteProgressChanged(routeProgress)
|
|
311
443
|
viewportDataSource.evaluate()
|
|
312
444
|
|
|
313
|
-
//
|
|
445
|
+
// Maneuver arrow on map
|
|
314
446
|
mapView.mapboxMap.style?.let { style ->
|
|
315
447
|
val arrowResult = routeArrowApi.addUpcomingManeuverArrow(routeProgress)
|
|
316
448
|
routeArrowView.renderManeuverUpdate(style, arrowResult)
|
|
317
449
|
}
|
|
318
450
|
|
|
319
|
-
//
|
|
320
|
-
// Fix for error: "Argument type mismatch: Unit? but R & Any expected"
|
|
321
|
-
// Solution: use explicit return type annotation on the success lambda
|
|
451
|
+
// Maneuver banner
|
|
322
452
|
val maneuvers = maneuverApi.getManeuvers(routeProgress)
|
|
323
453
|
maneuvers.fold(
|
|
324
|
-
{ error ->
|
|
325
|
-
Log.w(TAG, "Maneuver error: ${error.errorMessage}")
|
|
326
|
-
Unit
|
|
327
|
-
},
|
|
454
|
+
{ error -> Log.w(TAG, "Maneuver error: ${error.errorMessage}"); Unit },
|
|
328
455
|
{ _ ->
|
|
329
456
|
maneuverView?.visibility = View.VISIBLE
|
|
330
457
|
maneuverView?.renderManeuvers(maneuvers)
|
|
@@ -332,13 +459,7 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
332
459
|
}
|
|
333
460
|
)
|
|
334
461
|
|
|
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)
|
|
462
|
+
// Trip progress
|
|
342
463
|
val tripProgress = tripProgressApi.getTripProgress(routeProgress)
|
|
343
464
|
tripProgressView?.render(tripProgress)
|
|
344
465
|
updateEtaBar(
|
|
@@ -363,14 +484,22 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
363
484
|
override fun onNewRawLocation(rawLocation: Location) {}
|
|
364
485
|
override fun onNewLocationMatcherResult(result: LocationMatcherResult) {
|
|
365
486
|
val loc = result.enhancedLocation
|
|
487
|
+
|
|
366
488
|
navigationLocationProvider.changePosition(
|
|
367
489
|
location = loc,
|
|
368
490
|
keyPoints = result.keyPoints,
|
|
369
491
|
)
|
|
492
|
+
|
|
370
493
|
viewportDataSource.onLocationChanged(loc)
|
|
371
494
|
viewportDataSource.evaluate()
|
|
372
495
|
|
|
373
|
-
//
|
|
496
|
+
// First GPS fix → enter following mode
|
|
497
|
+
if (!firstLocationReceived) {
|
|
498
|
+
firstLocationReceived = true
|
|
499
|
+
safeCameraFollowing()
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Speed limit display
|
|
374
503
|
val fmtOptions = DistanceFormatterOptions.Builder(context).build()
|
|
375
504
|
val speedInfo = speedInfoApi.updatePostedAndCurrentSpeed(result, fmtOptions)
|
|
376
505
|
if (speedInfo != null) {
|
|
@@ -386,17 +515,51 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
386
515
|
}
|
|
387
516
|
|
|
388
517
|
// ─────────────────────────────────────────────────────────────────────────
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
|
|
392
|
-
//
|
|
518
|
+
// Button actions
|
|
519
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
520
|
+
|
|
521
|
+
// Toggle mute voice guidance
|
|
522
|
+
private fun toggleMute() {
|
|
523
|
+
isMuted = !isMuted
|
|
524
|
+
btnMute?.text = if (isMuted) "🔇" else "🔊"
|
|
525
|
+
btnMute?.setTextColor(if (isMuted) Color.parseColor("#FF4444") else Color.BLACK)
|
|
526
|
+
// Actual audio muting handled by the voice instruction observer in the app
|
|
527
|
+
// We expose the state via event so the RN layer can mute TTS
|
|
528
|
+
onNavigationCancelled(mapOf("type" to "mute", "muted" to isMuted))
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Toggle overview / following mode
|
|
532
|
+
private fun toggleOverview() {
|
|
533
|
+
if (isOverviewMode) {
|
|
534
|
+
recenterCamera()
|
|
535
|
+
} else {
|
|
536
|
+
isOverviewMode = true
|
|
537
|
+
btnOverview?.text = "◉"
|
|
538
|
+
btnRecenter?.visibility = View.VISIBLE
|
|
539
|
+
try {
|
|
540
|
+
navigationCamera.requestNavigationCameraToOverview()
|
|
541
|
+
} catch (e: Exception) {
|
|
542
|
+
Log.e(TAG, "Camera overview error: ${e.message}")
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Recenter camera to vehicle (following mode)
|
|
548
|
+
private fun recenterCamera() {
|
|
549
|
+
isOverviewMode = false
|
|
550
|
+
btnOverview?.text = "⊕"
|
|
551
|
+
btnRecenter?.visibility = View.GONE
|
|
552
|
+
safeCameraFollowing()
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
556
|
+
// ETA bar
|
|
393
557
|
// ─────────────────────────────────────────────────────────────────────────
|
|
394
558
|
private fun updateEtaBar(
|
|
395
559
|
estimatedTimeToArrivalMs: Long,
|
|
396
560
|
totalTimeRemainingSec: Double,
|
|
397
561
|
distanceRemainingMetres: Double
|
|
398
562
|
) {
|
|
399
|
-
// Arrival clock time from Unix timestamp
|
|
400
563
|
val arrivalCal = Calendar.getInstance().apply {
|
|
401
564
|
timeInMillis = estimatedTimeToArrivalMs
|
|
402
565
|
}
|
|
@@ -406,14 +569,12 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
406
569
|
arrivalCal.get(Calendar.MINUTE)
|
|
407
570
|
)
|
|
408
571
|
|
|
409
|
-
// Duration remaining
|
|
410
572
|
val totalMin = (totalTimeRemainingSec / 60).toInt()
|
|
411
573
|
tvDuration?.text = if (totalMin >= 60)
|
|
412
574
|
"${totalMin / 60}h ${totalMin % 60}min"
|
|
413
575
|
else
|
|
414
576
|
"${totalMin} min"
|
|
415
577
|
|
|
416
|
-
// Distance remaining
|
|
417
578
|
tvDistance?.text = if (distanceRemainingMetres >= 1000)
|
|
418
579
|
String.format("%.1f km", distanceRemainingMetres / 1000.0)
|
|
419
580
|
else
|
|
@@ -444,11 +605,21 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
444
605
|
}
|
|
445
606
|
}
|
|
446
607
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
611
|
+
// Camera following — issue #43 safe wrapper
|
|
612
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
613
|
+
private fun safeCameraFollowing() {
|
|
614
|
+
try {
|
|
615
|
+
navigationCamera.requestNavigationCameraToFollowing(
|
|
616
|
+
stateTransitionOptions = NavigationCameraTransitionOptions.Builder()
|
|
617
|
+
.maxDuration(0)
|
|
618
|
+
.build()
|
|
619
|
+
)
|
|
620
|
+
} catch (e: Exception) {
|
|
621
|
+
Log.e(TAG, "Camera following error: ${e.message}")
|
|
622
|
+
}
|
|
452
623
|
}
|
|
453
624
|
|
|
454
625
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -457,6 +628,8 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
457
628
|
private fun cancelNavigation() {
|
|
458
629
|
mapboxNavigation?.setNavigationRoutes(listOf())
|
|
459
630
|
mapboxNavigation?.stopTripSession()
|
|
631
|
+
firstLocationReceived = false
|
|
632
|
+
isOverviewMode = false
|
|
460
633
|
hideUI()
|
|
461
634
|
onNavigationCancelled(mapOf<String, Any>())
|
|
462
635
|
}
|
|
@@ -464,27 +637,19 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
464
637
|
private fun showUI() {
|
|
465
638
|
maneuverView?.visibility = View.VISIBLE
|
|
466
639
|
etaBar?.visibility = View.VISIBLE
|
|
640
|
+
sideButtons?.visibility = View.VISIBLE
|
|
467
641
|
}
|
|
468
642
|
|
|
469
643
|
private fun hideUI() {
|
|
470
644
|
maneuverView?.visibility = View.INVISIBLE
|
|
471
645
|
speedInfoView?.visibility = View.GONE
|
|
472
646
|
etaBar?.visibility = View.INVISIBLE
|
|
647
|
+
sideButtons?.visibility = View.INVISIBLE
|
|
473
648
|
}
|
|
474
649
|
|
|
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
|
|
650
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
651
|
+
// Issue #31 — Voice units
|
|
652
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
488
653
|
private fun resolveVoiceUnits(): String {
|
|
489
654
|
return when (voiceUnits?.lowercase()) {
|
|
490
655
|
"metric" -> "metric"
|
|
@@ -496,57 +661,83 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
496
661
|
}
|
|
497
662
|
}
|
|
498
663
|
|
|
664
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
499
665
|
// Route request
|
|
666
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
500
667
|
@SuppressLint("MissingPermission")
|
|
501
668
|
private fun fetchRoutes() {
|
|
502
669
|
val nav = mapboxNavigation ?: return
|
|
503
670
|
if (coordinates.size < 2) return
|
|
504
|
-
|
|
671
|
+
|
|
672
|
+
val points = coordinates.map {
|
|
673
|
+
Point.fromLngLat(it["longitude"] ?: 0.0, it["latitude"] ?: 0.0)
|
|
674
|
+
}
|
|
505
675
|
val locale = language?.let { Locale.forLanguageTag(it) } ?: Locale.getDefault()
|
|
676
|
+
|
|
506
677
|
val builder = RouteOptions.builder()
|
|
507
678
|
.applyDefaultNavigationOptions()
|
|
508
679
|
.language(locale.toLanguageTag())
|
|
509
680
|
.voiceUnits(resolveVoiceUnits())
|
|
510
681
|
.coordinatesList(points)
|
|
511
|
-
.annotations("maxspeed,congestion,duration")
|
|
682
|
+
.annotations("maxspeed,congestion,duration,speed")
|
|
683
|
+
|
|
512
684
|
waypointIndices?.let { builder.waypointIndicesList(it) }
|
|
513
|
-
navigationProfile?.let {
|
|
514
|
-
|
|
685
|
+
navigationProfile?.let {
|
|
686
|
+
builder.profile(if (it.startsWith("mapbox/")) it else "mapbox/$it")
|
|
687
|
+
}
|
|
688
|
+
excludeTypes?.takeIf { it.isNotEmpty() }?.let {
|
|
689
|
+
builder.exclude(it.joinToString(","))
|
|
690
|
+
}
|
|
515
691
|
maxHeight?.let { builder.maxHeight(it) }
|
|
516
692
|
maxWidth?.let { builder.maxWidth(it) }
|
|
693
|
+
|
|
517
694
|
nav.requestRoutes(builder.build(), object : NavigationRouterCallback {
|
|
518
|
-
override fun onRoutesReady(
|
|
519
|
-
|
|
695
|
+
override fun onRoutesReady(
|
|
696
|
+
routes: List<NavigationRoute>,
|
|
697
|
+
@RouterOrigin routerOrigin: String
|
|
698
|
+
) {
|
|
699
|
+
if (routes.isEmpty()) {
|
|
700
|
+
onRoutesFailed(mapOf("message" to "No routes returned"))
|
|
701
|
+
return
|
|
702
|
+
}
|
|
520
703
|
nav.setNavigationRoutes(routes)
|
|
521
704
|
nav.startTripSession()
|
|
522
|
-
safeCameraFollowing()
|
|
523
705
|
}
|
|
706
|
+
|
|
524
707
|
override fun onFailure(reasons: List<RouterFailure>, routeOptions: RouteOptions) {
|
|
525
708
|
val msg = reasons.firstOrNull()?.message ?: "Unknown error"
|
|
526
709
|
Log.e(TAG, "Route failed: $msg")
|
|
527
710
|
onRoutesFailed(mapOf("message" to msg))
|
|
528
711
|
}
|
|
529
|
-
|
|
712
|
+
|
|
713
|
+
override fun onCanceled(
|
|
714
|
+
routeOptions: RouteOptions,
|
|
715
|
+
@RouterOrigin routerOrigin: String
|
|
716
|
+
) {
|
|
530
717
|
Log.d(TAG, "Route cancelled")
|
|
531
718
|
}
|
|
532
719
|
})
|
|
533
720
|
}
|
|
534
721
|
|
|
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
|
-
|
|
722
|
+
// ── Prop setters ──────────────────────────────────────────────────────────
|
|
723
|
+
fun setCoordinates(coords: List<Map<String, Double>>) {
|
|
724
|
+
coordinates = coords
|
|
725
|
+
if (coords.size >= 2) fetchRoutes()
|
|
726
|
+
}
|
|
727
|
+
fun setWaypointIndices(i: List<Int>?) { waypointIndices = i }
|
|
728
|
+
fun setLanguage(l: String?) { language = l }
|
|
729
|
+
fun setVoiceUnits(u: String?) { voiceUnits = u }
|
|
730
|
+
fun setNavigationProfile(p: String?) { navigationProfile = p }
|
|
731
|
+
fun setExcludeTypes(t: List<String>?) { excludeTypes = t }
|
|
732
|
+
fun setMapStyle(s: String?) { mapStyle = s }
|
|
733
|
+
fun setMute(m: Boolean) { mute = m; if (m != isMuted) toggleMute() }
|
|
734
|
+
fun setMaxHeight(h: Double?) { maxHeight = h }
|
|
735
|
+
fun setMaxWidth(w: Double?) { maxWidth = w }
|
|
736
|
+
fun setUseMapMatching(u: Boolean) { useMapMatching = u }
|
|
737
|
+
fun setCustomRasterTileUrl(u: String?) { customRasterTileUrl = u }
|
|
738
|
+
fun setCustomRasterAboveLayerId(l: String?) { customRasterAboveLayerId = l }
|
|
739
|
+
|
|
740
|
+
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
|
550
741
|
override fun onDetachedFromWindow() {
|
|
551
742
|
super.onDetachedFromWindow()
|
|
552
743
|
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.3",
|
|
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",
|