@jacques_gordon/expo-mapbox-navigation 2.0.6 → 2.0.8

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.
@@ -8,6 +8,7 @@ class ExpoMapboxNavigationModule : Module() {
8
8
  Name("ExpoMapboxNavigation")
9
9
 
10
10
  View(ExpoMapboxNavigationView::class) {
11
+ // Events must be declared here — they correspond to EventDispatcher fields in the View
11
12
  Events(
12
13
  "onRouteProgressChanged",
13
14
  "onRoutesReady",
@@ -17,70 +18,42 @@ class ExpoMapboxNavigationModule : Module() {
17
18
  "onArrival"
18
19
  )
19
20
 
20
- // Coordinates: Array of {latitude, longitude} objects
21
21
  Prop("coordinates") { view: ExpoMapboxNavigationView, coordinates: List<Map<String, Double>> ->
22
22
  view.setCoordinates(coordinates)
23
23
  }
24
-
25
- // Waypoint indices
26
24
  Prop("waypointIndices") { view: ExpoMapboxNavigationView, indices: List<Int>? ->
27
25
  view.setWaypointIndices(indices)
28
26
  }
29
-
30
- // Language/locale
31
27
  Prop("language") { view: ExpoMapboxNavigationView, language: String? ->
32
28
  view.setLanguage(language)
33
29
  }
34
-
35
- // ─────────────────────────────────────────────────────────────
36
- // voiceUnits prop — fixes Issue #31 (metric/imperial selection)
37
- // Accepts: "metric" | "imperial" | null (auto from locale)
38
- // ─────────────────────────────────────────────────────────────
39
30
  Prop("voiceUnits") { view: ExpoMapboxNavigationView, units: String? ->
40
31
  view.setVoiceUnits(units)
41
32
  }
42
-
43
- // Navigation profile
44
33
  Prop("navigationProfile") { view: ExpoMapboxNavigationView, profile: String? ->
45
34
  view.setNavigationProfile(profile)
46
35
  }
47
-
48
- // Excluded road types
49
36
  Prop("excludeTypes") { view: ExpoMapboxNavigationView, types: List<String>? ->
50
37
  view.setExcludeTypes(types)
51
38
  }
52
-
53
- // Map style URL
54
39
  Prop("mapStyle") { view: ExpoMapboxNavigationView, style: String? ->
55
40
  view.setMapStyle(style)
56
41
  }
57
-
58
- // Mute navigation audio
59
42
  Prop("mute") { view: ExpoMapboxNavigationView, mute: Boolean ->
60
43
  view.setMute(mute)
61
44
  }
62
-
63
- // Vehicle height restriction (meters)
64
45
  Prop("maxHeight") { view: ExpoMapboxNavigationView, height: Double? ->
65
46
  view.setMaxHeight(height)
66
47
  }
67
-
68
- // Vehicle width restriction (meters)
69
48
  Prop("maxWidth") { view: ExpoMapboxNavigationView, width: Double? ->
70
49
  view.setMaxWidth(width)
71
50
  }
72
-
73
- // Map matching mode
74
51
  Prop("useMapMatching") { view: ExpoMapboxNavigationView, use: Boolean ->
75
52
  view.setUseMapMatching(use)
76
53
  }
77
-
78
- // Custom raster tile URL
79
54
  Prop("customRasterTileUrl") { view: ExpoMapboxNavigationView, url: String? ->
80
55
  view.setCustomRasterTileUrl(url)
81
56
  }
82
-
83
- // Layer ID above which custom raster goes
84
57
  Prop("customRasterAboveLayerId") { view: ExpoMapboxNavigationView, layerId: String? ->
85
58
  view.setCustomRasterAboveLayerId(layerId)
86
59
  }
@@ -1,11 +1,13 @@
1
1
  package expo.modules.mapboxnavigation
2
2
 
3
+ import android.annotation.SuppressLint
3
4
  import android.content.Context
4
5
  import android.util.Log
5
6
  import com.mapbox.api.directions.v5.models.RouteOptions
6
7
  import com.mapbox.geojson.Point
7
8
  import com.mapbox.maps.MapView
8
9
  import com.mapbox.maps.Style
10
+ import com.mapbox.maps.plugin.animation.camera
9
11
  import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions
10
12
  import com.mapbox.navigation.base.options.NavigationOptions
11
13
  import com.mapbox.navigation.base.route.NavigationRoute
@@ -25,6 +27,7 @@ import com.mapbox.navigation.ui.maps.route.line.api.MapboxRouteLineView
25
27
  import com.mapbox.navigation.ui.maps.route.line.model.MapboxRouteLineApiOptions
26
28
  import com.mapbox.navigation.ui.maps.route.line.model.MapboxRouteLineViewOptions
27
29
  import expo.modules.kotlin.AppContext
30
+ import expo.modules.kotlin.viewevent.EventDispatcher
28
31
  import expo.modules.kotlin.views.ExpoView
29
32
  import java.util.Locale
30
33
 
@@ -33,6 +36,16 @@ private const val TAG = "ExpoMapboxNavigation"
33
36
  class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
34
37
  ExpoView(context, appContext) {
35
38
 
39
+ // ── EventDispatchers — the correct Expo Modules API for View events ───────
40
+ // Each event declared here must match the Events(...) list in ExpoMapboxNavigationModule
41
+ private val onRouteProgressChanged by EventDispatcher()
42
+ private val onRoutesReady by EventDispatcher()
43
+ private val onNavigationFinished by EventDispatcher()
44
+ private val onNavigationCancelled by EventDispatcher()
45
+ private val onRoutesFailed by EventDispatcher()
46
+ private val onArrival by EventDispatcher()
47
+
48
+ // ── Map & Navigation ──────────────────────────────────────────────────────
36
49
  private val mapView: MapView = MapView(context)
37
50
  private var navigationCamera: NavigationCamera? = null
38
51
  private var viewportDataSource: MapboxNavigationViewportDataSource? = null
@@ -40,7 +53,7 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
40
53
  private var routeLineView: MapboxRouteLineView? = null
41
54
  private var mapboxNavigation: MapboxNavigation? = null
42
55
 
43
- // Props
56
+ // ── Props ─────────────────────────────────────────────────────────────────
44
57
  private var coordinates: List<Map<String, Double>> = emptyList()
45
58
  private var waypointIndices: List<Int>? = null
46
59
  private var language: String? = null
@@ -65,13 +78,14 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
65
78
  val navigationOptions = NavigationOptions.Builder(context).build()
66
79
  mapboxNavigation = MapboxNavigationProvider.create(navigationOptions)
67
80
 
68
- mapView.getMapboxMap().loadStyle(mapStyle ?: Style.MAPBOX_STREETS) { style ->
81
+ mapView.mapboxMap.loadStyle(mapStyle ?: Style.MAPBOX_STREETS) { _ ->
69
82
  routeLineApi = MapboxRouteLineApi(MapboxRouteLineApiOptions.Builder().build())
70
- routeLineView = MapboxRouteLineView(MapboxRouteLineViewOptions.Builder(context).build())
71
-
72
- viewportDataSource = MapboxNavigationViewportDataSource(mapView.getMapboxMap())
83
+ routeLineView = MapboxRouteLineView(
84
+ MapboxRouteLineViewOptions.Builder(context).build()
85
+ )
86
+ viewportDataSource = MapboxNavigationViewportDataSource(mapView.mapboxMap)
73
87
  navigationCamera = NavigationCamera(
74
- mapView.getMapboxMap(),
88
+ mapView.mapboxMap,
75
89
  mapView.camera,
76
90
  viewportDataSource!!
77
91
  )
@@ -87,33 +101,48 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
87
101
  val routes = result.navigationRoutes
88
102
  if (routes.isNotEmpty()) {
89
103
  routeLineApi?.setNavigationRoutes(routes) { value ->
90
- mapView.getMapboxMap().getStyle()?.let { style ->
104
+ mapView.mapboxMap.style?.let { style ->
91
105
  routeLineView?.renderRouteDrawData(style, value)
92
106
  }
93
107
  }
108
+ viewportDataSource?.onRouteChanged(routes.first())
109
+ viewportDataSource?.evaluate()
94
110
  safeCameraFollowing()
95
- emit("onRoutesReady", mapOf(
111
+
112
+ onRoutesReady(mapOf(
96
113
  "routeCount" to routes.size,
97
- "distanceMeters" to (routes.firstOrNull()?.directionsRoute?.distance() ?: 0.0),
98
- "durationSeconds" to (routes.firstOrNull()?.directionsRoute?.duration() ?: 0.0)
114
+ "distanceMeters" to (routes.first().directionsRoute.distance() ?: 0.0),
115
+ "durationSeconds" to (routes.first().directionsRoute.duration() ?: 0.0)
99
116
  ))
117
+ } else {
118
+ routeLineApi?.clearRouteLine { value ->
119
+ mapView.mapboxMap.style?.let { style ->
120
+ routeLineView?.renderClearRouteLineValue(style, value)
121
+ }
122
+ }
123
+ viewportDataSource?.clearRouteData()
124
+ viewportDataSource?.evaluate()
100
125
  }
101
126
  }
102
127
  })
103
128
 
104
- nav.registerRouteProgressObserver(RouteProgressObserver { progress ->
105
- emit("onRouteProgressChanged", mapOf(
106
- "distanceRemaining" to progress.distanceRemaining,
107
- "durationRemaining" to progress.durationRemaining,
108
- "distanceTraveled" to progress.distanceTraveled,
109
- "fractionTraveled" to progress.fractionTraveled,
129
+ nav.registerRouteProgressObserver(RouteProgressObserver { routeProgress ->
130
+ viewportDataSource?.onRouteProgressChanged(routeProgress)
131
+ viewportDataSource?.evaluate()
132
+
133
+ onRouteProgressChanged(mapOf(
134
+ "distanceRemaining" to routeProgress.distanceRemaining,
135
+ "durationRemaining" to routeProgress.durationRemaining,
136
+ "distanceTraveled" to routeProgress.distanceTraveled,
137
+ "fractionTraveled" to routeProgress.fractionTraveled,
110
138
  "currentStepDistanceRemaining" to
111
- (progress.currentLegProgress?.currentStepProgress?.distanceRemaining ?: 0f)
139
+ (routeProgress.currentLegProgress
140
+ ?.currentStepProgress?.distanceRemaining ?: 0f)
112
141
  ))
113
142
  })
114
143
  }
115
144
 
116
- // ── Issue #43: safe camera transition ────────────────────────────────────
145
+ // ── Issue #43: safe camera ────────────────────────────────────────────────
117
146
  private fun safeCameraFollowing() {
118
147
  try {
119
148
  navigationCamera?.requestNavigationCameraToFollowing(
@@ -127,18 +156,19 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
127
156
  }
128
157
 
129
158
  // ── Issue #31: voice units ────────────────────────────────────────────────
130
- private fun resolveUnitType(): String {
159
+ private fun resolveVoiceUnits(): String {
131
160
  return when (voiceUnits?.lowercase()) {
132
161
  "metric" -> "metric"
133
162
  "imperial" -> "imperial"
134
163
  else -> {
135
164
  val locale = language?.let { Locale.forLanguageTag(it) } ?: Locale.getDefault()
136
- val imperialCountries = setOf("US", "GB", "LR", "MM")
137
- if (locale.country in imperialCountries) "imperial" else "metric"
165
+ if (locale.country in setOf("US", "GB", "LR", "MM")) "imperial" else "metric"
138
166
  }
139
167
  }
140
168
  }
141
169
 
170
+ // ── Route request ─────────────────────────────────────────────────────────
171
+ @SuppressLint("MissingPermission")
142
172
  private fun fetchRoutes() {
143
173
  val nav = mapboxNavigation ?: return
144
174
  if (coordinates.size < 2) return
@@ -146,24 +176,24 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
146
176
  val points = coordinates.map { coord ->
147
177
  Point.fromLngLat(coord["longitude"] ?: 0.0, coord["latitude"] ?: 0.0)
148
178
  }
149
-
150
179
  val locale = language?.let { Locale.forLanguageTag(it) } ?: Locale.getDefault()
151
180
 
152
181
  val builder = RouteOptions.builder()
153
182
  .applyDefaultNavigationOptions()
154
183
  .language(locale.toLanguageTag())
155
- .voiceUnits(resolveUnitType())
184
+ .voiceUnits(resolveVoiceUnits())
156
185
  .coordinatesList(points)
157
186
 
158
187
  waypointIndices?.let { builder.waypointIndicesList(it) }
159
188
  navigationProfile?.let {
160
189
  builder.profile(if (it.startsWith("mapbox/")) it else "mapbox/$it")
161
190
  }
162
- excludeTypes?.takeIf { it.isNotEmpty() }?.let { builder.exclude(it.joinToString(",")) }
191
+ excludeTypes?.takeIf { it.isNotEmpty() }?.let {
192
+ builder.exclude(it.joinToString(","))
193
+ }
163
194
  maxHeight?.let { builder.maxHeight(it) }
164
195
  maxWidth?.let { builder.maxWidth(it) }
165
196
 
166
- // Nav SDK v3: NavigationRouterCallback uses @RouterOrigin String (not enum)
167
197
  nav.requestRoutes(
168
198
  builder.build(),
169
199
  object : NavigationRouterCallback {
@@ -172,7 +202,7 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
172
202
  @RouterOrigin routerOrigin: String
173
203
  ) {
174
204
  if (routes.isEmpty()) {
175
- emit("onRoutesFailed", mapOf("message" to "No routes returned"))
205
+ onRoutesFailed(mapOf("message" to "No routes returned"))
176
206
  return
177
207
  }
178
208
  nav.setNavigationRoutes(routes)
@@ -185,7 +215,7 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
185
215
  ) {
186
216
  val msg = reasons.firstOrNull()?.message ?: "Unknown error"
187
217
  Log.e(TAG, "Route request failed: $msg")
188
- emit("onRoutesFailed", mapOf("message" to msg))
218
+ onRoutesFailed(mapOf("message" to msg))
189
219
  }
190
220
 
191
221
  override fun onCanceled(
@@ -216,30 +246,13 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
216
246
  fun setCustomRasterTileUrl(url: String?) { customRasterTileUrl = url }
217
247
  fun setCustomRasterAboveLayerId(layerId: String?) { customRasterAboveLayerId = layerId }
218
248
 
219
- // ── Helpers ───────────────────────────────────────────────────────────────
220
- private fun getAccessToken(): String? {
221
- return try {
222
- val resId = context.resources.getIdentifier(
223
- "mapbox_access_token", "string", context.packageName
224
- )
225
- if (resId != 0) context.getString(resId) else null
226
- } catch (e: Exception) { null }
227
- }
228
-
229
- // Correct Expo SDK v53 event emitter API
230
- private fun emit(name: String, data: Map<String, Any>) {
231
- try {
232
- appContext.eventEmitter?.emit(name, data)
233
- } catch (e: Exception) {
234
- Log.e(TAG, "Failed to emit event $name: ${e.message}")
235
- }
236
- }
237
-
249
+ // ── Lifecycle ─────────────────────────────────────────────────────────────
238
250
  override fun onDetachedFromWindow() {
239
251
  super.onDetachedFromWindow()
252
+ routeLineApi?.cancel()
253
+ routeLineView?.cancel()
240
254
  mapboxNavigation?.stopTripSession()
241
255
  MapboxNavigationProvider.destroy()
242
- routeLineApi?.cancel()
243
256
  mapView.onStop()
244
257
  mapView.onDestroy()
245
258
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jacques_gordon/expo-mapbox-navigation",
3
- "version": "2.0.6",
3
+ "version": "2.0.8",
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",