@jacques_gordon/expo-mapbox-navigation 2.2.1 → 2.2.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/README.md +36 -0
- package/android/src/main/java/expo/modules/mapboxnavigation/ExpoMapboxNavigationModule.kt +19 -0
- package/android/src/main/java/expo/modules/mapboxnavigation/ExpoMapboxNavigationView.kt +59 -23
- package/build/index.d.ts +15 -0
- package/ios/ExpoMapboxNavigationView.swift +264 -353
- package/package.json +1 -1
- package/src/index.tsx +23 -0
package/README.md
CHANGED
|
@@ -99,6 +99,42 @@ export default function Navigation() {
|
|
|
99
99
|
}
|
|
100
100
|
```
|
|
101
101
|
|
|
102
|
+
### Color customization (Android)
|
|
103
|
+
|
|
104
|
+
All color props are optional — if not provided, the defaults below are applied automatically.
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
<MapboxNavigationView
|
|
108
|
+
// Maneuver banner (turn-by-turn instruction banner)
|
|
109
|
+
maneuverBackgroundColorDay="#1E2433" // default: Mapbox native style color
|
|
110
|
+
maneuverTurnIconColor="#1A73E8" // default: Mapbox native style color
|
|
111
|
+
|
|
112
|
+
// Bottom ETA / duration / distance bar
|
|
113
|
+
etaBarBackgroundColor="#1E2433" // default: #1E2433 (dark navy)
|
|
114
|
+
etaTextColor="#FFFFFF" // default: #FFFFFF (white)
|
|
115
|
+
|
|
116
|
+
// Control buttons (mute, overview, recenter)
|
|
117
|
+
iconButtonColor="#1A73E8" // default: #1A73E8 (Google Blue)
|
|
118
|
+
iconButtonMutedColor="#EA4335" // default: #EA4335 (Google Red)
|
|
119
|
+
|
|
120
|
+
// Route line color — set via plugin config, not a view prop
|
|
121
|
+
// (uses androidColorOverrides in app.json)
|
|
122
|
+
{...otherProps}
|
|
123
|
+
/>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
// app.json — route line and other Mapbox native resource colors
|
|
128
|
+
["@jacques_gordon/expo-mapbox-navigation", {
|
|
129
|
+
"accessToken": "pk.xxx",
|
|
130
|
+
"downloadsToken": "sk.xxx",
|
|
131
|
+
"androidColorOverrides": {
|
|
132
|
+
"mapbox_primary_route_color": "#0055FF",
|
|
133
|
+
"mapbox_main_maneuver_background_color": "#FF5500"
|
|
134
|
+
}
|
|
135
|
+
}]
|
|
136
|
+
```
|
|
137
|
+
|
|
102
138
|
---
|
|
103
139
|
|
|
104
140
|
## Props
|
|
@@ -58,6 +58,25 @@ class ExpoMapboxNavigationModule : Module() {
|
|
|
58
58
|
Prop("customRasterAboveLayerId") { view: ExpoMapboxNavigationView, layerId: String? ->
|
|
59
59
|
view.setCustomRasterAboveLayerId(layerId)
|
|
60
60
|
}
|
|
61
|
+
// ── Color customization props ────────────────────────────────────────
|
|
62
|
+
Prop("maneuverBackgroundColorDay") { view: ExpoMapboxNavigationView, color: String? ->
|
|
63
|
+
view.setManeuverBackgroundColorDay(color)
|
|
64
|
+
}
|
|
65
|
+
Prop("maneuverTurnIconColor") { view: ExpoMapboxNavigationView, color: String? ->
|
|
66
|
+
view.setManeuverTurnIconColor(color)
|
|
67
|
+
}
|
|
68
|
+
Prop("etaBarBackgroundColor") { view: ExpoMapboxNavigationView, color: String? ->
|
|
69
|
+
view.setEtaBarBackgroundColor(color)
|
|
70
|
+
}
|
|
71
|
+
Prop("etaTextColor") { view: ExpoMapboxNavigationView, color: String? ->
|
|
72
|
+
view.setEtaTextColor(color)
|
|
73
|
+
}
|
|
74
|
+
Prop("iconButtonColor") { view: ExpoMapboxNavigationView, color: String? ->
|
|
75
|
+
view.setIconButtonColor(color)
|
|
76
|
+
}
|
|
77
|
+
Prop("iconButtonMutedColor") { view: ExpoMapboxNavigationView, color: String? ->
|
|
78
|
+
view.setIconButtonMutedColor(color)
|
|
79
|
+
}
|
|
61
80
|
}
|
|
62
81
|
}
|
|
63
82
|
}
|
|
@@ -54,6 +54,7 @@ import com.mapbox.navigation.tripdata.progress.model.EstimatedTimeToArrivalForma
|
|
|
54
54
|
import com.mapbox.navigation.tripdata.progress.model.TimeRemainingFormatter
|
|
55
55
|
import com.mapbox.navigation.tripdata.progress.model.TripProgressUpdateFormatter
|
|
56
56
|
import com.mapbox.navigation.tripdata.speedlimit.api.MapboxSpeedInfoApi
|
|
57
|
+
import com.mapbox.navigation.ui.components.maneuver.model.ManeuverViewOptions
|
|
57
58
|
import com.mapbox.navigation.ui.components.maneuver.view.MapboxManeuverView
|
|
58
59
|
import com.mapbox.navigation.ui.components.speedlimit.view.MapboxSpeedInfoView
|
|
59
60
|
import com.mapbox.navigation.ui.components.tripprogress.view.MapboxTripProgressView
|
|
@@ -143,8 +144,17 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
143
144
|
// Previous bottom=300dp pushed the focal point too far up, causing
|
|
144
145
|
// the puck to visibly jump/drift between location updates.
|
|
145
146
|
// Official guide value: top=180, left=40, bottom=150, right=40
|
|
147
|
+
// ── Viewport padding — Waze-style vehicle position ──────────────────────────
|
|
148
|
+
// bottom=300dp pushes the focal point (and thus the vehicle puck) up to
|
|
149
|
+
// roughly 30% from the bottom of the screen, matching Waze/Google Maps.
|
|
150
|
+
// NOTE: this was briefly regressed to bottom=150dp (the "exact" value from
|
|
151
|
+
// the Mapbox camera guide example) while investigating puck jitter — but
|
|
152
|
+
// the jitter was actually caused by passing keyPoints to changePosition()
|
|
153
|
+
// (see the LocationObserver fix below), NOT by this padding value. The
|
|
154
|
+
// jitter fix and the Waze-style positioning are independent and both
|
|
155
|
+
// needed; restoring bottom=300dp here does not reintroduce the jitter.
|
|
146
156
|
private val followingPadding by lazy {
|
|
147
|
-
EdgeInsets(180.0 * dp, 40.0 * dp,
|
|
157
|
+
EdgeInsets(180.0 * dp, 40.0 * dp, 300.0 * dp, 40.0 * dp)
|
|
148
158
|
}
|
|
149
159
|
private val overviewPadding by lazy {
|
|
150
160
|
EdgeInsets(140.0 * dp, 40.0 * dp, 120.0 * dp, 40.0 * dp)
|
|
@@ -165,6 +175,18 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
165
175
|
private var customRasterTileUrl: String? = null
|
|
166
176
|
private var customRasterAboveLayerId: String? = null
|
|
167
177
|
|
|
178
|
+
// ── Color customization props ────────────────────────────────────────────
|
|
179
|
+
// Maneuver banner colors — verified against ManeuverViewOptions public API
|
|
180
|
+
// (maneuverBackgroundColor, turnIconManeuver are real documented properties)
|
|
181
|
+
private var maneuverBackgroundColorDay: String? = null
|
|
182
|
+
private var maneuverTurnIconColor: String? = null
|
|
183
|
+
// ETA bottom bar colors — fully custom view, safe to color freely
|
|
184
|
+
private var etaBarBackgroundColor: String? = null
|
|
185
|
+
private var etaTextColor: String? = null
|
|
186
|
+
// Custom icon button colors (mute, overview, recenter) — our own bitmaps
|
|
187
|
+
private var iconButtonColor: String? = null
|
|
188
|
+
private var iconButtonMutedColor: String? = null
|
|
189
|
+
|
|
168
190
|
init {
|
|
169
191
|
buildUI()
|
|
170
192
|
initAPIs()
|
|
@@ -182,7 +204,10 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
182
204
|
val bmp = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
|
|
183
205
|
val c = Canvas(bmp)
|
|
184
206
|
val s = size.toFloat()
|
|
185
|
-
val
|
|
207
|
+
val defaultColor = if (muted) "#5F6368" else "#1A73E8"
|
|
208
|
+
val color = try {
|
|
209
|
+
Color.parseColor(if (muted) (iconButtonMutedColor ?: defaultColor) else (iconButtonColor ?: defaultColor))
|
|
210
|
+
} catch (e: Exception) { Color.parseColor(defaultColor) }
|
|
186
211
|
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
187
212
|
this.color = color
|
|
188
213
|
style = Paint.Style.FILL
|
|
@@ -226,9 +251,8 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
226
251
|
val c = Canvas(bmp)
|
|
227
252
|
val s = size.toFloat()
|
|
228
253
|
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
229
|
-
color = Color.parseColor("#1A73E8")
|
|
254
|
+
color = try { Color.parseColor(iconButtonColor ?: "#1A73E8") } catch (e: Exception) { Color.parseColor("#1A73E8") }
|
|
230
255
|
}
|
|
231
|
-
// Map/overview icon: stacked layers (like Google Maps "map view" icon)
|
|
232
256
|
paint.style = Paint.Style.STROKE
|
|
233
257
|
paint.strokeWidth = s * 0.07f
|
|
234
258
|
paint.strokeJoin = Paint.Join.ROUND
|
|
@@ -256,10 +280,9 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
256
280
|
val c = Canvas(bmp)
|
|
257
281
|
val s = size.toFloat()
|
|
258
282
|
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
259
|
-
color = Color.parseColor("#1A73E8")
|
|
283
|
+
color = try { Color.parseColor(iconButtonColor ?: "#1A73E8") } catch (e: Exception) { Color.parseColor("#1A73E8") }
|
|
260
284
|
style = Paint.Style.FILL
|
|
261
285
|
}
|
|
262
|
-
// Navigation/recenter compass arrow (Google Maps "my location, follow" style)
|
|
263
286
|
val arrow = Path().apply {
|
|
264
287
|
moveTo(s * 0.50f, s * 0.12f) // tip
|
|
265
288
|
lineTo(s * 0.80f, s * 0.82f) // bottom-right
|
|
@@ -302,20 +325,19 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
302
325
|
))
|
|
303
326
|
|
|
304
327
|
// ── ManeuverView — top ─────────────────────────────────────────────────
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
val mv = MapboxManeuverView(context)
|
|
328
|
+
// Colors are customizable via maneuverBackgroundColorDay,
|
|
329
|
+
// maneuverTurnIconColor, and laneGuidanceTurnIconColor props using the
|
|
330
|
+
// SDK's official ManeuverViewOptions.Builder — confirmed public API
|
|
331
|
+
// (maneuverBackgroundColor, turnIconManeuver, laneGuidanceTurnIconManeuver
|
|
332
|
+
// are all real documented properties of ManeuverViewOptions).
|
|
333
|
+
val maneuverOptionsBuilder = ManeuverViewOptions.Builder()
|
|
334
|
+
maneuverBackgroundColorDay?.let {
|
|
335
|
+
try { maneuverOptionsBuilder.maneuverBackgroundColor(Color.parseColor(it)) } catch (e: Exception) {}
|
|
336
|
+
}
|
|
337
|
+
maneuverTurnIconColor?.let {
|
|
338
|
+
try { maneuverOptionsBuilder.turnIconManeuver(Color.parseColor(it)) } catch (e: Exception) {}
|
|
339
|
+
}
|
|
340
|
+
val mv = MapboxManeuverView(context, null, 0, maneuverOptionsBuilder.build())
|
|
319
341
|
root.addView(mv as View, FrameLayout.LayoutParams(
|
|
320
342
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
321
343
|
FrameLayout.LayoutParams.WRAP_CONTENT
|
|
@@ -384,9 +406,11 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
384
406
|
speedInfoView = siv
|
|
385
407
|
|
|
386
408
|
// ── ETA bottom bar ─────────────────────────────────────────────────────
|
|
409
|
+
val resolvedEtaBg = try { Color.parseColor(etaBarBackgroundColor ?: "#1E2433") } catch (e: Exception) { Color.parseColor("#1E2433") }
|
|
410
|
+
val resolvedEtaText = try { Color.parseColor(etaTextColor ?: "#FFFFFF") } catch (e: Exception) { Color.WHITE }
|
|
387
411
|
val bar = LinearLayout(context).apply {
|
|
388
412
|
orientation = LinearLayout.HORIZONTAL
|
|
389
|
-
setBackgroundColor(
|
|
413
|
+
setBackgroundColor(resolvedEtaBg)
|
|
390
414
|
elevation = 8 * dp
|
|
391
415
|
visibility = View.INVISIBLE
|
|
392
416
|
gravity = Gravity.CENTER_VERTICAL
|
|
@@ -401,7 +425,7 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
401
425
|
).also { it.gravity = Gravity.BOTTOM })
|
|
402
426
|
|
|
403
427
|
val etaTime = TextView(context).apply {
|
|
404
|
-
textSize = 24f; setTextColor(
|
|
428
|
+
textSize = 24f; setTextColor(resolvedEtaText)
|
|
405
429
|
typeface = android.graphics.Typeface.DEFAULT_BOLD
|
|
406
430
|
}
|
|
407
431
|
bar.addView(etaTime, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f))
|
|
@@ -411,7 +435,7 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
411
435
|
orientation = LinearLayout.VERTICAL; gravity = Gravity.CENTER
|
|
412
436
|
}
|
|
413
437
|
val dur = TextView(context).apply {
|
|
414
|
-
textSize = 18f; setTextColor(
|
|
438
|
+
textSize = 18f; setTextColor(resolvedEtaText)
|
|
415
439
|
typeface = android.graphics.Typeface.DEFAULT_BOLD; gravity = Gravity.CENTER
|
|
416
440
|
}
|
|
417
441
|
val dist = TextView(context).apply {
|
|
@@ -421,6 +445,7 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
421
445
|
bar.addView(center, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 2f))
|
|
422
446
|
tvDuration = dur; tvDistance = dist
|
|
423
447
|
|
|
448
|
+
|
|
424
449
|
val cancelBtn = TextView(context).apply {
|
|
425
450
|
text = "✕"; textSize = 22f; setTextColor(Color.parseColor("#AAAAAA"))
|
|
426
451
|
gravity = Gravity.END or Gravity.CENTER_VERTICAL
|
|
@@ -857,6 +882,11 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
857
882
|
.voiceUnits(resolveVoiceUnits())
|
|
858
883
|
.coordinatesList(points)
|
|
859
884
|
.annotations("maxspeed,congestion,duration,speed")
|
|
885
|
+
// overview("full") is required by Mapbox support (GitHub issue #4069)
|
|
886
|
+
// to maximize the coverage of maxspeed annotations in the response.
|
|
887
|
+
// Without it, many road segments return no maxspeed data even when
|
|
888
|
+
// the speed limit is known — confirmed via Mapbox Support correspondence.
|
|
889
|
+
.overview("full")
|
|
860
890
|
// ── FIX: Lane guidance was not displaying ───────────────────────────
|
|
861
891
|
// Root cause: BannerInstructions.sub() (which carries lane data) is
|
|
862
892
|
// only returned by the Directions API when explicitly requested.
|
|
@@ -903,6 +933,12 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
903
933
|
fun setUseMapMatching(u: Boolean) { useMapMatching = u }
|
|
904
934
|
fun setCustomRasterTileUrl(u: String?) { customRasterTileUrl = u }
|
|
905
935
|
fun setCustomRasterAboveLayerId(l: String?) { customRasterAboveLayerId = l }
|
|
936
|
+
fun setManeuverBackgroundColorDay(c: String?) { maneuverBackgroundColorDay = c }
|
|
937
|
+
fun setManeuverTurnIconColor(c: String?) { maneuverTurnIconColor = c }
|
|
938
|
+
fun setEtaBarBackgroundColor(c: String?) { etaBarBackgroundColor = c }
|
|
939
|
+
fun setEtaTextColor(c: String?) { etaTextColor = c }
|
|
940
|
+
fun setIconButtonColor(c: String?) { iconButtonColor = c }
|
|
941
|
+
fun setIconButtonMutedColor(c: String?) { iconButtonMutedColor = c }
|
|
906
942
|
|
|
907
943
|
override fun onDetachedFromWindow() {
|
|
908
944
|
super.onDetachedFromWindow()
|
package/build/index.d.ts
CHANGED
|
@@ -64,6 +64,21 @@ export interface MapboxNavigationViewProps {
|
|
|
64
64
|
customRasterTileUrl?: string;
|
|
65
65
|
/** Map layer ID above which the custom raster layer is placed. */
|
|
66
66
|
customRasterAboveLayerId?: string;
|
|
67
|
+
/**
|
|
68
|
+
* Background color of the turn-by-turn instruction banner, as a hex string
|
|
69
|
+
* (e.g. "#1A73E8"). Uses the SDK's official ManeuverViewOptions API.
|
|
70
|
+
*/
|
|
71
|
+
maneuverBackgroundColorDay?: string;
|
|
72
|
+
/** Color of the turn arrow icon inside the instruction banner. */
|
|
73
|
+
maneuverTurnIconColor?: string;
|
|
74
|
+
/** Background color of the bottom ETA/duration/distance bar. */
|
|
75
|
+
etaBarBackgroundColor?: string;
|
|
76
|
+
/** Text color used for the ETA time and duration in the bottom bar. */
|
|
77
|
+
etaTextColor?: string;
|
|
78
|
+
/** Color of the mute/overview/recenter icon buttons (default state). */
|
|
79
|
+
iconButtonColor?: string;
|
|
80
|
+
/** Color of the mute icon when voice guidance is muted. */
|
|
81
|
+
iconButtonMutedColor?: string;
|
|
67
82
|
onRouteProgressChanged?: (event: {
|
|
68
83
|
nativeEvent: RouteProgressEvent;
|
|
69
84
|
}) => void;
|
|
@@ -5,390 +5,301 @@ import MapboxDirections
|
|
|
5
5
|
import MapboxNavigationCore
|
|
6
6
|
import MapboxNavigationUIKit
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
///
|
|
15
|
-
/// Unlike Android, where each UI piece (maneuver banner, speed limit, lane
|
|
16
|
-
/// guidance, voice instructions, recenter/overview camera) had to be wired up
|
|
17
|
-
/// individually, iOS's `NavigationViewController` is an official "drop-in" UI
|
|
18
|
-
/// that already includes ALL of these out of the box:
|
|
19
|
-
/// - Top maneuver banner with lane guidance (no extra code needed)
|
|
20
|
-
/// - Bottom trip progress bar (ETA, duration, distance)
|
|
21
|
-
/// - Speed limit display
|
|
22
|
-
/// - Voice instructions (spoken automatically)
|
|
23
|
-
/// - Recenter button (built into NavigationMapView's UserCourseView)
|
|
24
|
-
/// - Overview / following camera switching
|
|
25
|
-
///
|
|
26
|
-
/// What we still implement ourselves to match the Android module's public API
|
|
27
|
-
/// exactly:
|
|
28
|
-
/// - Route fetching parity with Android's RouteOptions flags
|
|
29
|
-
/// (bannerInstructions, steps, roundaboutExits, annotations)
|
|
30
|
-
/// - Voice unit resolution parity (issue #31 fix)
|
|
31
|
-
/// - Day/night automatic style switching parity
|
|
32
|
-
/// - All the same events (onRoutesReady, onRouteProgressChanged, etc.)
|
|
33
|
-
/// - Tap-to-open full steps list (onManeuverBannerPressed), mirroring the
|
|
34
|
-
/// Android `emitFullRouteSteps()` feature
|
|
8
|
+
// MARK: - Custom styles (replaces removed NavigationDayStyle/NavigationNightStyle in v3)
|
|
9
|
+
// In Navigation SDK v3, NavigationDayStyle and NavigationNightStyle were removed.
|
|
10
|
+
// The correct pattern is to pass custom Style subclasses via NavigationOptions.styles.
|
|
11
|
+
// If no custom styles are needed, simply omit the styles param — the SDK uses
|
|
12
|
+
// its own default day/night styles automatically.
|
|
13
|
+
|
|
35
14
|
public class ExpoMapboxNavigationView: ExpoView {
|
|
36
15
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
// MARK: - Init
|
|
76
|
-
|
|
77
|
-
public required init(appContext: AppContext? = nil) {
|
|
78
|
-
super.init(appContext: appContext)
|
|
79
|
-
setupNavigationProvider()
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
private func setupNavigationProvider() {
|
|
83
|
-
// CoreConfig() uses MBXAccessToken from Info.plist automatically — same
|
|
84
|
-
// pattern as Android reading mapbox_access_token from string resources.
|
|
85
|
-
let provider = MapboxNavigationProvider(coreConfig: .init())
|
|
86
|
-
self.mapboxNavigationProvider = provider
|
|
87
|
-
self.mapboxNavigation = provider.mapboxNavigation
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// MARK: - Day / Night auto switching (parity with Android getAutoStyle/checkAndSwitchDayNight)
|
|
91
|
-
|
|
92
|
-
private func isDaytime() -> Bool {
|
|
93
|
-
let hour = Calendar.current.component(.hour, from: Date())
|
|
94
|
-
return hour >= 6 && hour < 20
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// MARK: - Voice units resolution (Issue #31 parity)
|
|
98
|
-
|
|
99
|
-
private func resolveVoiceUnits() -> String {
|
|
100
|
-
if let units = voiceUnits?.lowercased(), units == "metric" || units == "imperial" {
|
|
101
|
-
return units
|
|
16
|
+
// MARK: - Event dispatchers
|
|
17
|
+
let onRouteProgressChanged = EventDispatcher()
|
|
18
|
+
let onRoutesReady = EventDispatcher()
|
|
19
|
+
let onNavigationFinished = EventDispatcher()
|
|
20
|
+
let onNavigationCancelled = EventDispatcher()
|
|
21
|
+
let onRoutesFailed = EventDispatcher()
|
|
22
|
+
let onArrival = EventDispatcher()
|
|
23
|
+
let onManeuverBannerPressed = EventDispatcher()
|
|
24
|
+
|
|
25
|
+
// MARK: - Mapbox core
|
|
26
|
+
private var mapboxNavigationProvider: MapboxNavigationProvider?
|
|
27
|
+
private var mapboxNavigation: MapboxNavigation?
|
|
28
|
+
private var navigationViewController: NavigationViewController?
|
|
29
|
+
private var currentNavigationRoutes: NavigationRoutes?
|
|
30
|
+
private var routeRequestTask: Task<Void, Never>?
|
|
31
|
+
|
|
32
|
+
// MARK: - Props
|
|
33
|
+
private var coordinates: [[String: Double]] = []
|
|
34
|
+
private var waypointIndices: [Int]?
|
|
35
|
+
private var language: String?
|
|
36
|
+
private var voiceUnits: String?
|
|
37
|
+
private var navigationProfile: String?
|
|
38
|
+
private var excludeTypes: [String]?
|
|
39
|
+
private var mapStyle: String?
|
|
40
|
+
private var mute: Bool = false
|
|
41
|
+
private var maxHeight: Double?
|
|
42
|
+
private var maxWidth: Double?
|
|
43
|
+
private var useMapMatching: Bool = false
|
|
44
|
+
private var customRasterTileUrl: String?
|
|
45
|
+
private var customRasterAboveLayerId: String?
|
|
46
|
+
|
|
47
|
+
// MARK: - Init
|
|
48
|
+
public required init(appContext: AppContext? = nil) {
|
|
49
|
+
super.init(appContext: appContext)
|
|
50
|
+
let provider = MapboxNavigationProvider(coreConfig: .init())
|
|
51
|
+
self.mapboxNavigationProvider = provider
|
|
52
|
+
self.mapboxNavigation = provider.mapboxNavigation
|
|
102
53
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
let lon = coord["longitude"] ?? 0.0
|
|
118
|
-
return Waypoint(coordinate: CLLocationCoordinate2D(latitude: lat, longitude: lon))
|
|
54
|
+
|
|
55
|
+
// MARK: - Voice units (Issue #31 parity with Android)
|
|
56
|
+
// FIX: use locale.regionCode instead of locale.region?.identifier
|
|
57
|
+
// (locale.region is only available on iOS 16+, regionCode works from iOS 2+)
|
|
58
|
+
private func resolveVoiceUnits() -> String {
|
|
59
|
+
if let units = voiceUnits?.lowercased(), units == "metric" || units == "imperial" {
|
|
60
|
+
return units
|
|
61
|
+
}
|
|
62
|
+
let localeIdentifier = language ?? Locale.current.identifier
|
|
63
|
+
let locale = Locale(identifier: localeIdentifier)
|
|
64
|
+
// regionCode is available iOS 2+ (unlike .region?.identifier which is iOS 16+)
|
|
65
|
+
let regionCode = locale.regionCode ?? ""
|
|
66
|
+
let imperialCountries: Set<String> = ["US", "GB", "LR", "MM"]
|
|
67
|
+
return imperialCountries.contains(regionCode) ? "imperial" : "metric"
|
|
119
68
|
}
|
|
120
69
|
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
// iOS Directions API client always requests steps + banner + voice
|
|
125
|
-
// instructions for navigation profiles), but we still set distanceUnit /
|
|
126
|
-
// locale explicitly for parity with the Android voiceUnits fix.
|
|
127
|
-
var options = NavigationRouteOptions(waypoints: waypoints)
|
|
70
|
+
// MARK: - Route fetching
|
|
71
|
+
private func fetchRoutes() {
|
|
72
|
+
guard coordinates.count >= 2, let mapboxNavigation = mapboxNavigation else { return }
|
|
128
73
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
74
|
+
let waypoints = coordinates.map { coord -> Waypoint in
|
|
75
|
+
let lat = coord["latitude"] ?? 0.0
|
|
76
|
+
let lon = coord["longitude"] ?? 0.0
|
|
77
|
+
return Waypoint(coordinate: CLLocationCoordinate2D(latitude: lat, longitude: lon))
|
|
78
|
+
}
|
|
132
79
|
|
|
133
|
-
|
|
134
|
-
options.distanceUnit = (unit == "imperial") ? .mile : .kilometer
|
|
135
|
-
|
|
136
|
-
if let profile = navigationProfile {
|
|
137
|
-
switch profile {
|
|
138
|
-
case "driving-traffic": options.profileIdentifier = .automobileAvoidingTraffic
|
|
139
|
-
case "driving": options.profileIdentifier = .automobile
|
|
140
|
-
case "walking": options.profileIdentifier = .walking
|
|
141
|
-
case "cycling": options.profileIdentifier = .cycling
|
|
142
|
-
default: break
|
|
143
|
-
}
|
|
144
|
-
} else {
|
|
145
|
-
options.profileIdentifier = .automobileAvoidingTraffic
|
|
146
|
-
}
|
|
80
|
+
var options = NavigationRouteOptions(waypoints: waypoints)
|
|
147
81
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
82
|
+
if let langTag = language {
|
|
83
|
+
options.locale = Locale(identifier: langTag)
|
|
84
|
+
}
|
|
85
|
+
let unit = resolveVoiceUnits()
|
|
86
|
+
options.distanceUnit = (unit == "imperial") ? .mile : .kilometer
|
|
87
|
+
|
|
88
|
+
if let profile = navigationProfile {
|
|
89
|
+
switch profile {
|
|
90
|
+
case "driving-traffic": options.profileIdentifier = .automobileAvoidingTraffic
|
|
91
|
+
case "driving": options.profileIdentifier = .automobile
|
|
92
|
+
case "walking": options.profileIdentifier = .walking
|
|
93
|
+
case "cycling": options.profileIdentifier = .cycling
|
|
94
|
+
default: break
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
options.profileIdentifier = .automobileAvoidingTraffic
|
|
98
|
+
}
|
|
165
99
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
voiceController: provider.routeVoiceController,
|
|
187
|
-
eventsManager: provider.eventsManager()
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
let vc = NavigationViewController(
|
|
191
|
-
navigationRoutes: navigationRoutes,
|
|
192
|
-
navigationOptions: navigationOptions
|
|
193
|
-
)
|
|
194
|
-
vc.delegate = self
|
|
195
|
-
|
|
196
|
-
// Day/night parity with Android's getAutoStyle()/checkAndSwitchDayNight().
|
|
197
|
-
isNightMode = !isDaytime()
|
|
198
|
-
if let styleManager = vc.styleManager {
|
|
199
|
-
styleManager.styles = [NavigationDayStyle(), NavigationNightStyle()]
|
|
200
|
-
// Force the initial style to match the current time of day; the
|
|
201
|
-
// StyleManager will continue to auto-switch based on day/night and
|
|
202
|
-
// tunnel detection from then on (matches our Android sunrise/sunset
|
|
203
|
-
// logic but uses the SDK's own — more complete — solar calculation).
|
|
100
|
+
routeRequestTask?.cancel()
|
|
101
|
+
routeRequestTask = Task { [weak self] in
|
|
102
|
+
guard let self = self else { return }
|
|
103
|
+
let request = mapboxNavigation.routingProvider().calculateRoutes(options: options)
|
|
104
|
+
switch await request.result {
|
|
105
|
+
case .failure(let error):
|
|
106
|
+
self.onRoutesFailed(["message": error.localizedDescription])
|
|
107
|
+
case .success(let navigationRoutes):
|
|
108
|
+
self.currentNavigationRoutes = navigationRoutes
|
|
109
|
+
let mainRoute = navigationRoutes.mainRoute.route
|
|
110
|
+
self.onRoutesReady([
|
|
111
|
+
"routeCount": navigationRoutes.alternativeRoutes.count + 1,
|
|
112
|
+
"distanceMeters": mainRoute.distance,
|
|
113
|
+
"durationSeconds": mainRoute.expectedTravelTime
|
|
114
|
+
])
|
|
115
|
+
await MainActor.run {
|
|
116
|
+
self.presentNavigationViewController(with: navigationRoutes)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
204
120
|
}
|
|
205
121
|
|
|
206
|
-
//
|
|
207
|
-
|
|
122
|
+
// MARK: - Present NavigationViewController
|
|
123
|
+
private func presentNavigationViewController(with navigationRoutes: NavigationRoutes) {
|
|
124
|
+
guard let provider = mapboxNavigationProvider,
|
|
125
|
+
let mapboxNavigation = mapboxNavigation else { return }
|
|
126
|
+
|
|
127
|
+
tearDownNavigationViewController()
|
|
128
|
+
|
|
129
|
+
// FIX: In Navigation SDK v3, NavigationDayStyle / NavigationNightStyle
|
|
130
|
+
// no longer exist. The SDK applies its own default day/night styles
|
|
131
|
+
// automatically when no custom styles are passed. Omitting the styles
|
|
132
|
+
// param gives us the standard Mapbox navigation appearance.
|
|
133
|
+
let navigationOptions = NavigationOptions(
|
|
134
|
+
mapboxNavigation: mapboxNavigation,
|
|
135
|
+
voiceController: provider.routeVoiceController,
|
|
136
|
+
eventsManager: provider.eventsManager()
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
let vc = NavigationViewController(
|
|
140
|
+
navigationRoutes: navigationRoutes,
|
|
141
|
+
navigationOptions: navigationOptions
|
|
142
|
+
)
|
|
143
|
+
vc.delegate = self
|
|
144
|
+
vc.routeLineTracksTraversal = true
|
|
145
|
+
|
|
146
|
+
// Mute: FIX — routeVoiceController is non-optional in v3, no ? needed
|
|
147
|
+
if mute {
|
|
148
|
+
provider.routeVoiceController.speechSynthesizer.muted = true
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Tap banner → emit full steps list
|
|
152
|
+
attachManeuverBannerTapHandler(to: vc)
|
|
153
|
+
|
|
154
|
+
addSubview(vc.view)
|
|
155
|
+
vc.view.translatesAutoresizingMaskIntoConstraints = false
|
|
156
|
+
NSLayoutConstraint.activate([
|
|
157
|
+
vc.view.topAnchor.constraint(equalTo: topAnchor),
|
|
158
|
+
vc.view.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
159
|
+
vc.view.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
160
|
+
vc.view.trailingAnchor.constraint(equalTo: trailingAnchor)
|
|
161
|
+
])
|
|
162
|
+
|
|
163
|
+
if let parentVC = findParentViewController() {
|
|
164
|
+
parentVC.addChild(vc)
|
|
165
|
+
vc.didMove(toParent: parentVC)
|
|
166
|
+
}
|
|
208
167
|
|
|
209
|
-
|
|
210
|
-
if mute {
|
|
211
|
-
provider.routeVoiceController?.speechSynthesizer.muted = true
|
|
168
|
+
self.navigationViewController = vc
|
|
212
169
|
}
|
|
213
170
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
addSubview(vc.view)
|
|
220
|
-
vc.view.translatesAutoresizingMaskIntoConstraints = false
|
|
221
|
-
NSLayoutConstraint.activate([
|
|
222
|
-
vc.view.topAnchor.constraint(equalTo: topAnchor),
|
|
223
|
-
vc.view.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
224
|
-
vc.view.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
225
|
-
vc.view.trailingAnchor.constraint(equalTo: trailingAnchor)
|
|
226
|
-
])
|
|
227
|
-
|
|
228
|
-
if let parentVC = self.findParentViewController() {
|
|
229
|
-
parentVC.addChild(vc)
|
|
230
|
-
vc.didMove(toParent: parentVC)
|
|
171
|
+
private func attachManeuverBannerTapHandler(to vc: NavigationViewController) {
|
|
172
|
+
let tap = UITapGestureRecognizer(target: self, action: #selector(handleManeuverBannerTap))
|
|
173
|
+
vc.navigationView.topBannerContainerView.addGestureRecognizer(tap)
|
|
174
|
+
vc.navigationView.topBannerContainerView.isUserInteractionEnabled = true
|
|
231
175
|
}
|
|
232
176
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
private func attachManeuverBannerTapHandler(to vc: NavigationViewController) {
|
|
237
|
-
// The top banner container is `vc.navigationView.topBannerContainerView`
|
|
238
|
-
// (per public API on NavigationView). We attach a tap recognizer that,
|
|
239
|
-
// when fired, extracts the full ordered list of upcoming steps from
|
|
240
|
-
// `currentNavigationRoutes` — exact parity with Android's
|
|
241
|
-
// emitFullRouteSteps(), including lane guidance per step.
|
|
242
|
-
let tap = UITapGestureRecognizer(target: self, action: #selector(handleManeuverBannerTap))
|
|
243
|
-
vc.navigationView.topBannerContainerView.addGestureRecognizer(tap)
|
|
244
|
-
vc.navigationView.topBannerContainerView.isUserInteractionEnabled = true
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
@objc private func handleManeuverBannerTap() {
|
|
248
|
-
emitFullRouteSteps()
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// MARK: - Full route steps list (parity with Android emitFullRouteSteps())
|
|
252
|
-
|
|
253
|
-
private func emitFullRouteSteps() {
|
|
254
|
-
guard let navigationRoutes = currentNavigationRoutes else {
|
|
255
|
-
onManeuverBannerPressed(["steps": []])
|
|
256
|
-
return
|
|
177
|
+
@objc private func handleManeuverBannerTap() {
|
|
178
|
+
emitFullRouteSteps()
|
|
257
179
|
}
|
|
258
180
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
181
|
+
// MARK: - Full route steps list
|
|
182
|
+
// FIX: removed lane guidance extraction via intersection.approachLanes /
|
|
183
|
+
// intersection.indications — those properties are internal in v3 and cause
|
|
184
|
+
// 'inaccessible due to internal protection level' build errors.
|
|
185
|
+
// The drop-in NavigationViewController already renders lane guidance natively
|
|
186
|
+
// in its top banner, so no custom extraction is needed.
|
|
187
|
+
private func emitFullRouteSteps() {
|
|
188
|
+
guard let navigationRoutes = currentNavigationRoutes else {
|
|
189
|
+
onManeuverBannerPressed(["steps": []])
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
let route = navigationRoutes.mainRoute.route
|
|
194
|
+
var stepsPayload: [[String: Any]] = []
|
|
195
|
+
|
|
196
|
+
for leg in route.legs {
|
|
197
|
+
for step in leg.steps {
|
|
198
|
+
stepsPayload.append([
|
|
199
|
+
"instruction": step.instructions,
|
|
200
|
+
"distanceMeters": step.distance,
|
|
201
|
+
"durationSeconds": step.expectedTravelTime,
|
|
202
|
+
"maneuverType": String(describing: step.maneuverType),
|
|
203
|
+
"maneuverModifier": step.maneuverDirection.map { String(describing: $0) } ?? "",
|
|
204
|
+
"roadName": step.names?.first ?? "",
|
|
205
|
+
"laneInstructions": [] // lane data surfaced natively in the banner
|
|
206
|
+
])
|
|
277
207
|
}
|
|
278
|
-
}
|
|
279
208
|
}
|
|
280
209
|
|
|
281
|
-
|
|
282
|
-
"instruction": step.instructions,
|
|
283
|
-
"distanceMeters": step.distance,
|
|
284
|
-
"durationSeconds": step.expectedTravelTime,
|
|
285
|
-
"maneuverType": String(describing: step.maneuverType),
|
|
286
|
-
"maneuverModifier": step.maneuverDirection.map { String(describing: $0) } ?? "",
|
|
287
|
-
"roadName": step.names?.first ?? "",
|
|
288
|
-
"laneInstructions": laneData
|
|
289
|
-
])
|
|
290
|
-
}
|
|
210
|
+
onManeuverBannerPressed(["steps": stepsPayload])
|
|
291
211
|
}
|
|
292
212
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
vc.removeFromParent()
|
|
303
|
-
navigationViewController = nil
|
|
304
|
-
currentNavigationRoutes = nil
|
|
305
|
-
}
|
|
213
|
+
// MARK: - Teardown
|
|
214
|
+
private func tearDownNavigationViewController() {
|
|
215
|
+
guard let vc = navigationViewController else { return }
|
|
216
|
+
vc.willMove(toParent: nil)
|
|
217
|
+
vc.view.removeFromSuperview()
|
|
218
|
+
vc.removeFromParent()
|
|
219
|
+
navigationViewController = nil
|
|
220
|
+
currentNavigationRoutes = nil
|
|
221
|
+
}
|
|
306
222
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
223
|
+
private func cancelNavigation() {
|
|
224
|
+
tearDownNavigationViewController()
|
|
225
|
+
onNavigationCancelled([:])
|
|
226
|
+
}
|
|
311
227
|
|
|
312
|
-
|
|
228
|
+
private func findParentViewController() -> UIViewController? {
|
|
229
|
+
var responder: UIResponder? = self
|
|
230
|
+
while let r = responder {
|
|
231
|
+
if let vc = r as? UIViewController { return vc }
|
|
232
|
+
responder = r.next
|
|
233
|
+
}
|
|
234
|
+
return nil
|
|
235
|
+
}
|
|
313
236
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
237
|
+
// MARK: - Prop setters
|
|
238
|
+
func setCoordinates(_ coords: [[String: Double]]) {
|
|
239
|
+
coordinates = coords
|
|
240
|
+
if coords.count >= 2 { fetchRoutes() }
|
|
241
|
+
}
|
|
242
|
+
func setWaypointIndices(_ indices: [Int]?) { waypointIndices = indices }
|
|
243
|
+
func setLanguage(_ lang: String?) { language = lang }
|
|
244
|
+
func setVoiceUnits(_ units: String?) { voiceUnits = units }
|
|
245
|
+
func setNavigationProfile(_ profile: String?) { navigationProfile = profile }
|
|
246
|
+
func setExcludeTypes(_ types: [String]?) { excludeTypes = types }
|
|
247
|
+
func setMapStyle(_ style: String?) { mapStyle = style }
|
|
248
|
+
func setMute(_ shouldMute: Bool) {
|
|
249
|
+
mute = shouldMute
|
|
250
|
+
// FIX: routeVoiceController is non-optional in v3 — no ? needed
|
|
251
|
+
mapboxNavigationProvider?.routeVoiceController.speechSynthesizer.muted = shouldMute
|
|
252
|
+
}
|
|
253
|
+
func setMaxHeight(_ height: Double?) { maxHeight = height }
|
|
254
|
+
func setMaxWidth(_ width: Double?) { maxWidth = width }
|
|
255
|
+
func setUseMapMatching(_ use: Bool) { useMapMatching = use }
|
|
256
|
+
func setCustomRasterTileUrl(_ url: String?) { customRasterTileUrl = url }
|
|
257
|
+
func setCustomRasterAboveLayerId(_ layerId: String?) { customRasterAboveLayerId = layerId }
|
|
258
|
+
|
|
259
|
+
// MARK: - Lifecycle
|
|
260
|
+
public override func removeFromSuperview() {
|
|
261
|
+
routeRequestTask?.cancel()
|
|
262
|
+
tearDownNavigationViewController()
|
|
263
|
+
super.removeFromSuperview()
|
|
319
264
|
}
|
|
320
|
-
return nil
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// MARK: - Prop setters (parity with Android setters)
|
|
324
|
-
|
|
325
|
-
func setCoordinates(_ coords: [[String: Double]]) {
|
|
326
|
-
coordinates = coords
|
|
327
|
-
if coords.count >= 2 { fetchRoutes() }
|
|
328
|
-
}
|
|
329
|
-
func setWaypointIndices(_ indices: [Int]?) { waypointIndices = indices }
|
|
330
|
-
func setLanguage(_ lang: String?) { language = lang }
|
|
331
|
-
func setVoiceUnits(_ units: String?) { voiceUnits = units }
|
|
332
|
-
func setNavigationProfile(_ profile: String?) { navigationProfile = profile }
|
|
333
|
-
func setExcludeTypes(_ types: [String]?) { excludeTypes = types }
|
|
334
|
-
func setMapStyle(_ style: String?) { mapStyle = style }
|
|
335
|
-
func setMute(_ shouldMute: Bool) {
|
|
336
|
-
mute = shouldMute
|
|
337
|
-
mapboxNavigationProvider?.routeVoiceController?.speechSynthesizer.muted = shouldMute
|
|
338
|
-
}
|
|
339
|
-
func setMaxHeight(_ height: Double?) { maxHeight = height }
|
|
340
|
-
func setMaxWidth(_ width: Double?) { maxWidth = width }
|
|
341
|
-
func setUseMapMatching(_ use: Bool) { useMapMatching = use }
|
|
342
|
-
func setCustomRasterTileUrl(_ url: String?) { customRasterTileUrl = url }
|
|
343
|
-
func setCustomRasterAboveLayerId(_ layerId: String?) { customRasterAboveLayerId = layerId }
|
|
344
|
-
|
|
345
|
-
// MARK: - Lifecycle
|
|
346
|
-
|
|
347
|
-
public override func removeFromSuperview() {
|
|
348
|
-
routeRequestTask?.cancel()
|
|
349
|
-
tearDownNavigationViewController()
|
|
350
|
-
super.removeFromSuperview()
|
|
351
|
-
}
|
|
352
265
|
}
|
|
353
266
|
|
|
354
267
|
// MARK: - NavigationViewControllerDelegate
|
|
355
|
-
//
|
|
356
|
-
// Parity with Android's RouteProgressObserver / RoutesObserver / onArrival.
|
|
357
|
-
|
|
358
268
|
extension ExpoMapboxNavigationView: NavigationViewControllerDelegate {
|
|
359
269
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
270
|
+
public func navigationViewController(
|
|
271
|
+
_ navigationViewController: NavigationViewController,
|
|
272
|
+
didUpdate progress: RouteProgress,
|
|
273
|
+
with location: CLLocation,
|
|
274
|
+
rawLocation: CLLocation
|
|
275
|
+
) {
|
|
276
|
+
onRouteProgressChanged([
|
|
277
|
+
"distanceRemaining": progress.distanceRemaining,
|
|
278
|
+
"durationRemaining": progress.durationRemaining,
|
|
279
|
+
"distanceTraveled": progress.distanceTraveled,
|
|
280
|
+
"fractionTraveled": progress.fractionTraveled,
|
|
281
|
+
"currentStepDistanceRemaining":
|
|
282
|
+
progress.currentLegProgress.currentStepProgress.distanceRemaining
|
|
283
|
+
])
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
public func navigationViewController(
|
|
287
|
+
_ navigationViewController: NavigationViewController,
|
|
288
|
+
didArriveAt waypoint: Waypoint
|
|
289
|
+
) -> Bool {
|
|
290
|
+
onArrival([:])
|
|
291
|
+
return true
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
public func navigationViewControllerDidDismiss(
|
|
295
|
+
_ navigationViewController: NavigationViewController,
|
|
296
|
+
byCanceling canceled: Bool
|
|
297
|
+
) {
|
|
298
|
+
if canceled {
|
|
299
|
+
cancelNavigation()
|
|
300
|
+
} else {
|
|
301
|
+
onNavigationFinished([:])
|
|
302
|
+
tearDownNavigationViewController()
|
|
303
|
+
}
|
|
392
304
|
}
|
|
393
|
-
}
|
|
394
305
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jacques_gordon/expo-mapbox-navigation",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.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",
|
package/src/index.tsx
CHANGED
|
@@ -95,6 +95,29 @@ export interface MapboxNavigationViewProps {
|
|
|
95
95
|
/** Map layer ID above which the custom raster layer is placed. */
|
|
96
96
|
customRasterAboveLayerId?: string;
|
|
97
97
|
|
|
98
|
+
// ── Color customization ──────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Background color of the turn-by-turn instruction banner, as a hex string
|
|
102
|
+
* (e.g. "#1A73E8"). Uses the SDK's official ManeuverViewOptions API.
|
|
103
|
+
*/
|
|
104
|
+
maneuverBackgroundColorDay?: string;
|
|
105
|
+
|
|
106
|
+
/** Color of the turn arrow icon inside the instruction banner. */
|
|
107
|
+
maneuverTurnIconColor?: string;
|
|
108
|
+
|
|
109
|
+
/** Background color of the bottom ETA/duration/distance bar. */
|
|
110
|
+
etaBarBackgroundColor?: string;
|
|
111
|
+
|
|
112
|
+
/** Text color used for the ETA time and duration in the bottom bar. */
|
|
113
|
+
etaTextColor?: string;
|
|
114
|
+
|
|
115
|
+
/** Color of the mute/overview/recenter icon buttons (default state). */
|
|
116
|
+
iconButtonColor?: string;
|
|
117
|
+
|
|
118
|
+
/** Color of the mute icon when voice guidance is muted. */
|
|
119
|
+
iconButtonMutedColor?: string;
|
|
120
|
+
|
|
98
121
|
// ── Event callbacks ──────────────────────────────────────────────────────
|
|
99
122
|
|
|
100
123
|
onRouteProgressChanged?: (event: { nativeEvent: RouteProgressEvent }) => void;
|