@jacques_gordon/expo-mapbox-navigation 2.1.2 → 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,18 +3,31 @@ 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
|
|
15
|
+
import android.graphics.drawable.GradientDrawable
|
|
7
16
|
import android.util.Log
|
|
8
17
|
import android.view.Gravity
|
|
9
18
|
import android.view.View
|
|
10
19
|
import android.widget.FrameLayout
|
|
20
|
+
import android.widget.ImageView
|
|
11
21
|
import android.widget.LinearLayout
|
|
12
22
|
import android.widget.TextView
|
|
13
23
|
import com.mapbox.api.directions.v5.models.RouteOptions
|
|
14
24
|
import com.mapbox.common.location.Location
|
|
15
25
|
import com.mapbox.geojson.Point
|
|
16
26
|
import com.mapbox.maps.EdgeInsets
|
|
27
|
+
import com.mapbox.maps.ImageHolder
|
|
17
28
|
import com.mapbox.maps.MapView
|
|
29
|
+
import com.mapbox.maps.plugin.LocationPuck2D
|
|
30
|
+
import com.mapbox.maps.plugin.PuckBearing
|
|
18
31
|
import com.mapbox.maps.plugin.animation.camera
|
|
19
32
|
import com.mapbox.maps.plugin.locationcomponent.location
|
|
20
33
|
import com.mapbox.navigation.base.TimeFormat
|
|
@@ -33,6 +46,7 @@ import com.mapbox.navigation.core.formatter.MapboxDistanceFormatter
|
|
|
33
46
|
import com.mapbox.navigation.core.trip.session.LocationMatcherResult
|
|
34
47
|
import com.mapbox.navigation.core.trip.session.LocationObserver
|
|
35
48
|
import com.mapbox.navigation.core.trip.session.RouteProgressObserver
|
|
49
|
+
import com.mapbox.navigation.core.trip.session.VoiceInstructionsObserver
|
|
36
50
|
import com.mapbox.navigation.tripdata.maneuver.api.MapboxManeuverApi
|
|
37
51
|
import com.mapbox.navigation.tripdata.progress.api.MapboxTripProgressApi
|
|
38
52
|
import com.mapbox.navigation.tripdata.progress.model.DistanceRemainingFormatter
|
|
@@ -55,6 +69,12 @@ import com.mapbox.navigation.ui.maps.route.line.api.MapboxRouteLineApi
|
|
|
55
69
|
import com.mapbox.navigation.ui.maps.route.line.api.MapboxRouteLineView
|
|
56
70
|
import com.mapbox.navigation.ui.maps.route.line.model.MapboxRouteLineApiOptions
|
|
57
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
|
|
58
78
|
import expo.modules.kotlin.AppContext
|
|
59
79
|
import expo.modules.kotlin.viewevent.EventDispatcher
|
|
60
80
|
import expo.modules.kotlin.views.ExpoView
|
|
@@ -83,6 +103,10 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
83
103
|
private var tvDuration: TextView? = null
|
|
84
104
|
private var tvDistance: TextView? = null
|
|
85
105
|
private var etaBar: LinearLayout? = null
|
|
106
|
+
private var btnMuteView: ImageView? = null
|
|
107
|
+
private var btnOverviewView: ImageView? = null
|
|
108
|
+
private var btnRecenterView: ImageView? = null
|
|
109
|
+
private var sideButtons: LinearLayout? = null
|
|
86
110
|
|
|
87
111
|
// ── Navigation APIs ───────────────────────────────────────────────────────
|
|
88
112
|
private lateinit var navigationCamera: NavigationCamera
|
|
@@ -96,29 +120,26 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
96
120
|
private lateinit var routeArrowView: MapboxRouteArrowView
|
|
97
121
|
private val navigationLocationProvider = NavigationLocationProvider()
|
|
98
122
|
private var mapboxNavigation: MapboxNavigation? = null
|
|
99
|
-
private var isNightMode = false
|
|
100
123
|
|
|
101
|
-
// ──
|
|
102
|
-
|
|
124
|
+
// ── Voice APIs ────────────────────────────────────────────────────────────
|
|
125
|
+
private lateinit var speechApi: MapboxSpeechApi
|
|
126
|
+
private lateinit var voiceInstructionsPlayer: MapboxVoiceInstructionsPlayer
|
|
127
|
+
|
|
128
|
+
// ── State ─────────────────────────────────────────────────────────────────
|
|
129
|
+
private var isNightMode = false
|
|
130
|
+
private var isMuted = false
|
|
131
|
+
private var isOverviewMode = false
|
|
103
132
|
private var firstLocationReceived = false
|
|
104
133
|
|
|
105
|
-
// ──
|
|
106
|
-
private val
|
|
134
|
+
// ── Pixel density ─────────────────────────────────────────────────────────
|
|
135
|
+
private val dp = context.resources.displayMetrics.density
|
|
136
|
+
|
|
137
|
+
// ── Viewport padding ──────────────────────────────────────────────────────
|
|
107
138
|
private val followingPadding by lazy {
|
|
108
|
-
EdgeInsets(
|
|
109
|
-
180.0 * pixelDensity, // top — room for maneuver banner
|
|
110
|
-
40.0 * pixelDensity,
|
|
111
|
-
150.0 * pixelDensity, // bottom — room for ETA bar
|
|
112
|
-
40.0 * pixelDensity
|
|
113
|
-
)
|
|
139
|
+
EdgeInsets(180.0 * dp, 40.0 * dp, 300.0 * dp, 40.0 * dp)
|
|
114
140
|
}
|
|
115
141
|
private val overviewPadding by lazy {
|
|
116
|
-
EdgeInsets(
|
|
117
|
-
140.0 * pixelDensity,
|
|
118
|
-
40.0 * pixelDensity,
|
|
119
|
-
120.0 * pixelDensity,
|
|
120
|
-
40.0 * pixelDensity
|
|
121
|
-
)
|
|
142
|
+
EdgeInsets(120.0 * dp, 40.0 * dp, 120.0 * dp, 40.0 * dp)
|
|
122
143
|
}
|
|
123
144
|
|
|
124
145
|
// ── Props ─────────────────────────────────────────────────────────────────
|
|
@@ -142,23 +163,138 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
142
163
|
setupNavigation()
|
|
143
164
|
}
|
|
144
165
|
|
|
166
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
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
|
+
|
|
145
283
|
// ─────────────────────────────────────────────────────────────────────────
|
|
146
284
|
// Build UI
|
|
147
285
|
// ─────────────────────────────────────────────────────────────────────────
|
|
148
286
|
private fun buildUI() {
|
|
149
|
-
val dp = context.resources.displayMetrics.density
|
|
150
287
|
val root = FrameLayout(context).apply {
|
|
151
288
|
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
|
152
289
|
}
|
|
153
290
|
|
|
154
|
-
// Full
|
|
291
|
+
// Full-screen map
|
|
155
292
|
root.addView(mapView, FrameLayout.LayoutParams(
|
|
156
293
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
157
294
|
FrameLayout.LayoutParams.MATCH_PARENT
|
|
158
295
|
))
|
|
159
296
|
|
|
160
|
-
// ── ManeuverView — top
|
|
161
|
-
// extends ConstraintLayout → must cast to View for addView()
|
|
297
|
+
// ── ManeuverView — top ─────────────────────────────────────────────────
|
|
162
298
|
val mv = MapboxManeuverView(context)
|
|
163
299
|
root.addView(mv as View, FrameLayout.LayoutParams(
|
|
164
300
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
@@ -167,16 +303,53 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
167
303
|
mv.visibility = View.INVISIBLE
|
|
168
304
|
maneuverView = mv
|
|
169
305
|
|
|
170
|
-
// ──
|
|
171
|
-
//
|
|
172
|
-
val
|
|
173
|
-
|
|
306
|
+
// ── Side buttons — RIGHT side, just below maneuver banner ─────────────
|
|
307
|
+
// Position: top margin = maneuver banner height (~180dp) + 8dp gap
|
|
308
|
+
val sideCol = LinearLayout(context).apply {
|
|
309
|
+
orientation = LinearLayout.VERTICAL
|
|
310
|
+
gravity = Gravity.CENTER_HORIZONTAL
|
|
311
|
+
visibility = View.INVISIBLE
|
|
312
|
+
}
|
|
313
|
+
root.addView(sideCol, FrameLayout.LayoutParams(
|
|
174
314
|
FrameLayout.LayoutParams.WRAP_CONTENT,
|
|
175
315
|
FrameLayout.LayoutParams.WRAP_CONTENT
|
|
316
|
+
).also {
|
|
317
|
+
it.gravity = Gravity.TOP or Gravity.END
|
|
318
|
+
it.setMargins(0, (188 * dp).toInt(), (12 * dp).toInt(), 0)
|
|
319
|
+
})
|
|
320
|
+
sideButtons = sideCol
|
|
321
|
+
|
|
322
|
+
val btnSize = (56 * dp).toInt()
|
|
323
|
+
val btnParams = LinearLayout.LayoutParams(btnSize, btnSize).also {
|
|
324
|
+
it.bottomMargin = (10 * dp).toInt()
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Button 1: Mute/Unmute (speaker icon)
|
|
328
|
+
val muteBtn = makeIconButton(drawSpeakerIcon(false)) { toggleMute() }
|
|
329
|
+
sideCol.addView(muteBtn, btnParams)
|
|
330
|
+
btnMuteView = muteBtn
|
|
331
|
+
|
|
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
|
|
338
|
+
|
|
339
|
+
// Button 3: Recenter / Navigation arrow (hidden until in overview mode)
|
|
340
|
+
val recenterBtn = makeIconButton(drawNavigationArrowIcon()) { recenterCamera() }
|
|
341
|
+
recenterBtn.visibility = View.GONE
|
|
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()
|
|
176
350
|
).also {
|
|
177
351
|
it.gravity = Gravity.BOTTOM or Gravity.START
|
|
178
|
-
|
|
179
|
-
it.setMargins((16 * dp).toInt(), 0, 0, (96 * dp).toInt())
|
|
352
|
+
it.setMargins((12 * dp).toInt(), 0, 0, (88 * dp).toInt())
|
|
180
353
|
})
|
|
181
354
|
siv.visibility = View.GONE
|
|
182
355
|
speedInfoView = siv
|
|
@@ -184,7 +357,7 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
184
357
|
// ── ETA bottom bar ─────────────────────────────────────────────────────
|
|
185
358
|
val bar = LinearLayout(context).apply {
|
|
186
359
|
orientation = LinearLayout.HORIZONTAL
|
|
187
|
-
setBackgroundColor(Color.parseColor("#1E2433"))
|
|
360
|
+
setBackgroundColor(Color.parseColor("#1E2433"))
|
|
188
361
|
elevation = 8 * dp
|
|
189
362
|
visibility = View.INVISIBLE
|
|
190
363
|
gravity = Gravity.CENTER_VERTICAL
|
|
@@ -198,58 +371,38 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
198
371
|
FrameLayout.LayoutParams.WRAP_CONTENT
|
|
199
372
|
).also { it.gravity = Gravity.BOTTOM })
|
|
200
373
|
|
|
201
|
-
// ETA arrival time
|
|
202
374
|
val etaTime = TextView(context).apply {
|
|
203
|
-
textSize = 24f
|
|
204
|
-
setTextColor(Color.WHITE)
|
|
375
|
+
textSize = 24f; setTextColor(Color.WHITE)
|
|
205
376
|
typeface = android.graphics.Typeface.DEFAULT_BOLD
|
|
206
377
|
}
|
|
207
|
-
bar.addView(etaTime, LinearLayout.LayoutParams(
|
|
208
|
-
0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f
|
|
209
|
-
))
|
|
378
|
+
bar.addView(etaTime, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f))
|
|
210
379
|
tvEtaTime = etaTime
|
|
211
380
|
|
|
212
|
-
// Duration + distance (center)
|
|
213
381
|
val center = LinearLayout(context).apply {
|
|
214
|
-
orientation = LinearLayout.VERTICAL
|
|
215
|
-
gravity = Gravity.CENTER
|
|
382
|
+
orientation = LinearLayout.VERTICAL; gravity = Gravity.CENTER
|
|
216
383
|
}
|
|
217
384
|
val dur = TextView(context).apply {
|
|
218
|
-
textSize = 18f
|
|
219
|
-
|
|
220
|
-
typeface = android.graphics.Typeface.DEFAULT_BOLD
|
|
221
|
-
gravity = Gravity.CENTER
|
|
385
|
+
textSize = 18f; setTextColor(Color.WHITE)
|
|
386
|
+
typeface = android.graphics.Typeface.DEFAULT_BOLD; gravity = Gravity.CENTER
|
|
222
387
|
}
|
|
223
388
|
val dist = TextView(context).apply {
|
|
224
|
-
textSize = 13f
|
|
225
|
-
setTextColor(Color.parseColor("#AAAAAA"))
|
|
226
|
-
gravity = Gravity.CENTER
|
|
389
|
+
textSize = 13f; setTextColor(Color.parseColor("#AAAAAA")); gravity = Gravity.CENTER
|
|
227
390
|
}
|
|
228
|
-
center.addView(dur)
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
0, LinearLayout.LayoutParams.WRAP_CONTENT, 2f
|
|
232
|
-
))
|
|
233
|
-
tvDuration = dur
|
|
234
|
-
tvDistance = dist
|
|
391
|
+
center.addView(dur); center.addView(dist)
|
|
392
|
+
bar.addView(center, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 2f))
|
|
393
|
+
tvDuration = dur; tvDistance = dist
|
|
235
394
|
|
|
236
|
-
// Cancel button
|
|
237
395
|
val cancelBtn = TextView(context).apply {
|
|
238
|
-
text = "✕"
|
|
239
|
-
textSize = 22f
|
|
240
|
-
setTextColor(Color.parseColor("#AAAAAA"))
|
|
396
|
+
text = "✕"; textSize = 22f; setTextColor(Color.parseColor("#AAAAAA"))
|
|
241
397
|
gravity = Gravity.END or Gravity.CENTER_VERTICAL
|
|
242
398
|
setOnClickListener { cancelNavigation() }
|
|
243
399
|
}
|
|
244
|
-
bar.addView(cancelBtn, LinearLayout.LayoutParams(
|
|
245
|
-
0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f
|
|
246
|
-
))
|
|
400
|
+
bar.addView(cancelBtn, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f))
|
|
247
401
|
etaBar = bar
|
|
248
402
|
|
|
249
|
-
//
|
|
250
|
-
// FIX: was created but never added to root — now added but invisible
|
|
403
|
+
// TripProgressView — 1×1 hidden (required for render() API)
|
|
251
404
|
val tpv = MapboxTripProgressView(context)
|
|
252
|
-
root.addView(tpv as View, FrameLayout.LayoutParams(1, 1))
|
|
405
|
+
root.addView(tpv as View, FrameLayout.LayoutParams(1, 1))
|
|
253
406
|
tpv.visibility = View.GONE
|
|
254
407
|
tripProgressView = tpv
|
|
255
408
|
|
|
@@ -284,6 +437,55 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
284
437
|
)
|
|
285
438
|
|
|
286
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)
|
|
287
489
|
}
|
|
288
490
|
|
|
289
491
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -297,11 +499,7 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
297
499
|
mapView.mapboxMap.loadStyle(mapStyle ?: getAutoStyle()) { style ->
|
|
298
500
|
routeLineView.initializeLayers(style)
|
|
299
501
|
|
|
300
|
-
// Camera viewport data source
|
|
301
502
|
viewportDataSource = MapboxNavigationViewportDataSource(mapView.mapboxMap)
|
|
302
|
-
|
|
303
|
-
// FIX: Set padding BEFORE creating NavigationCamera
|
|
304
|
-
// Without this, the camera doesn't account for the maneuver banner and ETA bar
|
|
305
503
|
viewportDataSource.followingPadding = followingPadding
|
|
306
504
|
viewportDataSource.overviewPadding = overviewPadding
|
|
307
505
|
|
|
@@ -311,10 +509,19 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
311
509
|
viewportDataSource
|
|
312
510
|
)
|
|
313
511
|
|
|
314
|
-
//
|
|
512
|
+
// Navigation arrow puck
|
|
315
513
|
mapView.location.apply {
|
|
316
514
|
setLocationProvider(navigationLocationProvider)
|
|
317
|
-
|
|
515
|
+
updateSettings {
|
|
516
|
+
locationPuck = LocationPuck2D(
|
|
517
|
+
bearingImage = ImageHolder.from(
|
|
518
|
+
com.mapbox.navigation.R.drawable.mapbox_navigation_puck_icon
|
|
519
|
+
)
|
|
520
|
+
)
|
|
521
|
+
puckBearingEnabled = true
|
|
522
|
+
enabled = true
|
|
523
|
+
}
|
|
524
|
+
puckBearing = PuckBearing.COURSE
|
|
318
525
|
}
|
|
319
526
|
|
|
320
527
|
registerObservers()
|
|
@@ -327,23 +534,22 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
327
534
|
private fun registerObservers() {
|
|
328
535
|
val nav = mapboxNavigation ?: return
|
|
329
536
|
|
|
330
|
-
//
|
|
537
|
+
// Routes observer
|
|
331
538
|
nav.registerRoutesObserver(object : RoutesObserver {
|
|
332
539
|
override fun onRoutesChanged(result: RoutesUpdatedResult) {
|
|
333
540
|
if (result.navigationRoutes.isNotEmpty()) {
|
|
541
|
+
// Cancel any in-flight speech when route changes
|
|
542
|
+
speechApi.cancel()
|
|
543
|
+
voiceInstructionsPlayer.clear()
|
|
544
|
+
|
|
334
545
|
routeLineApi.setNavigationRoutes(result.navigationRoutes) { value ->
|
|
335
|
-
mapView.mapboxMap.style?.apply {
|
|
336
|
-
routeLineView.renderRouteDrawData(this, value)
|
|
337
|
-
}
|
|
546
|
+
mapView.mapboxMap.style?.apply { routeLineView.renderRouteDrawData(this, value) }
|
|
338
547
|
}
|
|
339
548
|
viewportDataSource.onRouteChanged(result.navigationRoutes.first())
|
|
340
549
|
viewportDataSource.evaluate()
|
|
341
|
-
|
|
342
|
-
// FIX: transition to following mode when route is set
|
|
343
|
-
// This triggers the camera to zoom/pitch/center correctly
|
|
550
|
+
isOverviewMode = false
|
|
344
551
|
safeCameraFollowing()
|
|
345
552
|
showUI()
|
|
346
|
-
|
|
347
553
|
onRoutesReady(mapOf(
|
|
348
554
|
"routeCount" to result.navigationRoutes.size,
|
|
349
555
|
"distanceMeters" to (result.navigationRoutes.first().directionsRoute.distance() ?: 0.0),
|
|
@@ -363,25 +569,19 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
363
569
|
}
|
|
364
570
|
})
|
|
365
571
|
|
|
366
|
-
//
|
|
572
|
+
// Route progress observer
|
|
367
573
|
nav.registerRouteProgressObserver(RouteProgressObserver { routeProgress ->
|
|
368
|
-
// Update viewport with progress (camera follows route progress)
|
|
369
574
|
viewportDataSource.onRouteProgressChanged(routeProgress)
|
|
370
575
|
viewportDataSource.evaluate()
|
|
371
576
|
|
|
372
|
-
// Draw maneuver arrow
|
|
373
577
|
mapView.mapboxMap.style?.let { style ->
|
|
374
578
|
val arrowResult = routeArrowApi.addUpcomingManeuverArrow(routeProgress)
|
|
375
579
|
routeArrowView.renderManeuverUpdate(style, arrowResult)
|
|
376
580
|
}
|
|
377
581
|
|
|
378
|
-
// Maneuver banner
|
|
379
582
|
val maneuvers = maneuverApi.getManeuvers(routeProgress)
|
|
380
583
|
maneuvers.fold(
|
|
381
|
-
{ error ->
|
|
382
|
-
Log.w(TAG, "Maneuver error: ${error.errorMessage}")
|
|
383
|
-
Unit
|
|
384
|
-
},
|
|
584
|
+
{ error -> Log.w(TAG, "Maneuver error: ${error.errorMessage}"); Unit },
|
|
385
585
|
{ _ ->
|
|
386
586
|
maneuverView?.visibility = View.VISIBLE
|
|
387
587
|
maneuverView?.renderManeuvers(maneuvers)
|
|
@@ -389,7 +589,6 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
389
589
|
}
|
|
390
590
|
)
|
|
391
591
|
|
|
392
|
-
// Trip progress
|
|
393
592
|
val tripProgress = tripProgressApi.getTripProgress(routeProgress)
|
|
394
593
|
tripProgressView?.render(tripProgress)
|
|
395
594
|
updateEtaBar(
|
|
@@ -404,37 +603,24 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
404
603
|
"distanceTraveled" to routeProgress.distanceTraveled,
|
|
405
604
|
"fractionTraveled" to routeProgress.fractionTraveled,
|
|
406
605
|
"currentStepDistanceRemaining" to
|
|
407
|
-
(routeProgress.currentLegProgress
|
|
408
|
-
?.currentStepProgress?.distanceRemaining ?: 0f)
|
|
606
|
+
(routeProgress.currentLegProgress?.currentStepProgress?.distanceRemaining ?: 0f)
|
|
409
607
|
))
|
|
410
608
|
})
|
|
411
609
|
|
|
412
|
-
//
|
|
610
|
+
// Location observer
|
|
413
611
|
nav.registerLocationObserver(object : LocationObserver {
|
|
414
612
|
override fun onNewRawLocation(rawLocation: Location) {}
|
|
415
|
-
|
|
416
613
|
override fun onNewLocationMatcherResult(result: LocationMatcherResult) {
|
|
417
614
|
val loc = result.enhancedLocation
|
|
418
|
-
|
|
419
|
-
// Update location puck position
|
|
420
|
-
navigationLocationProvider.changePosition(
|
|
421
|
-
location = loc,
|
|
422
|
-
keyPoints = result.keyPoints,
|
|
423
|
-
)
|
|
424
|
-
|
|
425
|
-
// FIX: Update viewport data source with location AND bearing
|
|
426
|
-
// This is what drives the camera to follow the vehicle's direction
|
|
615
|
+
navigationLocationProvider.changePosition(location = loc, keyPoints = result.keyPoints)
|
|
427
616
|
viewportDataSource.onLocationChanged(loc)
|
|
428
617
|
viewportDataSource.evaluate()
|
|
429
618
|
|
|
430
|
-
// FIX: On first location fix, immediately enter following mode
|
|
431
|
-
// This ensures the camera centers on the user as soon as GPS is available
|
|
432
619
|
if (!firstLocationReceived) {
|
|
433
620
|
firstLocationReceived = true
|
|
434
621
|
safeCameraFollowing()
|
|
435
622
|
}
|
|
436
623
|
|
|
437
|
-
// ── Speed limit display ────────────────────────────────────────
|
|
438
624
|
val fmtOptions = DistanceFormatterOptions.Builder(context).build()
|
|
439
625
|
val speedInfo = speedInfoApi.updatePostedAndCurrentSpeed(result, fmtOptions)
|
|
440
626
|
if (speedInfo != null) {
|
|
@@ -447,43 +633,63 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
447
633
|
checkAndSwitchDayNight()
|
|
448
634
|
}
|
|
449
635
|
})
|
|
636
|
+
|
|
637
|
+
// Voice instructions observer — triggers TTS announcements
|
|
638
|
+
nav.registerVoiceInstructionsObserver(voiceInstructionsObserver)
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
642
|
+
// Button actions
|
|
643
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
644
|
+
private fun toggleMute() {
|
|
645
|
+
isMuted = !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))
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
private fun toggleOverview() {
|
|
653
|
+
if (isOverviewMode) {
|
|
654
|
+
recenterCamera()
|
|
655
|
+
} else {
|
|
656
|
+
isOverviewMode = true
|
|
657
|
+
btnRecenterView?.visibility = View.VISIBLE
|
|
658
|
+
try {
|
|
659
|
+
navigationCamera.requestNavigationCameraToOverview()
|
|
660
|
+
} catch (e: Exception) {
|
|
661
|
+
Log.e(TAG, "Camera overview error: ${e.message}")
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
private fun recenterCamera() {
|
|
667
|
+
isOverviewMode = false
|
|
668
|
+
btnRecenterView?.visibility = View.GONE
|
|
669
|
+
safeCameraFollowing()
|
|
450
670
|
}
|
|
451
671
|
|
|
452
672
|
// ─────────────────────────────────────────────────────────────────────────
|
|
453
|
-
// ETA bar
|
|
454
|
-
// Fields confirmed from TripProgressUpdateValue docs:
|
|
455
|
-
// estimatedTimeToArrival: Long — Unix timestamp ms
|
|
456
|
-
// totalTimeRemaining: Double — seconds
|
|
457
|
-
// distanceRemaining: Double — metres
|
|
673
|
+
// ETA bar
|
|
458
674
|
// ─────────────────────────────────────────────────────────────────────────
|
|
459
675
|
private fun updateEtaBar(
|
|
460
676
|
estimatedTimeToArrivalMs: Long,
|
|
461
677
|
totalTimeRemainingSec: Double,
|
|
462
678
|
distanceRemainingMetres: Double
|
|
463
679
|
) {
|
|
464
|
-
val arrivalCal = Calendar.getInstance().apply {
|
|
465
|
-
timeInMillis = estimatedTimeToArrivalMs
|
|
466
|
-
}
|
|
680
|
+
val arrivalCal = Calendar.getInstance().apply { timeInMillis = estimatedTimeToArrivalMs }
|
|
467
681
|
tvEtaTime?.text = String.format(
|
|
468
|
-
"%02d:%02d",
|
|
469
|
-
arrivalCal.get(Calendar.HOUR_OF_DAY),
|
|
470
|
-
arrivalCal.get(Calendar.MINUTE)
|
|
682
|
+
"%02d:%02d", arrivalCal.get(Calendar.HOUR_OF_DAY), arrivalCal.get(Calendar.MINUTE)
|
|
471
683
|
)
|
|
472
|
-
|
|
473
684
|
val totalMin = (totalTimeRemainingSec / 60).toInt()
|
|
474
|
-
tvDuration?.text = if (totalMin >= 60)
|
|
475
|
-
"${totalMin / 60}h ${totalMin % 60}min"
|
|
476
|
-
else
|
|
477
|
-
"${totalMin} min"
|
|
478
|
-
|
|
685
|
+
tvDuration?.text = if (totalMin >= 60) "${totalMin / 60}h ${totalMin % 60}min" else "$totalMin min"
|
|
479
686
|
tvDistance?.text = if (distanceRemainingMetres >= 1000)
|
|
480
687
|
String.format("%.1f km", distanceRemainingMetres / 1000.0)
|
|
481
|
-
else
|
|
482
|
-
"${distanceRemainingMetres.toInt()} m"
|
|
688
|
+
else "${distanceRemainingMetres.toInt()} m"
|
|
483
689
|
}
|
|
484
690
|
|
|
485
691
|
// ─────────────────────────────────────────────────────────────────────────
|
|
486
|
-
// Day / Night
|
|
692
|
+
// Day / Night
|
|
487
693
|
// ─────────────────────────────────────────────────────────────────────────
|
|
488
694
|
private fun getAutoStyle(): String {
|
|
489
695
|
val hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY)
|
|
@@ -496,9 +702,9 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
496
702
|
val shouldBeNight = hour !in 6..20
|
|
497
703
|
if (shouldBeNight == isNightMode) return
|
|
498
704
|
isNightMode = shouldBeNight
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
705
|
+
mapView.mapboxMap.loadStyle(
|
|
706
|
+
if (shouldBeNight) NavigationStyles.NAVIGATION_NIGHT_STYLE else NavigationStyles.NAVIGATION_DAY_STYLE
|
|
707
|
+
) { style ->
|
|
502
708
|
routeLineView.initializeLayers(style)
|
|
503
709
|
mapboxNavigation?.getNavigationRoutes()?.takeIf { it.isNotEmpty() }?.let { routes ->
|
|
504
710
|
routeLineApi.setNavigationRoutes(routes) { value ->
|
|
@@ -508,29 +714,23 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
508
714
|
}
|
|
509
715
|
}
|
|
510
716
|
|
|
511
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
512
|
-
// Camera following — safe wrapper (issue #43)
|
|
513
|
-
// FIX: maxDuration(0) = instant transition, no animation delay
|
|
514
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
515
717
|
private fun safeCameraFollowing() {
|
|
516
718
|
try {
|
|
517
719
|
navigationCamera.requestNavigationCameraToFollowing(
|
|
518
|
-
stateTransitionOptions = NavigationCameraTransitionOptions.Builder()
|
|
519
|
-
.maxDuration(0)
|
|
520
|
-
.build()
|
|
720
|
+
stateTransitionOptions = NavigationCameraTransitionOptions.Builder().maxDuration(0).build()
|
|
521
721
|
)
|
|
522
722
|
} catch (e: Exception) {
|
|
523
723
|
Log.e(TAG, "Camera following error: ${e.message}")
|
|
524
724
|
}
|
|
525
725
|
}
|
|
526
726
|
|
|
527
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
528
|
-
// Cancel navigation — also stops foreground service (fixes notification crash)
|
|
529
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
530
727
|
private fun cancelNavigation() {
|
|
728
|
+
speechApi.cancel()
|
|
729
|
+
voiceInstructionsPlayer.clear()
|
|
531
730
|
mapboxNavigation?.setNavigationRoutes(listOf())
|
|
532
731
|
mapboxNavigation?.stopTripSession()
|
|
533
732
|
firstLocationReceived = false
|
|
733
|
+
isOverviewMode = false
|
|
534
734
|
hideUI()
|
|
535
735
|
onNavigationCancelled(mapOf<String, Any>())
|
|
536
736
|
}
|
|
@@ -538,17 +738,16 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
538
738
|
private fun showUI() {
|
|
539
739
|
maneuverView?.visibility = View.VISIBLE
|
|
540
740
|
etaBar?.visibility = View.VISIBLE
|
|
741
|
+
sideButtons?.visibility = View.VISIBLE
|
|
541
742
|
}
|
|
542
743
|
|
|
543
744
|
private fun hideUI() {
|
|
544
745
|
maneuverView?.visibility = View.INVISIBLE
|
|
545
746
|
speedInfoView?.visibility = View.GONE
|
|
546
747
|
etaBar?.visibility = View.INVISIBLE
|
|
748
|
+
sideButtons?.visibility = View.INVISIBLE
|
|
547
749
|
}
|
|
548
750
|
|
|
549
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
550
|
-
// Issue #31 — Voice units
|
|
551
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
552
751
|
private fun resolveVoiceUnits(): String {
|
|
553
752
|
return when (voiceUnits?.lowercase()) {
|
|
554
753
|
"metric" -> "metric"
|
|
@@ -560,91 +759,58 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
560
759
|
}
|
|
561
760
|
}
|
|
562
761
|
|
|
563
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
564
|
-
// Route request
|
|
565
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
566
762
|
@SuppressLint("MissingPermission")
|
|
567
763
|
private fun fetchRoutes() {
|
|
568
764
|
val nav = mapboxNavigation ?: return
|
|
569
765
|
if (coordinates.size < 2) return
|
|
570
|
-
|
|
571
|
-
val points = coordinates.map {
|
|
572
|
-
Point.fromLngLat(it["longitude"] ?: 0.0, it["latitude"] ?: 0.0)
|
|
573
|
-
}
|
|
766
|
+
val points = coordinates.map { Point.fromLngLat(it["longitude"] ?: 0.0, it["latitude"] ?: 0.0) }
|
|
574
767
|
val locale = language?.let { Locale.forLanguageTag(it) } ?: Locale.getDefault()
|
|
575
|
-
|
|
576
768
|
val builder = RouteOptions.builder()
|
|
577
769
|
.applyDefaultNavigationOptions()
|
|
578
770
|
.language(locale.toLanguageTag())
|
|
579
771
|
.voiceUnits(resolveVoiceUnits())
|
|
580
772
|
.coordinatesList(points)
|
|
581
|
-
|
|
582
|
-
.annotations("maxspeed,congestion,duration")
|
|
583
|
-
|
|
773
|
+
.annotations("maxspeed,congestion,duration,speed")
|
|
584
774
|
waypointIndices?.let { builder.waypointIndicesList(it) }
|
|
585
|
-
navigationProfile?.let {
|
|
586
|
-
|
|
587
|
-
}
|
|
588
|
-
excludeTypes?.takeIf { it.isNotEmpty() }?.let {
|
|
589
|
-
builder.exclude(it.joinToString(","))
|
|
590
|
-
}
|
|
775
|
+
navigationProfile?.let { builder.profile(if (it.startsWith("mapbox/")) it else "mapbox/$it") }
|
|
776
|
+
excludeTypes?.takeIf { it.isNotEmpty() }?.let { builder.exclude(it.joinToString(",")) }
|
|
591
777
|
maxHeight?.let { builder.maxHeight(it) }
|
|
592
778
|
maxWidth?.let { builder.maxWidth(it) }
|
|
593
|
-
|
|
594
779
|
nav.requestRoutes(builder.build(), object : NavigationRouterCallback {
|
|
595
|
-
override fun onRoutesReady(
|
|
596
|
-
routes
|
|
597
|
-
@RouterOrigin routerOrigin: String
|
|
598
|
-
) {
|
|
599
|
-
if (routes.isEmpty()) {
|
|
600
|
-
onRoutesFailed(mapOf("message" to "No routes returned"))
|
|
601
|
-
return
|
|
602
|
-
}
|
|
780
|
+
override fun onRoutesReady(routes: List<NavigationRoute>, @RouterOrigin routerOrigin: String) {
|
|
781
|
+
if (routes.isEmpty()) { onRoutesFailed(mapOf("message" to "No routes returned")); return }
|
|
603
782
|
nav.setNavigationRoutes(routes)
|
|
604
783
|
nav.startTripSession()
|
|
605
|
-
// Camera will follow automatically via onNewLocationMatcherResult
|
|
606
|
-
// + onRoutesChanged observer
|
|
607
784
|
}
|
|
608
|
-
|
|
609
785
|
override fun onFailure(reasons: List<RouterFailure>, routeOptions: RouteOptions) {
|
|
610
|
-
|
|
611
|
-
Log.e(TAG, "Route failed: $msg")
|
|
612
|
-
onRoutesFailed(mapOf("message" to msg))
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
override fun onCanceled(
|
|
616
|
-
routeOptions: RouteOptions,
|
|
617
|
-
@RouterOrigin routerOrigin: String
|
|
618
|
-
) {
|
|
619
|
-
Log.d(TAG, "Route cancelled")
|
|
786
|
+
onRoutesFailed(mapOf("message" to (reasons.firstOrNull()?.message ?: "Unknown error")))
|
|
620
787
|
}
|
|
788
|
+
override fun onCanceled(routeOptions: RouteOptions, @RouterOrigin routerOrigin: String) {}
|
|
621
789
|
})
|
|
622
790
|
}
|
|
623
791
|
|
|
624
|
-
|
|
625
|
-
fun setCoordinates(coords: List<Map<String, Double>>) {
|
|
626
|
-
coordinates = coords
|
|
627
|
-
if (coords.size >= 2) fetchRoutes()
|
|
628
|
-
}
|
|
792
|
+
fun setCoordinates(coords: List<Map<String, Double>>) { coordinates = coords; if (coords.size >= 2) fetchRoutes() }
|
|
629
793
|
fun setWaypointIndices(i: List<Int>?) { waypointIndices = i }
|
|
630
794
|
fun setLanguage(l: String?) { language = l }
|
|
631
795
|
fun setVoiceUnits(u: String?) { voiceUnits = u }
|
|
632
796
|
fun setNavigationProfile(p: String?) { navigationProfile = p }
|
|
633
797
|
fun setExcludeTypes(t: List<String>?) { excludeTypes = t }
|
|
634
798
|
fun setMapStyle(s: String?) { mapStyle = s }
|
|
635
|
-
fun setMute(m: Boolean) {
|
|
799
|
+
fun setMute(m: Boolean) { if (m != isMuted) toggleMute() }
|
|
636
800
|
fun setMaxHeight(h: Double?) { maxHeight = h }
|
|
637
801
|
fun setMaxWidth(w: Double?) { maxWidth = w }
|
|
638
802
|
fun setUseMapMatching(u: Boolean) { useMapMatching = u }
|
|
639
803
|
fun setCustomRasterTileUrl(u: String?) { customRasterTileUrl = u }
|
|
640
804
|
fun setCustomRasterAboveLayerId(l: String?) { customRasterAboveLayerId = l }
|
|
641
805
|
|
|
642
|
-
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
|
643
806
|
override fun onDetachedFromWindow() {
|
|
644
807
|
super.onDetachedFromWindow()
|
|
808
|
+
speechApi.cancel()
|
|
809
|
+
voiceInstructionsPlayer.shutdown()
|
|
645
810
|
maneuverApi.cancel()
|
|
646
811
|
routeLineApi.cancel()
|
|
647
812
|
routeLineView.cancel()
|
|
813
|
+
mapboxNavigation?.unregisterVoiceInstructionsObserver(voiceInstructionsObserver)
|
|
648
814
|
mapboxNavigation?.stopTripSession()
|
|
649
815
|
MapboxNavigationProvider.destroy()
|
|
650
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",
|