@jacques_gordon/expo-mapbox-navigation 2.1.3 → 2.1.5
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,44 @@ 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
|
+
// MapboxNavigationConsumer is in com.mapbox.navigation.ui.base.util (NOT base.route)
|
|
452
|
+
// voiceInstructionsPlayerCallback must be defined BEFORE speechCallback (used inside it)
|
|
453
|
+
private val voiceInstructionsPlayerCallback =
|
|
454
|
+
com.mapbox.navigation.ui.base.util.MapboxNavigationConsumer<SpeechAnnouncement> { value ->
|
|
455
|
+
speechApi.clean(value)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
private val speechCallback =
|
|
459
|
+
com.mapbox.navigation.ui.base.util.MapboxNavigationConsumer<
|
|
460
|
+
com.mapbox.bindgen.Expected<SpeechError, SpeechValue>
|
|
461
|
+
> { expected ->
|
|
462
|
+
expected.fold(
|
|
463
|
+
{ error ->
|
|
464
|
+
if (!isMuted) {
|
|
465
|
+
voiceInstructionsPlayer.play(error.fallback, voiceInstructionsPlayerCallback)
|
|
466
|
+
}
|
|
467
|
+
},
|
|
468
|
+
{ value ->
|
|
469
|
+
if (!isMuted) {
|
|
470
|
+
voiceInstructionsPlayer.play(value.announcement, voiceInstructionsPlayerCallback)
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
private val voiceInstructionsObserver = VoiceInstructionsObserver { voiceInstructions ->
|
|
477
|
+
speechApi.generate(voiceInstructions, speechCallback)
|
|
353
478
|
}
|
|
354
479
|
|
|
355
480
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -363,7 +488,6 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
363
488
|
mapView.mapboxMap.loadStyle(mapStyle ?: getAutoStyle()) { style ->
|
|
364
489
|
routeLineView.initializeLayers(style)
|
|
365
490
|
|
|
366
|
-
// Camera viewport
|
|
367
491
|
viewportDataSource = MapboxNavigationViewportDataSource(mapView.mapboxMap)
|
|
368
492
|
viewportDataSource.followingPadding = followingPadding
|
|
369
493
|
viewportDataSource.overviewPadding = overviewPadding
|
|
@@ -374,24 +498,19 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
374
498
|
viewportDataSource
|
|
375
499
|
)
|
|
376
500
|
|
|
377
|
-
//
|
|
378
|
-
// FIX: use mapbox_navigation_puck_icon (built-in arrow)
|
|
379
|
-
// + PuckBearing.COURSE to orient it in direction of travel
|
|
501
|
+
// Navigation arrow puck
|
|
380
502
|
mapView.location.apply {
|
|
381
503
|
setLocationProvider(navigationLocationProvider)
|
|
382
504
|
updateSettings {
|
|
383
|
-
// Built-in navigation arrow (chevron) from Nav SDK
|
|
384
505
|
locationPuck = LocationPuck2D(
|
|
385
506
|
bearingImage = ImageHolder.from(
|
|
386
507
|
com.mapbox.navigation.R.drawable.mapbox_navigation_puck_icon
|
|
387
508
|
)
|
|
388
509
|
)
|
|
389
|
-
// COURSE = direction of movement (not compass heading)
|
|
390
510
|
puckBearingEnabled = true
|
|
391
511
|
enabled = true
|
|
392
512
|
}
|
|
393
|
-
|
|
394
|
-
puckBearing = com.mapbox.maps.plugin.PuckBearing.COURSE
|
|
513
|
+
puckBearing = PuckBearing.COURSE
|
|
395
514
|
}
|
|
396
515
|
|
|
397
516
|
registerObservers()
|
|
@@ -408,10 +527,12 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
408
527
|
nav.registerRoutesObserver(object : RoutesObserver {
|
|
409
528
|
override fun onRoutesChanged(result: RoutesUpdatedResult) {
|
|
410
529
|
if (result.navigationRoutes.isNotEmpty()) {
|
|
530
|
+
// Cancel any in-flight speech when route changes
|
|
531
|
+
speechApi.cancel()
|
|
532
|
+
voiceInstructionsPlayer.clear()
|
|
533
|
+
|
|
411
534
|
routeLineApi.setNavigationRoutes(result.navigationRoutes) { value ->
|
|
412
|
-
mapView.mapboxMap.style?.apply {
|
|
413
|
-
routeLineView.renderRouteDrawData(this, value)
|
|
414
|
-
}
|
|
535
|
+
mapView.mapboxMap.style?.apply { routeLineView.renderRouteDrawData(this, value) }
|
|
415
536
|
}
|
|
416
537
|
viewportDataSource.onRouteChanged(result.navigationRoutes.first())
|
|
417
538
|
viewportDataSource.evaluate()
|
|
@@ -442,13 +563,11 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
442
563
|
viewportDataSource.onRouteProgressChanged(routeProgress)
|
|
443
564
|
viewportDataSource.evaluate()
|
|
444
565
|
|
|
445
|
-
// Maneuver arrow on map
|
|
446
566
|
mapView.mapboxMap.style?.let { style ->
|
|
447
567
|
val arrowResult = routeArrowApi.addUpcomingManeuverArrow(routeProgress)
|
|
448
568
|
routeArrowView.renderManeuverUpdate(style, arrowResult)
|
|
449
569
|
}
|
|
450
570
|
|
|
451
|
-
// Maneuver banner
|
|
452
571
|
val maneuvers = maneuverApi.getManeuvers(routeProgress)
|
|
453
572
|
maneuvers.fold(
|
|
454
573
|
{ error -> Log.w(TAG, "Maneuver error: ${error.errorMessage}"); Unit },
|
|
@@ -459,7 +578,6 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
459
578
|
}
|
|
460
579
|
)
|
|
461
580
|
|
|
462
|
-
// Trip progress
|
|
463
581
|
val tripProgress = tripProgressApi.getTripProgress(routeProgress)
|
|
464
582
|
tripProgressView?.render(tripProgress)
|
|
465
583
|
updateEtaBar(
|
|
@@ -474,8 +592,7 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
474
592
|
"distanceTraveled" to routeProgress.distanceTraveled,
|
|
475
593
|
"fractionTraveled" to routeProgress.fractionTraveled,
|
|
476
594
|
"currentStepDistanceRemaining" to
|
|
477
|
-
(routeProgress.currentLegProgress
|
|
478
|
-
?.currentStepProgress?.distanceRemaining ?: 0f)
|
|
595
|
+
(routeProgress.currentLegProgress?.currentStepProgress?.distanceRemaining ?: 0f)
|
|
479
596
|
))
|
|
480
597
|
})
|
|
481
598
|
|
|
@@ -484,22 +601,15 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
484
601
|
override fun onNewRawLocation(rawLocation: Location) {}
|
|
485
602
|
override fun onNewLocationMatcherResult(result: LocationMatcherResult) {
|
|
486
603
|
val loc = result.enhancedLocation
|
|
487
|
-
|
|
488
|
-
navigationLocationProvider.changePosition(
|
|
489
|
-
location = loc,
|
|
490
|
-
keyPoints = result.keyPoints,
|
|
491
|
-
)
|
|
492
|
-
|
|
604
|
+
navigationLocationProvider.changePosition(location = loc, keyPoints = result.keyPoints)
|
|
493
605
|
viewportDataSource.onLocationChanged(loc)
|
|
494
606
|
viewportDataSource.evaluate()
|
|
495
607
|
|
|
496
|
-
// First GPS fix → enter following mode
|
|
497
608
|
if (!firstLocationReceived) {
|
|
498
609
|
firstLocationReceived = true
|
|
499
610
|
safeCameraFollowing()
|
|
500
611
|
}
|
|
501
612
|
|
|
502
|
-
// Speed limit display
|
|
503
613
|
val fmtOptions = DistanceFormatterOptions.Builder(context).build()
|
|
504
614
|
val speedInfo = speedInfoApi.updatePostedAndCurrentSpeed(result, fmtOptions)
|
|
505
615
|
if (speedInfo != null) {
|
|
@@ -512,30 +622,28 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
512
622
|
checkAndSwitchDayNight()
|
|
513
623
|
}
|
|
514
624
|
})
|
|
625
|
+
|
|
626
|
+
// Voice instructions observer — triggers TTS announcements
|
|
627
|
+
nav.registerVoiceInstructionsObserver(voiceInstructionsObserver)
|
|
515
628
|
}
|
|
516
629
|
|
|
517
630
|
// ─────────────────────────────────────────────────────────────────────────
|
|
518
631
|
// Button actions
|
|
519
632
|
// ─────────────────────────────────────────────────────────────────────────
|
|
520
|
-
|
|
521
|
-
// Toggle mute voice guidance
|
|
522
633
|
private fun toggleMute() {
|
|
523
634
|
isMuted = !isMuted
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
//
|
|
527
|
-
|
|
528
|
-
onNavigationCancelled(mapOf("type" to "mute", "muted" to isMuted))
|
|
635
|
+
// Update icon to show muted/unmuted state
|
|
636
|
+
btnMuteView?.setImageBitmap(drawSpeakerIcon(isMuted))
|
|
637
|
+
// Apply volume via SpeechVolume API
|
|
638
|
+
voiceInstructionsPlayer.volume(SpeechVolume(if (isMuted) 0f else 1f))
|
|
529
639
|
}
|
|
530
640
|
|
|
531
|
-
// Toggle overview / following mode
|
|
532
641
|
private fun toggleOverview() {
|
|
533
642
|
if (isOverviewMode) {
|
|
534
643
|
recenterCamera()
|
|
535
644
|
} else {
|
|
536
645
|
isOverviewMode = true
|
|
537
|
-
|
|
538
|
-
btnRecenter?.visibility = View.VISIBLE
|
|
646
|
+
btnRecenterView?.visibility = View.VISIBLE
|
|
539
647
|
try {
|
|
540
648
|
navigationCamera.requestNavigationCameraToOverview()
|
|
541
649
|
} catch (e: Exception) {
|
|
@@ -544,11 +652,9 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
544
652
|
}
|
|
545
653
|
}
|
|
546
654
|
|
|
547
|
-
// Recenter camera to vehicle (following mode)
|
|
548
655
|
private fun recenterCamera() {
|
|
549
656
|
isOverviewMode = false
|
|
550
|
-
|
|
551
|
-
btnRecenter?.visibility = View.GONE
|
|
657
|
+
btnRecenterView?.visibility = View.GONE
|
|
552
658
|
safeCameraFollowing()
|
|
553
659
|
}
|
|
554
660
|
|
|
@@ -560,25 +666,15 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
560
666
|
totalTimeRemainingSec: Double,
|
|
561
667
|
distanceRemainingMetres: Double
|
|
562
668
|
) {
|
|
563
|
-
val arrivalCal = Calendar.getInstance().apply {
|
|
564
|
-
timeInMillis = estimatedTimeToArrivalMs
|
|
565
|
-
}
|
|
669
|
+
val arrivalCal = Calendar.getInstance().apply { timeInMillis = estimatedTimeToArrivalMs }
|
|
566
670
|
tvEtaTime?.text = String.format(
|
|
567
|
-
"%02d:%02d",
|
|
568
|
-
arrivalCal.get(Calendar.HOUR_OF_DAY),
|
|
569
|
-
arrivalCal.get(Calendar.MINUTE)
|
|
671
|
+
"%02d:%02d", arrivalCal.get(Calendar.HOUR_OF_DAY), arrivalCal.get(Calendar.MINUTE)
|
|
570
672
|
)
|
|
571
|
-
|
|
572
673
|
val totalMin = (totalTimeRemainingSec / 60).toInt()
|
|
573
|
-
tvDuration?.text = if (totalMin >= 60)
|
|
574
|
-
"${totalMin / 60}h ${totalMin % 60}min"
|
|
575
|
-
else
|
|
576
|
-
"${totalMin} min"
|
|
577
|
-
|
|
674
|
+
tvDuration?.text = if (totalMin >= 60) "${totalMin / 60}h ${totalMin % 60}min" else "$totalMin min"
|
|
578
675
|
tvDistance?.text = if (distanceRemainingMetres >= 1000)
|
|
579
676
|
String.format("%.1f km", distanceRemainingMetres / 1000.0)
|
|
580
|
-
else
|
|
581
|
-
"${distanceRemainingMetres.toInt()} m"
|
|
677
|
+
else "${distanceRemainingMetres.toInt()} m"
|
|
582
678
|
}
|
|
583
679
|
|
|
584
680
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -595,9 +691,9 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
595
691
|
val shouldBeNight = hour !in 6..20
|
|
596
692
|
if (shouldBeNight == isNightMode) return
|
|
597
693
|
isNightMode = shouldBeNight
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
694
|
+
mapView.mapboxMap.loadStyle(
|
|
695
|
+
if (shouldBeNight) NavigationStyles.NAVIGATION_NIGHT_STYLE else NavigationStyles.NAVIGATION_DAY_STYLE
|
|
696
|
+
) { style ->
|
|
601
697
|
routeLineView.initializeLayers(style)
|
|
602
698
|
mapboxNavigation?.getNavigationRoutes()?.takeIf { it.isNotEmpty() }?.let { routes ->
|
|
603
699
|
routeLineApi.setNavigationRoutes(routes) { value ->
|
|
@@ -607,25 +703,19 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
607
703
|
}
|
|
608
704
|
}
|
|
609
705
|
|
|
610
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
611
|
-
// Camera following — issue #43 safe wrapper
|
|
612
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
613
706
|
private fun safeCameraFollowing() {
|
|
614
707
|
try {
|
|
615
708
|
navigationCamera.requestNavigationCameraToFollowing(
|
|
616
|
-
stateTransitionOptions = NavigationCameraTransitionOptions.Builder()
|
|
617
|
-
.maxDuration(0)
|
|
618
|
-
.build()
|
|
709
|
+
stateTransitionOptions = NavigationCameraTransitionOptions.Builder().maxDuration(0).build()
|
|
619
710
|
)
|
|
620
711
|
} catch (e: Exception) {
|
|
621
712
|
Log.e(TAG, "Camera following error: ${e.message}")
|
|
622
713
|
}
|
|
623
714
|
}
|
|
624
715
|
|
|
625
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
626
|
-
// Cancel navigation
|
|
627
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
628
716
|
private fun cancelNavigation() {
|
|
717
|
+
speechApi.cancel()
|
|
718
|
+
voiceInstructionsPlayer.clear()
|
|
629
719
|
mapboxNavigation?.setNavigationRoutes(listOf())
|
|
630
720
|
mapboxNavigation?.stopTripSession()
|
|
631
721
|
firstLocationReceived = false
|
|
@@ -647,9 +737,6 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
647
737
|
sideButtons?.visibility = View.INVISIBLE
|
|
648
738
|
}
|
|
649
739
|
|
|
650
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
651
|
-
// Issue #31 — Voice units
|
|
652
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
653
740
|
private fun resolveVoiceUnits(): String {
|
|
654
741
|
return when (voiceUnits?.lowercase()) {
|
|
655
742
|
"metric" -> "metric"
|
|
@@ -661,88 +748,58 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
661
748
|
}
|
|
662
749
|
}
|
|
663
750
|
|
|
664
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
665
|
-
// Route request
|
|
666
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
667
751
|
@SuppressLint("MissingPermission")
|
|
668
752
|
private fun fetchRoutes() {
|
|
669
753
|
val nav = mapboxNavigation ?: return
|
|
670
754
|
if (coordinates.size < 2) return
|
|
671
|
-
|
|
672
|
-
val points = coordinates.map {
|
|
673
|
-
Point.fromLngLat(it["longitude"] ?: 0.0, it["latitude"] ?: 0.0)
|
|
674
|
-
}
|
|
755
|
+
val points = coordinates.map { Point.fromLngLat(it["longitude"] ?: 0.0, it["latitude"] ?: 0.0) }
|
|
675
756
|
val locale = language?.let { Locale.forLanguageTag(it) } ?: Locale.getDefault()
|
|
676
|
-
|
|
677
757
|
val builder = RouteOptions.builder()
|
|
678
758
|
.applyDefaultNavigationOptions()
|
|
679
759
|
.language(locale.toLanguageTag())
|
|
680
760
|
.voiceUnits(resolveVoiceUnits())
|
|
681
761
|
.coordinatesList(points)
|
|
682
762
|
.annotations("maxspeed,congestion,duration,speed")
|
|
683
|
-
|
|
684
763
|
waypointIndices?.let { builder.waypointIndicesList(it) }
|
|
685
|
-
navigationProfile?.let {
|
|
686
|
-
|
|
687
|
-
}
|
|
688
|
-
excludeTypes?.takeIf { it.isNotEmpty() }?.let {
|
|
689
|
-
builder.exclude(it.joinToString(","))
|
|
690
|
-
}
|
|
764
|
+
navigationProfile?.let { builder.profile(if (it.startsWith("mapbox/")) it else "mapbox/$it") }
|
|
765
|
+
excludeTypes?.takeIf { it.isNotEmpty() }?.let { builder.exclude(it.joinToString(",")) }
|
|
691
766
|
maxHeight?.let { builder.maxHeight(it) }
|
|
692
767
|
maxWidth?.let { builder.maxWidth(it) }
|
|
693
|
-
|
|
694
768
|
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
|
-
}
|
|
769
|
+
override fun onRoutesReady(routes: List<NavigationRoute>, @RouterOrigin routerOrigin: String) {
|
|
770
|
+
if (routes.isEmpty()) { onRoutesFailed(mapOf("message" to "No routes returned")); return }
|
|
703
771
|
nav.setNavigationRoutes(routes)
|
|
704
772
|
nav.startTripSession()
|
|
705
773
|
}
|
|
706
|
-
|
|
707
774
|
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")
|
|
775
|
+
onRoutesFailed(mapOf("message" to (reasons.firstOrNull()?.message ?: "Unknown error")))
|
|
718
776
|
}
|
|
777
|
+
override fun onCanceled(routeOptions: RouteOptions, @RouterOrigin routerOrigin: String) {}
|
|
719
778
|
})
|
|
720
779
|
}
|
|
721
780
|
|
|
722
|
-
|
|
723
|
-
fun setCoordinates(coords: List<Map<String, Double>>) {
|
|
724
|
-
coordinates = coords
|
|
725
|
-
if (coords.size >= 2) fetchRoutes()
|
|
726
|
-
}
|
|
781
|
+
fun setCoordinates(coords: List<Map<String, Double>>) { coordinates = coords; if (coords.size >= 2) fetchRoutes() }
|
|
727
782
|
fun setWaypointIndices(i: List<Int>?) { waypointIndices = i }
|
|
728
783
|
fun setLanguage(l: String?) { language = l }
|
|
729
784
|
fun setVoiceUnits(u: String?) { voiceUnits = u }
|
|
730
785
|
fun setNavigationProfile(p: String?) { navigationProfile = p }
|
|
731
786
|
fun setExcludeTypes(t: List<String>?) { excludeTypes = t }
|
|
732
787
|
fun setMapStyle(s: String?) { mapStyle = s }
|
|
733
|
-
fun setMute(m: Boolean) {
|
|
788
|
+
fun setMute(m: Boolean) { if (m != isMuted) toggleMute() }
|
|
734
789
|
fun setMaxHeight(h: Double?) { maxHeight = h }
|
|
735
790
|
fun setMaxWidth(w: Double?) { maxWidth = w }
|
|
736
791
|
fun setUseMapMatching(u: Boolean) { useMapMatching = u }
|
|
737
792
|
fun setCustomRasterTileUrl(u: String?) { customRasterTileUrl = u }
|
|
738
793
|
fun setCustomRasterAboveLayerId(l: String?) { customRasterAboveLayerId = l }
|
|
739
794
|
|
|
740
|
-
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
|
741
795
|
override fun onDetachedFromWindow() {
|
|
742
796
|
super.onDetachedFromWindow()
|
|
797
|
+
speechApi.cancel()
|
|
798
|
+
voiceInstructionsPlayer.shutdown()
|
|
743
799
|
maneuverApi.cancel()
|
|
744
800
|
routeLineApi.cancel()
|
|
745
801
|
routeLineView.cancel()
|
|
802
|
+
mapboxNavigation?.unregisterVoiceInstructionsObserver(voiceInstructionsObserver)
|
|
746
803
|
mapboxNavigation?.stopTripSession()
|
|
747
804
|
MapboxNavigationProvider.destroy()
|
|
748
805
|
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.5",
|
|
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",
|