@jacques_gordon/expo-mapbox-navigation 2.1.3 → 2.1.4
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.
package/android/build.gradle
CHANGED
|
@@ -74,6 +74,9 @@ dependencies {
|
|
|
74
74
|
// UI Maps (NavigationStyles, NavigationLocationProvider, NavigationCamera)
|
|
75
75
|
implementation "com.mapbox.navigationcore:ui-maps:${MAPBOX_NAV_VERSION}"
|
|
76
76
|
|
|
77
|
+
// Voice instructions (MapboxSpeechApi, MapboxVoiceInstructionsPlayer, SpeechVolume)
|
|
78
|
+
implementation "com.mapbox.navigationcore:voice:${MAPBOX_NAV_VERSION}"
|
|
79
|
+
|
|
77
80
|
// ConstraintLayout — required because MapboxManeuverView extends ConstraintLayout
|
|
78
81
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
|
79
82
|
|
|
@@ -3,7 +3,15 @@ package expo.modules.mapboxnavigation
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
4
|
import android.content.Context
|
|
5
5
|
import android.content.res.Resources
|
|
6
|
+
import android.graphics.Bitmap
|
|
7
|
+
import android.graphics.Canvas
|
|
6
8
|
import android.graphics.Color
|
|
9
|
+
import android.graphics.Paint
|
|
10
|
+
import android.graphics.Path
|
|
11
|
+
import android.graphics.PorterDuff
|
|
12
|
+
import android.graphics.PorterDuffXfermode
|
|
13
|
+
import android.graphics.RectF
|
|
14
|
+
import android.graphics.drawable.BitmapDrawable
|
|
7
15
|
import android.graphics.drawable.GradientDrawable
|
|
8
16
|
import android.util.Log
|
|
9
17
|
import android.view.Gravity
|
|
@@ -19,10 +27,9 @@ import com.mapbox.maps.EdgeInsets
|
|
|
19
27
|
import com.mapbox.maps.ImageHolder
|
|
20
28
|
import com.mapbox.maps.MapView
|
|
21
29
|
import com.mapbox.maps.plugin.LocationPuck2D
|
|
30
|
+
import com.mapbox.maps.plugin.PuckBearing
|
|
22
31
|
import com.mapbox.maps.plugin.animation.camera
|
|
23
32
|
import com.mapbox.maps.plugin.locationcomponent.location
|
|
24
|
-
import com.mapbox.maps.plugin.locationcomponent.OnIndicatorBearingChangedListener
|
|
25
|
-
import com.mapbox.maps.plugin.locationcomponent.OnIndicatorPositionChangedListener
|
|
26
33
|
import com.mapbox.navigation.base.TimeFormat
|
|
27
34
|
import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions
|
|
28
35
|
import com.mapbox.navigation.base.formatter.DistanceFormatterOptions
|
|
@@ -39,6 +46,7 @@ import com.mapbox.navigation.core.formatter.MapboxDistanceFormatter
|
|
|
39
46
|
import com.mapbox.navigation.core.trip.session.LocationMatcherResult
|
|
40
47
|
import com.mapbox.navigation.core.trip.session.LocationObserver
|
|
41
48
|
import com.mapbox.navigation.core.trip.session.RouteProgressObserver
|
|
49
|
+
import com.mapbox.navigation.core.trip.session.VoiceInstructionsObserver
|
|
42
50
|
import com.mapbox.navigation.tripdata.maneuver.api.MapboxManeuverApi
|
|
43
51
|
import com.mapbox.navigation.tripdata.progress.api.MapboxTripProgressApi
|
|
44
52
|
import com.mapbox.navigation.tripdata.progress.model.DistanceRemainingFormatter
|
|
@@ -61,6 +69,12 @@ import com.mapbox.navigation.ui.maps.route.line.api.MapboxRouteLineApi
|
|
|
61
69
|
import com.mapbox.navigation.ui.maps.route.line.api.MapboxRouteLineView
|
|
62
70
|
import com.mapbox.navigation.ui.maps.route.line.model.MapboxRouteLineApiOptions
|
|
63
71
|
import com.mapbox.navigation.ui.maps.route.line.model.MapboxRouteLineViewOptions
|
|
72
|
+
import com.mapbox.navigation.voice.api.MapboxSpeechApi
|
|
73
|
+
import com.mapbox.navigation.voice.api.MapboxVoiceInstructionsPlayer
|
|
74
|
+
import com.mapbox.navigation.voice.model.SpeechAnnouncement
|
|
75
|
+
import com.mapbox.navigation.voice.model.SpeechError
|
|
76
|
+
import com.mapbox.navigation.voice.model.SpeechValue
|
|
77
|
+
import com.mapbox.navigation.voice.model.SpeechVolume
|
|
64
78
|
import expo.modules.kotlin.AppContext
|
|
65
79
|
import expo.modules.kotlin.viewevent.EventDispatcher
|
|
66
80
|
import expo.modules.kotlin.views.ExpoView
|
|
@@ -89,11 +103,9 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
89
103
|
private var tvDuration: TextView? = null
|
|
90
104
|
private var tvDistance: TextView? = null
|
|
91
105
|
private var etaBar: LinearLayout? = null
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
private var
|
|
95
|
-
private var btnOverview: TextView? = null
|
|
96
|
-
private var btnRecenter: TextView? = null
|
|
106
|
+
private var btnMuteView: ImageView? = null
|
|
107
|
+
private var btnOverviewView: ImageView? = null
|
|
108
|
+
private var btnRecenterView: ImageView? = null
|
|
97
109
|
private var sideButtons: LinearLayout? = null
|
|
98
110
|
|
|
99
111
|
// ── Navigation APIs ───────────────────────────────────────────────────────
|
|
@@ -109,6 +121,10 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
109
121
|
private val navigationLocationProvider = NavigationLocationProvider()
|
|
110
122
|
private var mapboxNavigation: MapboxNavigation? = null
|
|
111
123
|
|
|
124
|
+
// ── Voice APIs ────────────────────────────────────────────────────────────
|
|
125
|
+
private lateinit var speechApi: MapboxSpeechApi
|
|
126
|
+
private lateinit var voiceInstructionsPlayer: MapboxVoiceInstructionsPlayer
|
|
127
|
+
|
|
112
128
|
// ── State ─────────────────────────────────────────────────────────────────
|
|
113
129
|
private var isNightMode = false
|
|
114
130
|
private var isMuted = false
|
|
@@ -118,25 +134,12 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
118
134
|
// ── Pixel density ─────────────────────────────────────────────────────────
|
|
119
135
|
private val dp = context.resources.displayMetrics.density
|
|
120
136
|
|
|
121
|
-
// ── Viewport padding
|
|
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
|
|
137
|
+
// ── Viewport padding ──────────────────────────────────────────────────────
|
|
125
138
|
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
|
-
)
|
|
139
|
+
EdgeInsets(180.0 * dp, 40.0 * dp, 300.0 * dp, 40.0 * dp)
|
|
132
140
|
}
|
|
133
141
|
private val overviewPadding by lazy {
|
|
134
|
-
EdgeInsets(
|
|
135
|
-
120.0 * dp,
|
|
136
|
-
40.0 * dp,
|
|
137
|
-
120.0 * dp,
|
|
138
|
-
40.0 * dp
|
|
139
|
-
)
|
|
142
|
+
EdgeInsets(120.0 * dp, 40.0 * dp, 120.0 * dp, 40.0 * dp)
|
|
140
143
|
}
|
|
141
144
|
|
|
142
145
|
// ── Props ─────────────────────────────────────────────────────────────────
|
|
@@ -161,7 +164,124 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
161
164
|
}
|
|
162
165
|
|
|
163
166
|
// ─────────────────────────────────────────────────────────────────────────
|
|
164
|
-
//
|
|
167
|
+
// Draw icons programmatically (matching screenshot: speaker, route, arrow)
|
|
168
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
private fun drawSpeakerIcon(muted: Boolean): Bitmap {
|
|
171
|
+
val size = (44 * dp).toInt()
|
|
172
|
+
val bmp = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
|
|
173
|
+
val c = Canvas(bmp)
|
|
174
|
+
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
175
|
+
color = Color.BLACK
|
|
176
|
+
style = Paint.Style.FILL
|
|
177
|
+
}
|
|
178
|
+
val s = size.toFloat()
|
|
179
|
+
// Speaker body (trapezoid)
|
|
180
|
+
val body = Path().apply {
|
|
181
|
+
moveTo(s * 0.18f, s * 0.38f)
|
|
182
|
+
lineTo(s * 0.38f, s * 0.38f)
|
|
183
|
+
lineTo(s * 0.58f, s * 0.22f)
|
|
184
|
+
lineTo(s * 0.58f, s * 0.78f)
|
|
185
|
+
lineTo(s * 0.38f, s * 0.62f)
|
|
186
|
+
lineTo(s * 0.18f, s * 0.62f)
|
|
187
|
+
close()
|
|
188
|
+
}
|
|
189
|
+
c.drawPath(body, paint)
|
|
190
|
+
if (!muted) {
|
|
191
|
+
// Sound waves arcs
|
|
192
|
+
paint.style = Paint.Style.STROKE
|
|
193
|
+
paint.strokeWidth = s * 0.065f
|
|
194
|
+
paint.strokeCap = Paint.Cap.ROUND
|
|
195
|
+
// Inner arc
|
|
196
|
+
c.drawArc(RectF(s*0.60f, s*0.33f, s*0.80f, s*0.67f), -60f, 120f, false, paint)
|
|
197
|
+
// Outer arc
|
|
198
|
+
c.drawArc(RectF(s*0.64f, s*0.24f, s*0.92f, s*0.76f), -55f, 110f, false, paint)
|
|
199
|
+
} else {
|
|
200
|
+
// X mark for muted
|
|
201
|
+
paint.style = Paint.Style.STROKE
|
|
202
|
+
paint.strokeWidth = s * 0.07f
|
|
203
|
+
paint.strokeCap = Paint.Cap.ROUND
|
|
204
|
+
paint.color = Color.parseColor("#FF4444")
|
|
205
|
+
c.drawLine(s*0.65f, s*0.32f, s*0.88f, s*0.68f, paint)
|
|
206
|
+
c.drawLine(s*0.88f, s*0.32f, s*0.65f, s*0.68f, paint)
|
|
207
|
+
}
|
|
208
|
+
return bmp
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private fun drawRouteOverviewIcon(): Bitmap {
|
|
212
|
+
val size = (44 * dp).toInt()
|
|
213
|
+
val bmp = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
|
|
214
|
+
val c = Canvas(bmp)
|
|
215
|
+
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
216
|
+
color = Color.BLACK
|
|
217
|
+
style = Paint.Style.STROKE
|
|
218
|
+
strokeWidth = size * 0.08f
|
|
219
|
+
strokeCap = Paint.Cap.ROUND
|
|
220
|
+
strokeJoin = Paint.Join.ROUND
|
|
221
|
+
}
|
|
222
|
+
val s = size.toFloat()
|
|
223
|
+
// Route line with S-curve (like the image: start dot → curve → end pin)
|
|
224
|
+
// Start dot
|
|
225
|
+
paint.style = Paint.Style.FILL
|
|
226
|
+
c.drawCircle(s * 0.28f, s * 0.75f, s * 0.07f, paint)
|
|
227
|
+
// Route path
|
|
228
|
+
paint.style = Paint.Style.STROKE
|
|
229
|
+
val routePath = Path().apply {
|
|
230
|
+
moveTo(s * 0.28f, s * 0.75f)
|
|
231
|
+
cubicTo(
|
|
232
|
+
s * 0.28f, s * 0.45f,
|
|
233
|
+
s * 0.72f, s * 0.55f,
|
|
234
|
+
s * 0.72f, s * 0.25f
|
|
235
|
+
)
|
|
236
|
+
}
|
|
237
|
+
c.drawPath(routePath, paint)
|
|
238
|
+
// End pin (circle with dot)
|
|
239
|
+
paint.style = Paint.Style.STROKE
|
|
240
|
+
c.drawCircle(s * 0.72f, s * 0.22f, s * 0.10f, paint)
|
|
241
|
+
paint.style = Paint.Style.FILL
|
|
242
|
+
c.drawCircle(s * 0.72f, s * 0.22f, s * 0.04f, paint)
|
|
243
|
+
return bmp
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private fun drawNavigationArrowIcon(): Bitmap {
|
|
247
|
+
val size = (44 * dp).toInt()
|
|
248
|
+
val bmp = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
|
|
249
|
+
val c = Canvas(bmp)
|
|
250
|
+
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
251
|
+
color = Color.BLACK
|
|
252
|
+
style = Paint.Style.FILL
|
|
253
|
+
}
|
|
254
|
+
val s = size.toFloat()
|
|
255
|
+
// Navigation arrow pointing up (filled chevron/arrow like Waze)
|
|
256
|
+
val arrow = Path().apply {
|
|
257
|
+
moveTo(s * 0.50f, s * 0.14f) // tip
|
|
258
|
+
lineTo(s * 0.78f, s * 0.76f) // bottom-right
|
|
259
|
+
lineTo(s * 0.50f, s * 0.60f) // bottom-center indent
|
|
260
|
+
lineTo(s * 0.22f, s * 0.76f) // bottom-left
|
|
261
|
+
close()
|
|
262
|
+
}
|
|
263
|
+
c.drawPath(arrow, paint)
|
|
264
|
+
return bmp
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private fun makeIconButton(bitmap: Bitmap, onClick: () -> Unit): ImageView {
|
|
268
|
+
val size = (56 * dp).toInt()
|
|
269
|
+
val iv = ImageView(context).apply {
|
|
270
|
+
setImageBitmap(bitmap)
|
|
271
|
+
scaleType = ImageView.ScaleType.CENTER_INSIDE
|
|
272
|
+
setPadding((8 * dp).toInt(), (8 * dp).toInt(), (8 * dp).toInt(), (8 * dp).toInt())
|
|
273
|
+
background = GradientDrawable().apply {
|
|
274
|
+
shape = GradientDrawable.OVAL
|
|
275
|
+
setColor(Color.WHITE)
|
|
276
|
+
}
|
|
277
|
+
elevation = 6 * dp
|
|
278
|
+
setOnClickListener { onClick() }
|
|
279
|
+
}
|
|
280
|
+
return iv
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
284
|
+
// Build UI
|
|
165
285
|
// ─────────────────────────────────────────────────────────────────────────
|
|
166
286
|
private fun buildUI() {
|
|
167
287
|
val root = FrameLayout(context).apply {
|
|
@@ -174,7 +294,7 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
174
294
|
FrameLayout.LayoutParams.MATCH_PARENT
|
|
175
295
|
))
|
|
176
296
|
|
|
177
|
-
// ── ManeuverView — top
|
|
297
|
+
// ── ManeuverView — top ─────────────────────────────────────────────────
|
|
178
298
|
val mv = MapboxManeuverView(context)
|
|
179
299
|
root.addView(mv as View, FrameLayout.LayoutParams(
|
|
180
300
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
@@ -183,19 +303,8 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
183
303
|
mv.visibility = View.INVISIBLE
|
|
184
304
|
maneuverView = mv
|
|
185
305
|
|
|
186
|
-
// ──
|
|
187
|
-
|
|
188
|
-
root.addView(siv as View, FrameLayout.LayoutParams(
|
|
189
|
-
(72 * dp).toInt(),
|
|
190
|
-
(72 * dp).toInt()
|
|
191
|
-
).also {
|
|
192
|
-
it.gravity = Gravity.BOTTOM or Gravity.START
|
|
193
|
-
it.setMargins((12 * dp).toInt(), 0, 0, (88 * dp).toInt())
|
|
194
|
-
})
|
|
195
|
-
siv.visibility = View.GONE
|
|
196
|
-
speedInfoView = siv
|
|
197
|
-
|
|
198
|
-
// ── Side action buttons (right side, Waze-style) ──────────────────────
|
|
306
|
+
// ── Side buttons — RIGHT side, just below maneuver banner ─────────────
|
|
307
|
+
// Position: top margin = maneuver banner height (~180dp) + 8dp gap
|
|
199
308
|
val sideCol = LinearLayout(context).apply {
|
|
200
309
|
orientation = LinearLayout.VERTICAL
|
|
201
310
|
gravity = Gravity.CENTER_HORIZONTAL
|
|
@@ -205,33 +314,45 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
205
314
|
FrameLayout.LayoutParams.WRAP_CONTENT,
|
|
206
315
|
FrameLayout.LayoutParams.WRAP_CONTENT
|
|
207
316
|
).also {
|
|
208
|
-
it.gravity = Gravity.
|
|
209
|
-
it.setMargins(0,
|
|
317
|
+
it.gravity = Gravity.TOP or Gravity.END
|
|
318
|
+
it.setMargins(0, (188 * dp).toInt(), (12 * dp).toInt(), 0)
|
|
210
319
|
})
|
|
211
320
|
sideButtons = sideCol
|
|
212
321
|
|
|
213
|
-
|
|
214
|
-
val
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
btnMute = muteBtn
|
|
218
|
-
|
|
219
|
-
sideCol.addView(View(context), LinearLayout.LayoutParams(1, (10 * dp).toInt()))
|
|
322
|
+
val btnSize = (56 * dp).toInt()
|
|
323
|
+
val btnParams = LinearLayout.LayoutParams(btnSize, btnSize).also {
|
|
324
|
+
it.bottomMargin = (10 * dp).toInt()
|
|
325
|
+
}
|
|
220
326
|
|
|
221
|
-
//
|
|
222
|
-
val
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
btnOverview = overviewBtn
|
|
327
|
+
// Button 1: Mute/Unmute (speaker icon)
|
|
328
|
+
val muteBtn = makeIconButton(drawSpeakerIcon(false)) { toggleMute() }
|
|
329
|
+
sideCol.addView(muteBtn, btnParams)
|
|
330
|
+
btnMuteView = muteBtn
|
|
226
331
|
|
|
227
|
-
|
|
332
|
+
// Button 2: Overview / Route view
|
|
333
|
+
val overviewBtn = makeIconButton(drawRouteOverviewIcon()) { toggleOverview() }
|
|
334
|
+
sideCol.addView(overviewBtn, LinearLayout.LayoutParams(btnSize, btnSize).also {
|
|
335
|
+
it.bottomMargin = (10 * dp).toInt()
|
|
336
|
+
})
|
|
337
|
+
btnOverviewView = overviewBtn
|
|
228
338
|
|
|
229
|
-
// Recenter
|
|
230
|
-
val recenterBtn =
|
|
231
|
-
recenterBtn.setOnClickListener { recenterCamera() }
|
|
339
|
+
// Button 3: Recenter / Navigation arrow (hidden until in overview mode)
|
|
340
|
+
val recenterBtn = makeIconButton(drawNavigationArrowIcon()) { recenterCamera() }
|
|
232
341
|
recenterBtn.visibility = View.GONE
|
|
233
|
-
sideCol.addView(recenterBtn,
|
|
234
|
-
|
|
342
|
+
sideCol.addView(recenterBtn, LinearLayout.LayoutParams(btnSize, btnSize))
|
|
343
|
+
btnRecenterView = recenterBtn
|
|
344
|
+
|
|
345
|
+
// ── SpeedInfoView — bottom-left, above ETA bar ────────────────────────
|
|
346
|
+
val siv = MapboxSpeedInfoView(context)
|
|
347
|
+
root.addView(siv as View, FrameLayout.LayoutParams(
|
|
348
|
+
(72 * dp).toInt(),
|
|
349
|
+
(72 * dp).toInt()
|
|
350
|
+
).also {
|
|
351
|
+
it.gravity = Gravity.BOTTOM or Gravity.START
|
|
352
|
+
it.setMargins((12 * dp).toInt(), 0, 0, (88 * dp).toInt())
|
|
353
|
+
})
|
|
354
|
+
siv.visibility = View.GONE
|
|
355
|
+
speedInfoView = siv
|
|
235
356
|
|
|
236
357
|
// ── ETA bottom bar ─────────────────────────────────────────────────────
|
|
237
358
|
val bar = LinearLayout(context).apply {
|
|
@@ -250,42 +371,29 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
250
371
|
FrameLayout.LayoutParams.WRAP_CONTENT
|
|
251
372
|
).also { it.gravity = Gravity.BOTTOM })
|
|
252
373
|
|
|
253
|
-
// ETA arrival time
|
|
254
374
|
val etaTime = TextView(context).apply {
|
|
255
|
-
textSize = 24f
|
|
256
|
-
setTextColor(Color.WHITE)
|
|
375
|
+
textSize = 24f; setTextColor(Color.WHITE)
|
|
257
376
|
typeface = android.graphics.Typeface.DEFAULT_BOLD
|
|
258
377
|
}
|
|
259
378
|
bar.addView(etaTime, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f))
|
|
260
379
|
tvEtaTime = etaTime
|
|
261
380
|
|
|
262
|
-
// Duration + distance (center)
|
|
263
381
|
val center = LinearLayout(context).apply {
|
|
264
|
-
orientation = LinearLayout.VERTICAL
|
|
265
|
-
gravity = Gravity.CENTER
|
|
382
|
+
orientation = LinearLayout.VERTICAL; gravity = Gravity.CENTER
|
|
266
383
|
}
|
|
267
384
|
val dur = TextView(context).apply {
|
|
268
|
-
textSize = 18f
|
|
269
|
-
|
|
270
|
-
typeface = android.graphics.Typeface.DEFAULT_BOLD
|
|
271
|
-
gravity = Gravity.CENTER
|
|
385
|
+
textSize = 18f; setTextColor(Color.WHITE)
|
|
386
|
+
typeface = android.graphics.Typeface.DEFAULT_BOLD; gravity = Gravity.CENTER
|
|
272
387
|
}
|
|
273
388
|
val dist = TextView(context).apply {
|
|
274
|
-
textSize = 13f
|
|
275
|
-
setTextColor(Color.parseColor("#AAAAAA"))
|
|
276
|
-
gravity = Gravity.CENTER
|
|
389
|
+
textSize = 13f; setTextColor(Color.parseColor("#AAAAAA")); gravity = Gravity.CENTER
|
|
277
390
|
}
|
|
278
|
-
center.addView(dur)
|
|
279
|
-
center.addView(dist)
|
|
391
|
+
center.addView(dur); center.addView(dist)
|
|
280
392
|
bar.addView(center, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 2f))
|
|
281
|
-
tvDuration = dur
|
|
282
|
-
tvDistance = dist
|
|
393
|
+
tvDuration = dur; tvDistance = dist
|
|
283
394
|
|
|
284
|
-
// Cancel button
|
|
285
395
|
val cancelBtn = TextView(context).apply {
|
|
286
|
-
text = "✕"
|
|
287
|
-
textSize = 22f
|
|
288
|
-
setTextColor(Color.parseColor("#AAAAAA"))
|
|
396
|
+
text = "✕"; textSize = 22f; setTextColor(Color.parseColor("#AAAAAA"))
|
|
289
397
|
gravity = Gravity.END or Gravity.CENTER_VERTICAL
|
|
290
398
|
setOnClickListener { cancelNavigation() }
|
|
291
399
|
}
|
|
@@ -301,27 +409,6 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
301
409
|
addView(root)
|
|
302
410
|
}
|
|
303
411
|
|
|
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
|
-
|
|
325
412
|
// ─────────────────────────────────────────────────────────────────────────
|
|
326
413
|
// Init APIs
|
|
327
414
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -350,6 +437,55 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
350
437
|
)
|
|
351
438
|
|
|
352
439
|
routeArrowView = MapboxRouteArrowView(RouteArrowOptions.Builder(context).build())
|
|
440
|
+
|
|
441
|
+
// ── Voice APIs ─────────────────────────────────────────────────────────
|
|
442
|
+
// Use device locale for TTS, fall back to "en" if needed
|
|
443
|
+
val voiceLang = language ?: Locale.getDefault().language.let {
|
|
444
|
+
if (it.isEmpty()) "en" else it
|
|
445
|
+
}
|
|
446
|
+
speechApi = MapboxSpeechApi(context, voiceLang)
|
|
447
|
+
voiceInstructionsPlayer = MapboxVoiceInstructionsPlayer(context, voiceLang)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// ── Voice callbacks (exact pattern from official TurnByTurnExperienceActivity) ──
|
|
451
|
+
// SpeechError and SpeechValue are swapped vs v2: fold(error, value) in v3
|
|
452
|
+
// speechCallback: Consumer<Expected<SpeechError, SpeechValue>>
|
|
453
|
+
// Exact pattern from official TurnByTurnExperienceActivity
|
|
454
|
+
// NOTE: in v3 fold order is (error, value) — SpeechError is left, SpeechValue is right
|
|
455
|
+
private val speechCallback =
|
|
456
|
+
com.mapbox.navigation.base.route.MapboxNavigationConsumer<
|
|
457
|
+
com.mapbox.bindgen.Expected<SpeechError, SpeechValue>
|
|
458
|
+
> { expected ->
|
|
459
|
+
expected.fold(
|
|
460
|
+
{ error ->
|
|
461
|
+
// On-device TTS fallback when MP3 is not available
|
|
462
|
+
if (!isMuted) {
|
|
463
|
+
voiceInstructionsPlayer.play(
|
|
464
|
+
error.fallback,
|
|
465
|
+
voiceInstructionsPlayerCallback
|
|
466
|
+
)
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
{ value ->
|
|
470
|
+
// Play the synthesized MP3 from Mapbox Voice API
|
|
471
|
+
if (!isMuted) {
|
|
472
|
+
voiceInstructionsPlayer.play(
|
|
473
|
+
value.announcement,
|
|
474
|
+
voiceInstructionsPlayerCallback
|
|
475
|
+
)
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
private val voiceInstructionsPlayerCallback =
|
|
482
|
+
{ announcement: SpeechAnnouncement ->
|
|
483
|
+
// Cleanup the file after playing
|
|
484
|
+
speechApi.clean(announcement)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
private val voiceInstructionsObserver = VoiceInstructionsObserver { voiceInstructions ->
|
|
488
|
+
speechApi.generate(voiceInstructions, speechCallback)
|
|
353
489
|
}
|
|
354
490
|
|
|
355
491
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -363,7 +499,6 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
363
499
|
mapView.mapboxMap.loadStyle(mapStyle ?: getAutoStyle()) { style ->
|
|
364
500
|
routeLineView.initializeLayers(style)
|
|
365
501
|
|
|
366
|
-
// Camera viewport
|
|
367
502
|
viewportDataSource = MapboxNavigationViewportDataSource(mapView.mapboxMap)
|
|
368
503
|
viewportDataSource.followingPadding = followingPadding
|
|
369
504
|
viewportDataSource.overviewPadding = overviewPadding
|
|
@@ -374,24 +509,19 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
374
509
|
viewportDataSource
|
|
375
510
|
)
|
|
376
511
|
|
|
377
|
-
//
|
|
378
|
-
// FIX: use mapbox_navigation_puck_icon (built-in arrow)
|
|
379
|
-
// + PuckBearing.COURSE to orient it in direction of travel
|
|
512
|
+
// Navigation arrow puck
|
|
380
513
|
mapView.location.apply {
|
|
381
514
|
setLocationProvider(navigationLocationProvider)
|
|
382
515
|
updateSettings {
|
|
383
|
-
// Built-in navigation arrow (chevron) from Nav SDK
|
|
384
516
|
locationPuck = LocationPuck2D(
|
|
385
517
|
bearingImage = ImageHolder.from(
|
|
386
518
|
com.mapbox.navigation.R.drawable.mapbox_navigation_puck_icon
|
|
387
519
|
)
|
|
388
520
|
)
|
|
389
|
-
// COURSE = direction of movement (not compass heading)
|
|
390
521
|
puckBearingEnabled = true
|
|
391
522
|
enabled = true
|
|
392
523
|
}
|
|
393
|
-
|
|
394
|
-
puckBearing = com.mapbox.maps.plugin.PuckBearing.COURSE
|
|
524
|
+
puckBearing = PuckBearing.COURSE
|
|
395
525
|
}
|
|
396
526
|
|
|
397
527
|
registerObservers()
|
|
@@ -408,10 +538,12 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
408
538
|
nav.registerRoutesObserver(object : RoutesObserver {
|
|
409
539
|
override fun onRoutesChanged(result: RoutesUpdatedResult) {
|
|
410
540
|
if (result.navigationRoutes.isNotEmpty()) {
|
|
541
|
+
// Cancel any in-flight speech when route changes
|
|
542
|
+
speechApi.cancel()
|
|
543
|
+
voiceInstructionsPlayer.clear()
|
|
544
|
+
|
|
411
545
|
routeLineApi.setNavigationRoutes(result.navigationRoutes) { value ->
|
|
412
|
-
mapView.mapboxMap.style?.apply {
|
|
413
|
-
routeLineView.renderRouteDrawData(this, value)
|
|
414
|
-
}
|
|
546
|
+
mapView.mapboxMap.style?.apply { routeLineView.renderRouteDrawData(this, value) }
|
|
415
547
|
}
|
|
416
548
|
viewportDataSource.onRouteChanged(result.navigationRoutes.first())
|
|
417
549
|
viewportDataSource.evaluate()
|
|
@@ -442,13 +574,11 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
442
574
|
viewportDataSource.onRouteProgressChanged(routeProgress)
|
|
443
575
|
viewportDataSource.evaluate()
|
|
444
576
|
|
|
445
|
-
// Maneuver arrow on map
|
|
446
577
|
mapView.mapboxMap.style?.let { style ->
|
|
447
578
|
val arrowResult = routeArrowApi.addUpcomingManeuverArrow(routeProgress)
|
|
448
579
|
routeArrowView.renderManeuverUpdate(style, arrowResult)
|
|
449
580
|
}
|
|
450
581
|
|
|
451
|
-
// Maneuver banner
|
|
452
582
|
val maneuvers = maneuverApi.getManeuvers(routeProgress)
|
|
453
583
|
maneuvers.fold(
|
|
454
584
|
{ error -> Log.w(TAG, "Maneuver error: ${error.errorMessage}"); Unit },
|
|
@@ -459,7 +589,6 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
459
589
|
}
|
|
460
590
|
)
|
|
461
591
|
|
|
462
|
-
// Trip progress
|
|
463
592
|
val tripProgress = tripProgressApi.getTripProgress(routeProgress)
|
|
464
593
|
tripProgressView?.render(tripProgress)
|
|
465
594
|
updateEtaBar(
|
|
@@ -474,8 +603,7 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
474
603
|
"distanceTraveled" to routeProgress.distanceTraveled,
|
|
475
604
|
"fractionTraveled" to routeProgress.fractionTraveled,
|
|
476
605
|
"currentStepDistanceRemaining" to
|
|
477
|
-
(routeProgress.currentLegProgress
|
|
478
|
-
?.currentStepProgress?.distanceRemaining ?: 0f)
|
|
606
|
+
(routeProgress.currentLegProgress?.currentStepProgress?.distanceRemaining ?: 0f)
|
|
479
607
|
))
|
|
480
608
|
})
|
|
481
609
|
|
|
@@ -484,22 +612,15 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
484
612
|
override fun onNewRawLocation(rawLocation: Location) {}
|
|
485
613
|
override fun onNewLocationMatcherResult(result: LocationMatcherResult) {
|
|
486
614
|
val loc = result.enhancedLocation
|
|
487
|
-
|
|
488
|
-
navigationLocationProvider.changePosition(
|
|
489
|
-
location = loc,
|
|
490
|
-
keyPoints = result.keyPoints,
|
|
491
|
-
)
|
|
492
|
-
|
|
615
|
+
navigationLocationProvider.changePosition(location = loc, keyPoints = result.keyPoints)
|
|
493
616
|
viewportDataSource.onLocationChanged(loc)
|
|
494
617
|
viewportDataSource.evaluate()
|
|
495
618
|
|
|
496
|
-
// First GPS fix → enter following mode
|
|
497
619
|
if (!firstLocationReceived) {
|
|
498
620
|
firstLocationReceived = true
|
|
499
621
|
safeCameraFollowing()
|
|
500
622
|
}
|
|
501
623
|
|
|
502
|
-
// Speed limit display
|
|
503
624
|
val fmtOptions = DistanceFormatterOptions.Builder(context).build()
|
|
504
625
|
val speedInfo = speedInfoApi.updatePostedAndCurrentSpeed(result, fmtOptions)
|
|
505
626
|
if (speedInfo != null) {
|
|
@@ -512,30 +633,28 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
512
633
|
checkAndSwitchDayNight()
|
|
513
634
|
}
|
|
514
635
|
})
|
|
636
|
+
|
|
637
|
+
// Voice instructions observer — triggers TTS announcements
|
|
638
|
+
nav.registerVoiceInstructionsObserver(voiceInstructionsObserver)
|
|
515
639
|
}
|
|
516
640
|
|
|
517
641
|
// ─────────────────────────────────────────────────────────────────────────
|
|
518
642
|
// Button actions
|
|
519
643
|
// ─────────────────────────────────────────────────────────────────────────
|
|
520
|
-
|
|
521
|
-
// Toggle mute voice guidance
|
|
522
644
|
private fun toggleMute() {
|
|
523
645
|
isMuted = !isMuted
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
//
|
|
527
|
-
|
|
528
|
-
onNavigationCancelled(mapOf("type" to "mute", "muted" to isMuted))
|
|
646
|
+
// Update icon to show muted/unmuted state
|
|
647
|
+
btnMuteView?.setImageBitmap(drawSpeakerIcon(isMuted))
|
|
648
|
+
// Apply volume via SpeechVolume API
|
|
649
|
+
voiceInstructionsPlayer.volume(SpeechVolume(if (isMuted) 0f else 1f))
|
|
529
650
|
}
|
|
530
651
|
|
|
531
|
-
// Toggle overview / following mode
|
|
532
652
|
private fun toggleOverview() {
|
|
533
653
|
if (isOverviewMode) {
|
|
534
654
|
recenterCamera()
|
|
535
655
|
} else {
|
|
536
656
|
isOverviewMode = true
|
|
537
|
-
|
|
538
|
-
btnRecenter?.visibility = View.VISIBLE
|
|
657
|
+
btnRecenterView?.visibility = View.VISIBLE
|
|
539
658
|
try {
|
|
540
659
|
navigationCamera.requestNavigationCameraToOverview()
|
|
541
660
|
} catch (e: Exception) {
|
|
@@ -544,11 +663,9 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
544
663
|
}
|
|
545
664
|
}
|
|
546
665
|
|
|
547
|
-
// Recenter camera to vehicle (following mode)
|
|
548
666
|
private fun recenterCamera() {
|
|
549
667
|
isOverviewMode = false
|
|
550
|
-
|
|
551
|
-
btnRecenter?.visibility = View.GONE
|
|
668
|
+
btnRecenterView?.visibility = View.GONE
|
|
552
669
|
safeCameraFollowing()
|
|
553
670
|
}
|
|
554
671
|
|
|
@@ -560,25 +677,15 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
560
677
|
totalTimeRemainingSec: Double,
|
|
561
678
|
distanceRemainingMetres: Double
|
|
562
679
|
) {
|
|
563
|
-
val arrivalCal = Calendar.getInstance().apply {
|
|
564
|
-
timeInMillis = estimatedTimeToArrivalMs
|
|
565
|
-
}
|
|
680
|
+
val arrivalCal = Calendar.getInstance().apply { timeInMillis = estimatedTimeToArrivalMs }
|
|
566
681
|
tvEtaTime?.text = String.format(
|
|
567
|
-
"%02d:%02d",
|
|
568
|
-
arrivalCal.get(Calendar.HOUR_OF_DAY),
|
|
569
|
-
arrivalCal.get(Calendar.MINUTE)
|
|
682
|
+
"%02d:%02d", arrivalCal.get(Calendar.HOUR_OF_DAY), arrivalCal.get(Calendar.MINUTE)
|
|
570
683
|
)
|
|
571
|
-
|
|
572
684
|
val totalMin = (totalTimeRemainingSec / 60).toInt()
|
|
573
|
-
tvDuration?.text = if (totalMin >= 60)
|
|
574
|
-
"${totalMin / 60}h ${totalMin % 60}min"
|
|
575
|
-
else
|
|
576
|
-
"${totalMin} min"
|
|
577
|
-
|
|
685
|
+
tvDuration?.text = if (totalMin >= 60) "${totalMin / 60}h ${totalMin % 60}min" else "$totalMin min"
|
|
578
686
|
tvDistance?.text = if (distanceRemainingMetres >= 1000)
|
|
579
687
|
String.format("%.1f km", distanceRemainingMetres / 1000.0)
|
|
580
|
-
else
|
|
581
|
-
"${distanceRemainingMetres.toInt()} m"
|
|
688
|
+
else "${distanceRemainingMetres.toInt()} m"
|
|
582
689
|
}
|
|
583
690
|
|
|
584
691
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -595,9 +702,9 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
595
702
|
val shouldBeNight = hour !in 6..20
|
|
596
703
|
if (shouldBeNight == isNightMode) return
|
|
597
704
|
isNightMode = shouldBeNight
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
705
|
+
mapView.mapboxMap.loadStyle(
|
|
706
|
+
if (shouldBeNight) NavigationStyles.NAVIGATION_NIGHT_STYLE else NavigationStyles.NAVIGATION_DAY_STYLE
|
|
707
|
+
) { style ->
|
|
601
708
|
routeLineView.initializeLayers(style)
|
|
602
709
|
mapboxNavigation?.getNavigationRoutes()?.takeIf { it.isNotEmpty() }?.let { routes ->
|
|
603
710
|
routeLineApi.setNavigationRoutes(routes) { value ->
|
|
@@ -607,25 +714,19 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
607
714
|
}
|
|
608
715
|
}
|
|
609
716
|
|
|
610
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
611
|
-
// Camera following — issue #43 safe wrapper
|
|
612
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
613
717
|
private fun safeCameraFollowing() {
|
|
614
718
|
try {
|
|
615
719
|
navigationCamera.requestNavigationCameraToFollowing(
|
|
616
|
-
stateTransitionOptions = NavigationCameraTransitionOptions.Builder()
|
|
617
|
-
.maxDuration(0)
|
|
618
|
-
.build()
|
|
720
|
+
stateTransitionOptions = NavigationCameraTransitionOptions.Builder().maxDuration(0).build()
|
|
619
721
|
)
|
|
620
722
|
} catch (e: Exception) {
|
|
621
723
|
Log.e(TAG, "Camera following error: ${e.message}")
|
|
622
724
|
}
|
|
623
725
|
}
|
|
624
726
|
|
|
625
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
626
|
-
// Cancel navigation
|
|
627
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
628
727
|
private fun cancelNavigation() {
|
|
728
|
+
speechApi.cancel()
|
|
729
|
+
voiceInstructionsPlayer.clear()
|
|
629
730
|
mapboxNavigation?.setNavigationRoutes(listOf())
|
|
630
731
|
mapboxNavigation?.stopTripSession()
|
|
631
732
|
firstLocationReceived = false
|
|
@@ -647,9 +748,6 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
647
748
|
sideButtons?.visibility = View.INVISIBLE
|
|
648
749
|
}
|
|
649
750
|
|
|
650
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
651
|
-
// Issue #31 — Voice units
|
|
652
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
653
751
|
private fun resolveVoiceUnits(): String {
|
|
654
752
|
return when (voiceUnits?.lowercase()) {
|
|
655
753
|
"metric" -> "metric"
|
|
@@ -661,88 +759,58 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
661
759
|
}
|
|
662
760
|
}
|
|
663
761
|
|
|
664
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
665
|
-
// Route request
|
|
666
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
667
762
|
@SuppressLint("MissingPermission")
|
|
668
763
|
private fun fetchRoutes() {
|
|
669
764
|
val nav = mapboxNavigation ?: return
|
|
670
765
|
if (coordinates.size < 2) return
|
|
671
|
-
|
|
672
|
-
val points = coordinates.map {
|
|
673
|
-
Point.fromLngLat(it["longitude"] ?: 0.0, it["latitude"] ?: 0.0)
|
|
674
|
-
}
|
|
766
|
+
val points = coordinates.map { Point.fromLngLat(it["longitude"] ?: 0.0, it["latitude"] ?: 0.0) }
|
|
675
767
|
val locale = language?.let { Locale.forLanguageTag(it) } ?: Locale.getDefault()
|
|
676
|
-
|
|
677
768
|
val builder = RouteOptions.builder()
|
|
678
769
|
.applyDefaultNavigationOptions()
|
|
679
770
|
.language(locale.toLanguageTag())
|
|
680
771
|
.voiceUnits(resolveVoiceUnits())
|
|
681
772
|
.coordinatesList(points)
|
|
682
773
|
.annotations("maxspeed,congestion,duration,speed")
|
|
683
|
-
|
|
684
774
|
waypointIndices?.let { builder.waypointIndicesList(it) }
|
|
685
|
-
navigationProfile?.let {
|
|
686
|
-
|
|
687
|
-
}
|
|
688
|
-
excludeTypes?.takeIf { it.isNotEmpty() }?.let {
|
|
689
|
-
builder.exclude(it.joinToString(","))
|
|
690
|
-
}
|
|
775
|
+
navigationProfile?.let { builder.profile(if (it.startsWith("mapbox/")) it else "mapbox/$it") }
|
|
776
|
+
excludeTypes?.takeIf { it.isNotEmpty() }?.let { builder.exclude(it.joinToString(",")) }
|
|
691
777
|
maxHeight?.let { builder.maxHeight(it) }
|
|
692
778
|
maxWidth?.let { builder.maxWidth(it) }
|
|
693
|
-
|
|
694
779
|
nav.requestRoutes(builder.build(), object : NavigationRouterCallback {
|
|
695
|
-
override fun onRoutesReady(
|
|
696
|
-
routes
|
|
697
|
-
@RouterOrigin routerOrigin: String
|
|
698
|
-
) {
|
|
699
|
-
if (routes.isEmpty()) {
|
|
700
|
-
onRoutesFailed(mapOf("message" to "No routes returned"))
|
|
701
|
-
return
|
|
702
|
-
}
|
|
780
|
+
override fun onRoutesReady(routes: List<NavigationRoute>, @RouterOrigin routerOrigin: String) {
|
|
781
|
+
if (routes.isEmpty()) { onRoutesFailed(mapOf("message" to "No routes returned")); return }
|
|
703
782
|
nav.setNavigationRoutes(routes)
|
|
704
783
|
nav.startTripSession()
|
|
705
784
|
}
|
|
706
|
-
|
|
707
785
|
override fun onFailure(reasons: List<RouterFailure>, routeOptions: RouteOptions) {
|
|
708
|
-
|
|
709
|
-
Log.e(TAG, "Route failed: $msg")
|
|
710
|
-
onRoutesFailed(mapOf("message" to msg))
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
override fun onCanceled(
|
|
714
|
-
routeOptions: RouteOptions,
|
|
715
|
-
@RouterOrigin routerOrigin: String
|
|
716
|
-
) {
|
|
717
|
-
Log.d(TAG, "Route cancelled")
|
|
786
|
+
onRoutesFailed(mapOf("message" to (reasons.firstOrNull()?.message ?: "Unknown error")))
|
|
718
787
|
}
|
|
788
|
+
override fun onCanceled(routeOptions: RouteOptions, @RouterOrigin routerOrigin: String) {}
|
|
719
789
|
})
|
|
720
790
|
}
|
|
721
791
|
|
|
722
|
-
|
|
723
|
-
fun setCoordinates(coords: List<Map<String, Double>>) {
|
|
724
|
-
coordinates = coords
|
|
725
|
-
if (coords.size >= 2) fetchRoutes()
|
|
726
|
-
}
|
|
792
|
+
fun setCoordinates(coords: List<Map<String, Double>>) { coordinates = coords; if (coords.size >= 2) fetchRoutes() }
|
|
727
793
|
fun setWaypointIndices(i: List<Int>?) { waypointIndices = i }
|
|
728
794
|
fun setLanguage(l: String?) { language = l }
|
|
729
795
|
fun setVoiceUnits(u: String?) { voiceUnits = u }
|
|
730
796
|
fun setNavigationProfile(p: String?) { navigationProfile = p }
|
|
731
797
|
fun setExcludeTypes(t: List<String>?) { excludeTypes = t }
|
|
732
798
|
fun setMapStyle(s: String?) { mapStyle = s }
|
|
733
|
-
fun setMute(m: Boolean) {
|
|
799
|
+
fun setMute(m: Boolean) { if (m != isMuted) toggleMute() }
|
|
734
800
|
fun setMaxHeight(h: Double?) { maxHeight = h }
|
|
735
801
|
fun setMaxWidth(w: Double?) { maxWidth = w }
|
|
736
802
|
fun setUseMapMatching(u: Boolean) { useMapMatching = u }
|
|
737
803
|
fun setCustomRasterTileUrl(u: String?) { customRasterTileUrl = u }
|
|
738
804
|
fun setCustomRasterAboveLayerId(l: String?) { customRasterAboveLayerId = l }
|
|
739
805
|
|
|
740
|
-
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
|
741
806
|
override fun onDetachedFromWindow() {
|
|
742
807
|
super.onDetachedFromWindow()
|
|
808
|
+
speechApi.cancel()
|
|
809
|
+
voiceInstructionsPlayer.shutdown()
|
|
743
810
|
maneuverApi.cancel()
|
|
744
811
|
routeLineApi.cancel()
|
|
745
812
|
routeLineView.cancel()
|
|
813
|
+
mapboxNavigation?.unregisterVoiceInstructionsObserver(voiceInstructionsObserver)
|
|
746
814
|
mapboxNavigation?.stopTripSession()
|
|
747
815
|
MapboxNavigationProvider.destroy()
|
|
748
816
|
mapView.onStop()
|
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.4",
|
|
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",
|