@jacques_gordon/expo-mapbox-navigation 2.0.9 → 2.1.0
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
|
@@ -13,12 +13,6 @@ android {
|
|
|
13
13
|
defaultConfig {
|
|
14
14
|
minSdkVersion safeExtGet("minSdkVersion", 24)
|
|
15
15
|
targetSdkVersion safeExtGet("targetSdkVersion", 35)
|
|
16
|
-
|
|
17
|
-
externalNativeBuild {
|
|
18
|
-
cmake {
|
|
19
|
-
arguments "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
16
|
ndk {
|
|
23
17
|
abiFilters "arm64-v8a", "x86_64"
|
|
24
18
|
}
|
|
@@ -63,22 +57,26 @@ dependencies {
|
|
|
63
57
|
implementation project(':expo-modules-core')
|
|
64
58
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${safeExtGet('kotlinVersion', '1.9.0')}"
|
|
65
59
|
|
|
66
|
-
// Mapbox Navigation SDK v3
|
|
60
|
+
// Mapbox Navigation SDK v3
|
|
67
61
|
implementation "com.mapbox.navigationcore:android:${MAPBOX_NAV_VERSION}"
|
|
68
62
|
|
|
69
|
-
// Mapbox Maps SDK
|
|
63
|
+
// Mapbox Maps SDK
|
|
70
64
|
implementation("com.mapbox.maps:android:${MAPBOX_MAPS_MIN_VERSION}") {
|
|
71
65
|
transitive = true
|
|
72
66
|
}
|
|
73
67
|
|
|
74
|
-
//
|
|
75
|
-
// MapboxManeuverView — turn-by-turn banner
|
|
68
|
+
// Navigation UI Components (ManeuverView, SpeedInfoView)
|
|
76
69
|
implementation "com.mapbox.navigationcore:ui-components:${MAPBOX_NAV_VERSION}"
|
|
77
|
-
|
|
70
|
+
|
|
71
|
+
// TripData (ManeuverApi, SpeedInfoApi, TripProgressApi)
|
|
78
72
|
implementation "com.mapbox.navigationcore:tripdata:${MAPBOX_NAV_VERSION}"
|
|
79
|
-
|
|
73
|
+
|
|
74
|
+
// UI Maps (NavigationStyles, NavigationLocationProvider, NavigationCamera)
|
|
80
75
|
implementation "com.mapbox.navigationcore:ui-maps:${MAPBOX_NAV_VERSION}"
|
|
81
76
|
|
|
77
|
+
// ConstraintLayout — required because MapboxManeuverView extends ConstraintLayout
|
|
78
|
+
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
|
79
|
+
|
|
82
80
|
// AndroidX
|
|
83
81
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
|
|
84
82
|
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
|
|
@@ -3,20 +3,19 @@ package expo.modules.mapboxnavigation
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
4
|
import android.content.Context
|
|
5
5
|
import android.graphics.Color
|
|
6
|
-
import android.os.Build
|
|
7
6
|
import android.util.Log
|
|
8
7
|
import android.view.Gravity
|
|
9
8
|
import android.view.View
|
|
10
9
|
import android.widget.FrameLayout
|
|
11
10
|
import android.widget.LinearLayout
|
|
12
11
|
import android.widget.TextView
|
|
13
|
-
import androidx.core.content.ContextCompat
|
|
14
12
|
import com.mapbox.api.directions.v5.models.RouteOptions
|
|
13
|
+
import com.mapbox.common.location.Location
|
|
15
14
|
import com.mapbox.geojson.Point
|
|
16
15
|
import com.mapbox.maps.MapView
|
|
17
|
-
import com.mapbox.maps.Style
|
|
18
16
|
import com.mapbox.maps.plugin.animation.camera
|
|
19
17
|
import com.mapbox.maps.plugin.locationcomponent.location
|
|
18
|
+
import com.mapbox.navigation.base.TimeFormat
|
|
20
19
|
import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions
|
|
21
20
|
import com.mapbox.navigation.base.formatter.DistanceFormatterOptions
|
|
22
21
|
import com.mapbox.navigation.base.options.NavigationOptions
|
|
@@ -28,6 +27,7 @@ import com.mapbox.navigation.core.MapboxNavigation
|
|
|
28
27
|
import com.mapbox.navigation.core.MapboxNavigationProvider
|
|
29
28
|
import com.mapbox.navigation.core.directions.session.RoutesObserver
|
|
30
29
|
import com.mapbox.navigation.core.directions.session.RoutesUpdatedResult
|
|
30
|
+
import com.mapbox.navigation.core.formatter.MapboxDistanceFormatter
|
|
31
31
|
import com.mapbox.navigation.core.trip.session.LocationMatcherResult
|
|
32
32
|
import com.mapbox.navigation.core.trip.session.LocationObserver
|
|
33
33
|
import com.mapbox.navigation.core.trip.session.RouteProgressObserver
|
|
@@ -47,6 +47,9 @@ import com.mapbox.navigation.ui.maps.camera.NavigationCamera
|
|
|
47
47
|
import com.mapbox.navigation.ui.maps.camera.data.MapboxNavigationViewportDataSource
|
|
48
48
|
import com.mapbox.navigation.ui.maps.camera.transition.NavigationCameraTransitionOptions
|
|
49
49
|
import com.mapbox.navigation.ui.maps.location.NavigationLocationProvider
|
|
50
|
+
import com.mapbox.navigation.ui.maps.route.arrow.api.MapboxRouteArrowApi
|
|
51
|
+
import com.mapbox.navigation.ui.maps.route.arrow.api.MapboxRouteArrowView
|
|
52
|
+
import com.mapbox.navigation.ui.maps.route.arrow.model.RouteArrowOptions
|
|
50
53
|
import com.mapbox.navigation.ui.maps.route.line.api.MapboxRouteLineApi
|
|
51
54
|
import com.mapbox.navigation.ui.maps.route.line.api.MapboxRouteLineView
|
|
52
55
|
import com.mapbox.navigation.ui.maps.route.line.model.MapboxRouteLineApiOptions
|
|
@@ -62,7 +65,7 @@ private const val TAG = "ExpoMapboxNavigation"
|
|
|
62
65
|
class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
63
66
|
ExpoView(context, appContext) {
|
|
64
67
|
|
|
65
|
-
// ── EventDispatchers
|
|
68
|
+
// ── EventDispatchers (correct Expo Modules API for View events) ───────────
|
|
66
69
|
private val onRouteProgressChanged by EventDispatcher()
|
|
67
70
|
private val onRoutesReady by EventDispatcher()
|
|
68
71
|
private val onNavigationFinished by EventDispatcher()
|
|
@@ -70,31 +73,30 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
70
73
|
private val onRoutesFailed by EventDispatcher()
|
|
71
74
|
private val onArrival by EventDispatcher()
|
|
72
75
|
|
|
73
|
-
// ──
|
|
76
|
+
// ── Views ─────────────────────────────────────────────────────────────────
|
|
74
77
|
private val mapView: MapView = MapView(context)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
private
|
|
78
|
-
private
|
|
79
|
-
private
|
|
80
|
-
private
|
|
81
|
-
private
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
private lateinit var
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
private var
|
|
88
|
-
private var
|
|
89
|
-
private var
|
|
90
|
-
private var
|
|
91
|
-
private
|
|
92
|
-
private var
|
|
93
|
-
private var speedInfoApi: MapboxSpeedInfoApi? = null
|
|
94
|
-
private var tripProgressApi: MapboxTripProgressApi? = null
|
|
78
|
+
private var maneuverView: MapboxManeuverView? = null
|
|
79
|
+
private var speedInfoView: MapboxSpeedInfoView? = null
|
|
80
|
+
private var tripProgressView: MapboxTripProgressView? = null
|
|
81
|
+
private var tvEtaTime: TextView? = null
|
|
82
|
+
private var tvDuration: TextView? = null
|
|
83
|
+
private var tvDistance: TextView? = null
|
|
84
|
+
private var etaBar: LinearLayout? = null
|
|
85
|
+
|
|
86
|
+
// ── Navigation APIs (from official TurnByTurnExperienceActivity) ──────────
|
|
87
|
+
private lateinit var navigationCamera: NavigationCamera
|
|
88
|
+
private lateinit var viewportDataSource: MapboxNavigationViewportDataSource
|
|
89
|
+
private lateinit var routeLineApi: MapboxRouteLineApi
|
|
90
|
+
private lateinit var routeLineView: MapboxRouteLineView
|
|
91
|
+
private lateinit var maneuverApi: MapboxManeuverApi
|
|
92
|
+
private lateinit var tripProgressApi: MapboxTripProgressApi
|
|
93
|
+
private lateinit var speedInfoApi: MapboxSpeedInfoApi
|
|
94
|
+
private val routeArrowApi = MapboxRouteArrowApi()
|
|
95
|
+
private lateinit var routeArrowView: MapboxRouteArrowView
|
|
95
96
|
private val navigationLocationProvider = NavigationLocationProvider()
|
|
97
|
+
private var mapboxNavigation: MapboxNavigation? = null
|
|
96
98
|
|
|
97
|
-
// ── Night mode
|
|
99
|
+
// ── Night mode ────────────────────────────────────────────────────────────
|
|
98
100
|
private var isNightMode = false
|
|
99
101
|
|
|
100
102
|
// ── Props ─────────────────────────────────────────────────────────────────
|
|
@@ -113,250 +115,250 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
113
115
|
private var customRasterAboveLayerId: String? = null
|
|
114
116
|
|
|
115
117
|
init {
|
|
116
|
-
|
|
118
|
+
buildUI()
|
|
119
|
+
initAPIs()
|
|
117
120
|
setupNavigation()
|
|
118
121
|
}
|
|
119
122
|
|
|
120
123
|
// ─────────────────────────────────────────────────────────────────────────
|
|
121
|
-
// Build
|
|
124
|
+
// 1. Build programmatic UI
|
|
122
125
|
// ─────────────────────────────────────────────────────────────────────────
|
|
123
|
-
private fun
|
|
126
|
+
private fun buildUI() {
|
|
127
|
+
val dp = context.resources.displayMetrics.density
|
|
124
128
|
val root = FrameLayout(context).apply {
|
|
125
129
|
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
|
126
130
|
}
|
|
127
131
|
|
|
128
|
-
// MapView
|
|
129
|
-
mapView
|
|
132
|
+
// Full-screen MapView
|
|
133
|
+
root.addView(mapView, FrameLayout.LayoutParams(
|
|
130
134
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
131
135
|
FrameLayout.LayoutParams.MATCH_PARENT
|
|
132
|
-
)
|
|
133
|
-
root.addView(mapView)
|
|
134
|
-
|
|
135
|
-
val dp = context.resources.displayMetrics.density
|
|
136
|
-
|
|
137
|
-
// ── Maneuver banner (top) ─────────────────────────────────────────────
|
|
138
|
-
maneuverView = MapboxManeuverView(context).apply {
|
|
139
|
-
layoutParams = FrameLayout.LayoutParams(
|
|
140
|
-
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
141
|
-
FrameLayout.LayoutParams.WRAP_CONTENT
|
|
142
|
-
).also { it.gravity = Gravity.TOP }
|
|
143
|
-
visibility = View.GONE
|
|
144
|
-
}
|
|
145
|
-
root.addView(maneuverView)
|
|
146
|
-
|
|
147
|
-
// ── Speed info (bottom-left, Waze style) ──────────────────────────────
|
|
148
|
-
speedInfoView = MapboxSpeedInfoView(context).apply {
|
|
149
|
-
layoutParams = FrameLayout.LayoutParams(
|
|
150
|
-
FrameLayout.LayoutParams.WRAP_CONTENT,
|
|
151
|
-
FrameLayout.LayoutParams.WRAP_CONTENT
|
|
152
|
-
).also {
|
|
153
|
-
it.gravity = Gravity.BOTTOM or Gravity.START
|
|
154
|
-
it.setMargins(
|
|
155
|
-
(16 * dp).toInt(), 0,
|
|
156
|
-
0, (120 * dp).toInt()
|
|
157
|
-
)
|
|
158
|
-
}
|
|
159
|
-
visibility = View.GONE
|
|
160
|
-
}
|
|
161
|
-
root.addView(speedInfoView)
|
|
136
|
+
))
|
|
162
137
|
|
|
163
|
-
//
|
|
164
|
-
|
|
138
|
+
// ManeuverView — top banner
|
|
139
|
+
// NOTE: MapboxManeuverView extends ConstraintLayout, cast to View for addView()
|
|
140
|
+
val mv = MapboxManeuverView(context)
|
|
141
|
+
val mvParams = FrameLayout.LayoutParams(
|
|
142
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
143
|
+
FrameLayout.LayoutParams.WRAP_CONTENT
|
|
144
|
+
)
|
|
145
|
+
mvParams.gravity = Gravity.TOP
|
|
146
|
+
mv.visibility = View.INVISIBLE // matches official example (INVISIBLE not GONE)
|
|
147
|
+
root.addView(mv as View, mvParams)
|
|
148
|
+
maneuverView = mv
|
|
149
|
+
|
|
150
|
+
// SpeedInfoView — bottom-left
|
|
151
|
+
val siv = MapboxSpeedInfoView(context)
|
|
152
|
+
val sivParams = FrameLayout.LayoutParams(
|
|
153
|
+
FrameLayout.LayoutParams.WRAP_CONTENT,
|
|
154
|
+
FrameLayout.LayoutParams.WRAP_CONTENT
|
|
155
|
+
)
|
|
156
|
+
sivParams.gravity = Gravity.BOTTOM or Gravity.START
|
|
157
|
+
sivParams.setMargins((16 * dp).toInt(), 0, 0, (100 * dp).toInt())
|
|
158
|
+
siv.visibility = View.GONE
|
|
159
|
+
root.addView(siv as View, sivParams)
|
|
160
|
+
speedInfoView = siv
|
|
161
|
+
|
|
162
|
+
// TripProgressView — embedded in bottom bar
|
|
163
|
+
val tpv = MapboxTripProgressView(context)
|
|
164
|
+
tripProgressView = tpv
|
|
165
|
+
|
|
166
|
+
// ETA bottom bar
|
|
167
|
+
val bar = LinearLayout(context).apply {
|
|
165
168
|
orientation = LinearLayout.HORIZONTAL
|
|
166
169
|
setBackgroundColor(Color.WHITE)
|
|
167
170
|
elevation = 8 * dp
|
|
168
|
-
|
|
169
|
-
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
170
|
-
(80 * dp).toInt()
|
|
171
|
-
).also { it.gravity = Gravity.BOTTOM }
|
|
171
|
+
visibility = View.INVISIBLE
|
|
172
172
|
gravity = Gravity.CENTER_VERTICAL
|
|
173
173
|
setPadding(
|
|
174
174
|
(16 * dp).toInt(), (8 * dp).toInt(),
|
|
175
175
|
(16 * dp).toInt(), (8 * dp).toInt()
|
|
176
176
|
)
|
|
177
|
-
visibility = View.GONE
|
|
178
177
|
}
|
|
178
|
+
val barParams = FrameLayout.LayoutParams(
|
|
179
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
180
|
+
(80 * dp).toInt()
|
|
181
|
+
)
|
|
182
|
+
barParams.gravity = Gravity.BOTTOM
|
|
179
183
|
|
|
180
|
-
|
|
181
|
-
tvEtaTime = TextView(context).apply {
|
|
184
|
+
val etaTime = TextView(context).apply {
|
|
182
185
|
textSize = 22f
|
|
183
186
|
setTextColor(Color.BLACK)
|
|
184
187
|
typeface = android.graphics.Typeface.DEFAULT_BOLD
|
|
185
|
-
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
|
|
186
188
|
}
|
|
187
|
-
|
|
189
|
+
bar.addView(etaTime, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f))
|
|
190
|
+
tvEtaTime = etaTime
|
|
188
191
|
|
|
189
|
-
|
|
190
|
-
val centerLayout = LinearLayout(context).apply {
|
|
192
|
+
val center = LinearLayout(context).apply {
|
|
191
193
|
orientation = LinearLayout.VERTICAL
|
|
192
194
|
gravity = Gravity.CENTER
|
|
193
|
-
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 2f)
|
|
194
|
-
}
|
|
195
|
-
tvDuration = TextView(context).apply {
|
|
196
|
-
textSize = 16f
|
|
197
|
-
setTextColor(Color.DKGRAY)
|
|
198
|
-
gravity = Gravity.CENTER
|
|
199
195
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
// Cancel button
|
|
210
|
-
btnCancel = TextView(context).apply {
|
|
196
|
+
val dur = TextView(context).apply { textSize = 16f; setTextColor(Color.DKGRAY); gravity = Gravity.CENTER }
|
|
197
|
+
val dist = TextView(context).apply { textSize = 13f; setTextColor(Color.GRAY); gravity = Gravity.CENTER }
|
|
198
|
+
center.addView(dur)
|
|
199
|
+
center.addView(dist)
|
|
200
|
+
bar.addView(center, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 2f))
|
|
201
|
+
tvDuration = dur
|
|
202
|
+
tvDistance = dist
|
|
203
|
+
|
|
204
|
+
val cancelBtn = TextView(context).apply {
|
|
211
205
|
text = "✕"
|
|
212
206
|
textSize = 22f
|
|
213
207
|
setTextColor(Color.DKGRAY)
|
|
214
208
|
gravity = Gravity.END or Gravity.CENTER_VERTICAL
|
|
215
|
-
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
|
|
216
209
|
setOnClickListener { cancelNavigation() }
|
|
217
210
|
}
|
|
218
|
-
|
|
219
|
-
|
|
211
|
+
bar.addView(cancelBtn, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f))
|
|
212
|
+
etaBar = bar
|
|
213
|
+
root.addView(bar, barParams)
|
|
220
214
|
|
|
221
215
|
addView(root)
|
|
222
216
|
}
|
|
223
217
|
|
|
224
218
|
// ─────────────────────────────────────────────────────────────────────────
|
|
225
|
-
//
|
|
219
|
+
// 2. Init APIs (from official example — exact same pattern)
|
|
226
220
|
// ─────────────────────────────────────────────────────────────────────────
|
|
227
|
-
private fun
|
|
228
|
-
val
|
|
229
|
-
mapboxNavigation = MapboxNavigationProvider.create(navigationOptions)
|
|
221
|
+
private fun initAPIs() {
|
|
222
|
+
val distanceFormatterOptions = DistanceFormatterOptions.Builder(context).build()
|
|
230
223
|
|
|
231
|
-
//
|
|
232
|
-
|
|
224
|
+
// EXACT same instantiation as TurnByTurnExperienceActivity
|
|
225
|
+
maneuverApi = MapboxManeuverApi(
|
|
226
|
+
MapboxDistanceFormatter(distanceFormatterOptions)
|
|
227
|
+
)
|
|
233
228
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
229
|
+
tripProgressApi = MapboxTripProgressApi(
|
|
230
|
+
TripProgressUpdateFormatter.Builder(context)
|
|
231
|
+
.distanceRemainingFormatter(DistanceRemainingFormatter(distanceFormatterOptions))
|
|
232
|
+
.timeRemainingFormatter(TimeRemainingFormatter(context))
|
|
233
|
+
.percentRouteTraveledFormatter(PercentDistanceTraveledFormatter())
|
|
234
|
+
.estimatedTimeToArrivalFormatter(
|
|
235
|
+
EstimatedTimeToArrivalFormatter(context, TimeFormat.NONE_SPECIFIED)
|
|
236
|
+
)
|
|
237
|
+
.build()
|
|
238
|
+
)
|
|
239
239
|
|
|
240
|
-
|
|
241
|
-
val mapboxMap = mapView.mapboxMap
|
|
240
|
+
speedInfoApi = MapboxSpeedInfoApi()
|
|
242
241
|
|
|
243
|
-
|
|
242
|
+
val routeLineViewOptions = MapboxRouteLineViewOptions.Builder(context)
|
|
243
|
+
.routeLineBelowLayerId("road-label-navigation")
|
|
244
|
+
.build()
|
|
244
245
|
routeLineApi = MapboxRouteLineApi(MapboxRouteLineApiOptions.Builder().build())
|
|
245
|
-
routeLineView = MapboxRouteLineView(
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
246
|
+
routeLineView = MapboxRouteLineView(routeLineViewOptions)
|
|
247
|
+
|
|
248
|
+
val routeArrowOptions = RouteArrowOptions.Builder(context).build()
|
|
249
|
+
routeArrowView = MapboxRouteArrowView(routeArrowOptions)
|
|
250
|
+
}
|
|
250
251
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
252
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
253
|
+
// 3. Setup Navigation
|
|
254
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
255
|
+
private fun setupNavigation() {
|
|
256
|
+
mapboxNavigation = MapboxNavigationProvider.create(
|
|
257
|
+
NavigationOptions.Builder(context).build()
|
|
257
258
|
)
|
|
258
259
|
|
|
259
|
-
|
|
260
|
-
|
|
260
|
+
val style = mapStyle ?: getAutoStyle()
|
|
261
|
+
mapView.mapboxMap.loadStyle(style) { mapStyle ->
|
|
261
262
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
maneuverApi = MapboxManeuverApi(
|
|
265
|
-
com.mapbox.navigation.base.formatter.MapboxDistanceFormatter(formatter)
|
|
266
|
-
)
|
|
263
|
+
// Route line layers must be initialized first
|
|
264
|
+
routeLineView.initializeLayers(mapStyle)
|
|
267
265
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
266
|
+
// Camera setup — EXACT same as official example
|
|
267
|
+
viewportDataSource = MapboxNavigationViewportDataSource(mapView.mapboxMap)
|
|
268
|
+
navigationCamera = NavigationCamera(
|
|
269
|
+
mapView.mapboxMap,
|
|
270
|
+
mapView.camera,
|
|
271
|
+
viewportDataSource
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
// Location puck
|
|
275
|
+
mapView.location.apply {
|
|
276
|
+
setLocationProvider(navigationLocationProvider)
|
|
277
|
+
enabled = true
|
|
278
|
+
}
|
|
276
279
|
|
|
277
|
-
|
|
278
|
-
mapView.location.apply {
|
|
279
|
-
setLocationProvider(navigationLocationProvider)
|
|
280
|
-
enabled = true
|
|
280
|
+
registerObservers()
|
|
281
281
|
}
|
|
282
282
|
}
|
|
283
283
|
|
|
284
284
|
// ─────────────────────────────────────────────────────────────────────────
|
|
285
|
-
//
|
|
285
|
+
// 4. Observers — based 100% on official TurnByTurnExperienceActivity
|
|
286
286
|
// ─────────────────────────────────────────────────────────────────────────
|
|
287
287
|
private fun registerObservers() {
|
|
288
288
|
val nav = mapboxNavigation ?: return
|
|
289
289
|
|
|
290
|
-
// Routes observer
|
|
290
|
+
// ── Routes observer ───────────────────────────────────────────────────
|
|
291
291
|
nav.registerRoutesObserver(object : RoutesObserver {
|
|
292
292
|
override fun onRoutesChanged(result: RoutesUpdatedResult) {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
routeLineApi
|
|
296
|
-
mapView.mapboxMap.style?.
|
|
297
|
-
routeLineView
|
|
293
|
+
if (result.navigationRoutes.isNotEmpty()) {
|
|
294
|
+
// Draw route line
|
|
295
|
+
routeLineApi.setNavigationRoutes(result.navigationRoutes) { value ->
|
|
296
|
+
mapView.mapboxMap.style?.apply {
|
|
297
|
+
routeLineView.renderRouteDrawData(this, value)
|
|
298
298
|
}
|
|
299
299
|
}
|
|
300
|
-
|
|
301
|
-
viewportDataSource
|
|
302
|
-
|
|
300
|
+
// Update camera viewport
|
|
301
|
+
viewportDataSource.onRouteChanged(result.navigationRoutes.first())
|
|
302
|
+
viewportDataSource.evaluate()
|
|
303
303
|
|
|
304
|
-
// Show UI
|
|
305
|
-
maneuverView
|
|
306
|
-
|
|
304
|
+
// Show navigation UI
|
|
305
|
+
maneuverView?.visibility = View.VISIBLE
|
|
306
|
+
etaBar?.visibility = View.VISIBLE
|
|
307
307
|
|
|
308
308
|
onRoutesReady(mapOf(
|
|
309
|
-
"routeCount" to
|
|
310
|
-
"distanceMeters" to (
|
|
311
|
-
"durationSeconds" to (
|
|
309
|
+
"routeCount" to result.navigationRoutes.size,
|
|
310
|
+
"distanceMeters" to (result.navigationRoutes.first().directionsRoute.distance() ?: 0.0),
|
|
311
|
+
"durationSeconds" to (result.navigationRoutes.first().directionsRoute.duration() ?: 0.0)
|
|
312
312
|
))
|
|
313
313
|
} else {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
314
|
+
// Clear route line
|
|
315
|
+
val style = mapView.mapboxMap.style
|
|
316
|
+
if (style != null) {
|
|
317
|
+
routeLineApi.clearRouteLine { value ->
|
|
318
|
+
routeLineView.renderClearRouteLineValue(style, value)
|
|
317
319
|
}
|
|
320
|
+
routeArrowView.render(style, routeArrowApi.clearArrows())
|
|
318
321
|
}
|
|
319
|
-
viewportDataSource
|
|
320
|
-
viewportDataSource
|
|
321
|
-
|
|
322
|
+
viewportDataSource.clearRouteData()
|
|
323
|
+
viewportDataSource.evaluate()
|
|
324
|
+
hideUI()
|
|
322
325
|
}
|
|
323
326
|
}
|
|
324
327
|
})
|
|
325
328
|
|
|
326
|
-
// Route progress observer
|
|
329
|
+
// ── Route progress observer ───────────────────────────────────────────
|
|
330
|
+
// EXACT same pattern as TurnByTurnExperienceActivity.routeProgressObserver
|
|
327
331
|
nav.registerRouteProgressObserver(RouteProgressObserver { routeProgress ->
|
|
328
|
-
|
|
329
|
-
viewportDataSource
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
332
|
+
// Update camera
|
|
333
|
+
viewportDataSource.onRouteProgressChanged(routeProgress)
|
|
334
|
+
viewportDataSource.evaluate()
|
|
335
|
+
|
|
336
|
+
// Draw maneuver arrow
|
|
337
|
+
val style = mapView.mapboxMap.style
|
|
338
|
+
if (style != null) {
|
|
339
|
+
val maneuverArrowResult = routeArrowApi.addUpcomingManeuverArrow(routeProgress)
|
|
340
|
+
routeArrowView.renderManeuverUpdate(style, maneuverArrowResult)
|
|
334
341
|
}
|
|
335
342
|
|
|
336
|
-
//
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
343
|
+
// Maneuver banner — EXACT same fold() pattern as official example
|
|
344
|
+
val maneuvers = maneuverApi.getManeuvers(routeProgress)
|
|
345
|
+
maneuvers.fold(
|
|
346
|
+
{ error -> Log.w(TAG, "Maneuver error: ${error.errorMessage}") },
|
|
347
|
+
{
|
|
348
|
+
maneuverView?.visibility = View.VISIBLE
|
|
349
|
+
maneuverView?.renderManeuvers(maneuvers)
|
|
341
350
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
val distKm = tripProgress.distanceRemaining / 1000.0
|
|
353
|
-
tvDistance.text = if (distKm >= 1.0)
|
|
354
|
-
String.format("%.1f km", distKm)
|
|
355
|
-
else
|
|
356
|
-
"${tripProgress.distanceRemaining.toInt()} m"
|
|
357
|
-
}
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
// Trip progress — EXACT same as official example
|
|
354
|
+
// getTripProgress returns TripProgressUpdateValue directly (not nullable)
|
|
355
|
+
val tripProgress = tripProgressApi.getTripProgress(routeProgress)
|
|
356
|
+
tripProgressView?.render(tripProgress)
|
|
357
|
+
|
|
358
|
+
// Update our custom ETA bar from tripProgress fields
|
|
359
|
+
updateEtaBar(tripProgress)
|
|
358
360
|
|
|
359
|
-
//
|
|
361
|
+
// Emit event
|
|
360
362
|
onRouteProgressChanged(mapOf(
|
|
361
363
|
"distanceRemaining" to routeProgress.distanceRemaining,
|
|
362
364
|
"durationRemaining" to routeProgress.durationRemaining,
|
|
@@ -368,9 +370,10 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
368
370
|
))
|
|
369
371
|
})
|
|
370
372
|
|
|
371
|
-
// Location observer
|
|
373
|
+
// ── Location observer ─────────────────────────────────────────────────
|
|
374
|
+
// EXACT same as official example
|
|
372
375
|
nav.registerLocationObserver(object : LocationObserver {
|
|
373
|
-
override fun onNewRawLocation(rawLocation:
|
|
376
|
+
override fun onNewRawLocation(rawLocation: Location) {}
|
|
374
377
|
|
|
375
378
|
override fun onNewLocationMatcherResult(locationMatcherResult: LocationMatcherResult) {
|
|
376
379
|
val enhancedLocation = locationMatcherResult.enhancedLocation
|
|
@@ -382,98 +385,117 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
382
385
|
)
|
|
383
386
|
|
|
384
387
|
// Update camera
|
|
385
|
-
viewportDataSource
|
|
386
|
-
viewportDataSource
|
|
388
|
+
viewportDataSource.onLocationChanged(enhancedLocation)
|
|
389
|
+
viewportDataSource.evaluate()
|
|
387
390
|
|
|
388
|
-
//
|
|
389
|
-
val
|
|
390
|
-
val
|
|
391
|
+
// Speed limit display
|
|
392
|
+
val formatterOptions = DistanceFormatterOptions.Builder(context).build()
|
|
393
|
+
val speedInfo = speedInfoApi.updatePostedAndCurrentSpeed(
|
|
391
394
|
locationMatcherResult,
|
|
392
|
-
|
|
395
|
+
formatterOptions
|
|
393
396
|
)
|
|
394
|
-
if (
|
|
395
|
-
speedInfoView
|
|
396
|
-
speedInfoView
|
|
397
|
+
if (speedInfo != null) {
|
|
398
|
+
speedInfoView?.visibility = View.VISIBLE
|
|
399
|
+
speedInfoView?.render(speedInfo)
|
|
397
400
|
} else {
|
|
398
|
-
speedInfoView
|
|
401
|
+
speedInfoView?.visibility = View.GONE
|
|
399
402
|
}
|
|
400
403
|
|
|
401
|
-
//
|
|
404
|
+
// Auto day/night switch
|
|
402
405
|
checkAndSwitchDayNight()
|
|
403
406
|
}
|
|
404
407
|
})
|
|
405
408
|
}
|
|
406
409
|
|
|
407
410
|
// ─────────────────────────────────────────────────────────────────────────
|
|
408
|
-
//
|
|
411
|
+
// 5. ETA bar update
|
|
412
|
+
// TripProgressUpdateValue fields confirmed from official tripdata API
|
|
413
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
414
|
+
private fun updateEtaBar(tripProgress: com.mapbox.navigation.tripdata.progress.model.TripProgressUpdateValue) {
|
|
415
|
+
// ETA arrival time
|
|
416
|
+
val etaSec = tripProgress.formatter.timeRemainingFormatter
|
|
417
|
+
.formatTime(tripProgress).toString()
|
|
418
|
+
// Calculate arrival clock time
|
|
419
|
+
val totalSec = tripProgress.totalTimeRemaining.toLong()
|
|
420
|
+
val cal = Calendar.getInstance().apply { add(Calendar.SECOND, totalSec.toInt()) }
|
|
421
|
+
tvEtaTime?.text = String.format("%02d:%02d",
|
|
422
|
+
cal.get(Calendar.HOUR_OF_DAY),
|
|
423
|
+
cal.get(Calendar.MINUTE)
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
// Duration remaining
|
|
427
|
+
val totalMin = (tripProgress.totalTimeRemaining / 60).toInt()
|
|
428
|
+
tvDuration?.text = if (totalMin >= 60)
|
|
429
|
+
"${totalMin / 60}h ${totalMin % 60}min"
|
|
430
|
+
else
|
|
431
|
+
"${totalMin} min"
|
|
432
|
+
|
|
433
|
+
// Distance remaining
|
|
434
|
+
val distM = tripProgress.distanceRemaining
|
|
435
|
+
tvDistance?.text = if (distM >= 1000)
|
|
436
|
+
String.format("%.1f km", distM / 1000.0)
|
|
437
|
+
else
|
|
438
|
+
"${distM.toInt()} m"
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
442
|
+
// 6. Day / Night auto switch
|
|
409
443
|
// ─────────────────────────────────────────────────────────────────────────
|
|
410
444
|
private fun getAutoStyle(): String {
|
|
411
445
|
val hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY)
|
|
412
|
-
return if (hour in 6..20)
|
|
413
|
-
|
|
414
|
-
else
|
|
415
|
-
NavigationStyles.NAVIGATION_NIGHT_STYLE
|
|
446
|
+
return if (hour in 6..20) NavigationStyles.NAVIGATION_DAY_STYLE
|
|
447
|
+
else NavigationStyles.NAVIGATION_NIGHT_STYLE
|
|
416
448
|
}
|
|
417
449
|
|
|
418
450
|
private fun checkAndSwitchDayNight() {
|
|
419
451
|
val hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY)
|
|
420
452
|
val shouldBeNight = hour !in 6..20
|
|
421
|
-
if (shouldBeNight
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
mapboxNavigation?.getNavigationRoutes()?.let { routes ->
|
|
433
|
-
if (routes.isNotEmpty()) {
|
|
434
|
-
api.setNavigationRoutes(routes) { value ->
|
|
435
|
-
routeLineView?.renderRouteDrawData(style, value)
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
}
|
|
453
|
+
if (shouldBeNight == isNightMode) return
|
|
454
|
+
isNightMode = shouldBeNight
|
|
455
|
+
|
|
456
|
+
val newStyle = if (shouldBeNight) NavigationStyles.NAVIGATION_NIGHT_STYLE
|
|
457
|
+
else NavigationStyles.NAVIGATION_DAY_STYLE
|
|
458
|
+
|
|
459
|
+
mapView.mapboxMap.loadStyle(newStyle) { style ->
|
|
460
|
+
routeLineView.initializeLayers(style)
|
|
461
|
+
mapboxNavigation?.getNavigationRoutes()?.takeIf { it.isNotEmpty() }?.let { routes ->
|
|
462
|
+
routeLineApi.setNavigationRoutes(routes) { value ->
|
|
463
|
+
routeLineView.renderRouteDrawData(style, value)
|
|
440
464
|
}
|
|
441
465
|
}
|
|
442
|
-
|
|
443
|
-
// Switch bottom bar color
|
|
444
|
-
etaBottomBar.setBackgroundColor(
|
|
445
|
-
if (shouldBeNight) Color.parseColor("#1E2433") else Color.WHITE
|
|
446
|
-
)
|
|
447
|
-
val textColor = if (shouldBeNight) Color.WHITE else Color.BLACK
|
|
448
|
-
tvEtaTime.setTextColor(textColor)
|
|
449
|
-
tvDuration.setTextColor(if (shouldBeNight) Color.LTGRAY else Color.DKGRAY)
|
|
450
|
-
tvDistance.setTextColor(if (shouldBeNight) Color.GRAY else Color.GRAY)
|
|
451
|
-
btnCancel.setTextColor(if (shouldBeNight) Color.LTGRAY else Color.DKGRAY)
|
|
452
466
|
}
|
|
467
|
+
|
|
468
|
+
etaBar?.setBackgroundColor(
|
|
469
|
+
if (shouldBeNight) Color.parseColor("#1E2433") else Color.WHITE
|
|
470
|
+
)
|
|
471
|
+
val tc = if (shouldBeNight) Color.WHITE else Color.BLACK
|
|
472
|
+
tvEtaTime?.setTextColor(tc)
|
|
473
|
+
tvDuration?.setTextColor(if (shouldBeNight) Color.LTGRAY else Color.DKGRAY)
|
|
474
|
+
tvDistance?.setTextColor(Color.GRAY)
|
|
453
475
|
}
|
|
454
476
|
|
|
455
477
|
// ─────────────────────────────────────────────────────────────────────────
|
|
456
|
-
// Cancel navigation
|
|
478
|
+
// 7. Cancel navigation
|
|
457
479
|
// ─────────────────────────────────────────────────────────────────────────
|
|
458
480
|
private fun cancelNavigation() {
|
|
459
|
-
mapboxNavigation?.setNavigationRoutes(
|
|
481
|
+
mapboxNavigation?.setNavigationRoutes(listOf())
|
|
460
482
|
mapboxNavigation?.stopTripSession()
|
|
461
|
-
|
|
483
|
+
hideUI()
|
|
462
484
|
onNavigationCancelled(mapOf<String, Any>())
|
|
463
485
|
}
|
|
464
486
|
|
|
465
|
-
private fun
|
|
466
|
-
maneuverView
|
|
467
|
-
speedInfoView
|
|
468
|
-
|
|
487
|
+
private fun hideUI() {
|
|
488
|
+
maneuverView?.visibility = View.INVISIBLE
|
|
489
|
+
speedInfoView?.visibility = View.GONE
|
|
490
|
+
etaBar?.visibility = View.INVISIBLE
|
|
469
491
|
}
|
|
470
492
|
|
|
471
493
|
// ─────────────────────────────────────────────────────────────────────────
|
|
472
|
-
// Issue #43
|
|
494
|
+
// 8. Issue #43 — safe camera following
|
|
473
495
|
// ─────────────────────────────────────────────────────────────────────────
|
|
474
496
|
private fun safeCameraFollowing() {
|
|
475
497
|
try {
|
|
476
|
-
navigationCamera
|
|
498
|
+
navigationCamera.requestNavigationCameraToFollowing(
|
|
477
499
|
stateTransitionOptions = NavigationCameraTransitionOptions.Builder()
|
|
478
500
|
.maxDuration(0)
|
|
479
501
|
.build()
|
|
@@ -484,7 +506,7 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
484
506
|
}
|
|
485
507
|
|
|
486
508
|
// ─────────────────────────────────────────────────────────────────────────
|
|
487
|
-
// Issue #31
|
|
509
|
+
// 9. Issue #31 — Voice units
|
|
488
510
|
// ─────────────────────────────────────────────────────────────────────────
|
|
489
511
|
private fun resolveVoiceUnits(): String {
|
|
490
512
|
return when (voiceUnits?.lowercase()) {
|
|
@@ -498,7 +520,7 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
498
520
|
}
|
|
499
521
|
|
|
500
522
|
// ─────────────────────────────────────────────────────────────────────────
|
|
501
|
-
// Route request
|
|
523
|
+
// 10. Route request
|
|
502
524
|
// ─────────────────────────────────────────────────────────────────────────
|
|
503
525
|
@SuppressLint("MissingPermission")
|
|
504
526
|
private fun fetchRoutes() {
|
|
@@ -515,16 +537,13 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
515
537
|
.language(locale.toLanguageTag())
|
|
516
538
|
.voiceUnits(resolveVoiceUnits())
|
|
517
539
|
.coordinatesList(points)
|
|
518
|
-
// maxspeed annotation required for speed limit display
|
|
519
540
|
.annotations("maxspeed,congestion,duration")
|
|
520
541
|
|
|
521
542
|
waypointIndices?.let { builder.waypointIndicesList(it) }
|
|
522
543
|
navigationProfile?.let {
|
|
523
544
|
builder.profile(if (it.startsWith("mapbox/")) it else "mapbox/$it")
|
|
524
545
|
}
|
|
525
|
-
excludeTypes?.takeIf { it.isNotEmpty() }?.let {
|
|
526
|
-
builder.exclude(it.joinToString(","))
|
|
527
|
-
}
|
|
546
|
+
excludeTypes?.takeIf { it.isNotEmpty() }?.let { builder.exclude(it.joinToString(",")) }
|
|
528
547
|
maxHeight?.let { builder.maxHeight(it) }
|
|
529
548
|
maxWidth?.let { builder.maxWidth(it) }
|
|
530
549
|
|
|
@@ -541,12 +560,10 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
541
560
|
}
|
|
542
561
|
nav.setNavigationRoutes(routes)
|
|
543
562
|
nav.startTripSession()
|
|
563
|
+
safeCameraFollowing()
|
|
544
564
|
}
|
|
545
565
|
|
|
546
|
-
override fun onFailure(
|
|
547
|
-
reasons: List<RouterFailure>,
|
|
548
|
-
routeOptions: RouteOptions
|
|
549
|
-
) {
|
|
566
|
+
override fun onFailure(reasons: List<RouterFailure>, routeOptions: RouteOptions) {
|
|
550
567
|
val msg = reasons.firstOrNull()?.message ?: "Unknown error"
|
|
551
568
|
Log.e(TAG, "Route request failed: $msg")
|
|
552
569
|
onRoutesFailed(mapOf("message" to msg))
|
|
@@ -583,9 +600,9 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
583
600
|
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
|
584
601
|
override fun onDetachedFromWindow() {
|
|
585
602
|
super.onDetachedFromWindow()
|
|
586
|
-
maneuverApi
|
|
587
|
-
routeLineApi
|
|
588
|
-
routeLineView
|
|
603
|
+
maneuverApi.cancel()
|
|
604
|
+
routeLineApi.cancel()
|
|
605
|
+
routeLineView.cancel()
|
|
589
606
|
mapboxNavigation?.stopTripSession()
|
|
590
607
|
MapboxNavigationProvider.destroy()
|
|
591
608
|
mapView.onStop()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jacques_gordon/expo-mapbox-navigation",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
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",
|
|
@@ -23,14 +23,14 @@
|
|
|
23
23
|
],
|
|
24
24
|
"repository": {
|
|
25
25
|
"type": "git",
|
|
26
|
-
"url": "https://github.com/
|
|
26
|
+
"url": "https://github.com/jacquesngomeheffa/expo-mapbox-navigation.git"
|
|
27
27
|
},
|
|
28
28
|
"bugs": {
|
|
29
|
-
"url": "https://github.com/
|
|
29
|
+
"url": "https://github.com/jacquesngomeheffa/expo-mapbox-navigation/issues"
|
|
30
30
|
},
|
|
31
31
|
"author": "Jacques Gordon",
|
|
32
32
|
"license": "MIT",
|
|
33
|
-
"homepage": "https://github.com/
|
|
33
|
+
"homepage": "https://github.com/jacquesngomeheffa/expo-mapbox-navigation#readme",
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"expo": ">=53.0.0",
|
|
36
36
|
"react": "*",
|