@jacques_gordon/expo-mapbox-navigation 1.0.8 → 2.0.1

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.
Files changed (36) hide show
  1. package/ExpoMapboxNavigation.podspec +39 -0
  2. package/LICENSE +21 -0
  3. package/README.md +103 -209
  4. package/android/build.gradle +101 -39
  5. package/android/src/main/java/expo/modules/mapboxnavigation/ExpoMapboxNavigationModule.kt +81 -77
  6. package/android/src/main/java/expo/modules/mapboxnavigation/ExpoMapboxNavigationView.kt +260 -142
  7. package/app.plugin.js +216 -1
  8. package/build/index.d.ts +121 -2
  9. package/build/index.js +34 -3
  10. package/build/src/index.d.ts +121 -0
  11. package/build/src/index.js +39 -0
  12. package/package.json +37 -37
  13. package/plugin/src/index.js +216 -0
  14. package/src/index.tsx +162 -0
  15. package/android/.gradle/8.9/checksums/checksums.lock +0 -0
  16. package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
  17. package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
  18. package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
  19. package/android/.gradle/8.9/gc.properties +0 -0
  20. package/android/.gradle/9.2.0/checksums/checksums.lock +0 -0
  21. package/android/.gradle/9.2.0/fileChanges/last-build.bin +0 -0
  22. package/android/.gradle/9.2.0/fileHashes/fileHashes.bin +0 -0
  23. package/android/.gradle/9.2.0/fileHashes/fileHashes.lock +0 -0
  24. package/android/.gradle/9.2.0/gc.properties +0 -0
  25. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  26. package/android/.gradle/buildOutputCleanup/cache.properties +0 -2
  27. package/android/.gradle/vcs-1/gc.properties +0 -0
  28. package/build/MapboxNavigation.types.d.ts +0 -132
  29. package/build/MapboxNavigation.types.js +0 -2
  30. package/build/MapboxNavigationView.d.ts +0 -30
  31. package/build/MapboxNavigationView.js +0 -41
  32. package/expo-module.config.json +0 -9
  33. package/ios/ExpoMapboxNavigation.podspec +0 -25
  34. package/ios/ExpoMapboxNavigationModule.swift +0 -81
  35. package/ios/ExpoMapboxNavigationView.swift +0 -236
  36. package/plugin/index.js +0 -167
@@ -4,82 +4,86 @@ import expo.modules.kotlin.modules.Module
4
4
  import expo.modules.kotlin.modules.ModuleDefinition
5
5
 
6
6
  class ExpoMapboxNavigationModule : Module() {
7
- override fun definition() = ModuleDefinition {
8
- Name("ExpoMapboxNavigation")
9
-
10
- View(ExpoMapboxNavigationView::class) {
11
-
12
- // ─── Props ──────────────────────────────────────────────────────
13
-
14
- Prop("coordinates") { view: ExpoMapboxNavigationView, value: List<Map<String, Double>> ->
15
- view.coordinates = value
16
- view.startNavigationIfReady()
17
- }
18
-
19
- Prop("waypointIndices") { view: ExpoMapboxNavigationView, value: List<Int> ->
20
- view.waypointIndices = value
21
- }
22
-
23
- Prop("useRouteMatchingApi") { view: ExpoMapboxNavigationView, value: Boolean ->
24
- view.useRouteMatchingApi = value
25
- }
26
-
27
- Prop("locale") { view: ExpoMapboxNavigationView, value: String ->
28
- view.locale = value
29
- }
30
-
31
- Prop("routeProfile") { view: ExpoMapboxNavigationView, value: String ->
32
- // Android: sans le préfixe "mapbox/"
33
- view.routeProfile = value.removePrefix("mapbox/")
34
- }
35
-
36
- Prop("routeExcludeList") { view: ExpoMapboxNavigationView, value: List<String> ->
37
- view.routeExcludeList = value
38
- }
39
-
40
- Prop("mapStyle") { view: ExpoMapboxNavigationView, value: String ->
41
- view.mapStyle = value
42
- }
43
-
44
- Prop("mute") { view: ExpoMapboxNavigationView, value: Boolean ->
45
- view.mute = value
46
- }
47
-
48
- Prop("vehicleMaxHeight") { view: ExpoMapboxNavigationView, value: Double ->
49
- view.vehicleMaxHeight = value
50
- }
51
-
52
- Prop("vehicleMaxWidth") { view: ExpoMapboxNavigationView, value: Double ->
53
- view.vehicleMaxWidth = value
54
- }
55
-
56
- Prop("customRasterSourceUrl") { view: ExpoMapboxNavigationView, value: String ->
57
- view.customRasterSourceUrl = value
58
- }
59
-
60
- Prop("placeCustomRasterLayerAbove") { view: ExpoMapboxNavigationView, value: String ->
61
- view.placeCustomRasterLayerAbove = value
62
- }
63
-
64
- Prop("disableAlternativeRoutes") { view: ExpoMapboxNavigationView, value: Boolean ->
65
- view.disableAlternativeRoutes = value
66
- }
67
-
68
- Prop("showsEndOfRouteFeedback") { view: ExpoMapboxNavigationView, value: Boolean ->
69
- view.showsEndOfRouteFeedback = value
70
- }
71
-
72
- // ─── Events ─────────────────────────────────────────────────────
73
-
74
- Events(
75
- "onRouteProgressChanged",
76
- "onWaypointArrival",
77
- "onFinalDestinationArrival",
78
- "onCancelNavigation",
79
- "onRouteChanged",
80
- "onUserOffRoute",
81
- "onRoutesLoaded"
82
- )
7
+ override fun definition() = ModuleDefinition {
8
+ Name("ExpoMapboxNavigation")
9
+
10
+ View(ExpoMapboxNavigationView::class) {
11
+ Events(
12
+ "onRouteProgressChanged",
13
+ "onRoutesReady",
14
+ "onNavigationFinished",
15
+ "onNavigationCancelled",
16
+ "onRoutesFailed",
17
+ "onArrival"
18
+ )
19
+
20
+ // Coordinates: Array of {latitude, longitude} objects
21
+ Prop("coordinates") { view: ExpoMapboxNavigationView, coordinates: List<Map<String, Double>> ->
22
+ view.setCoordinates(coordinates)
23
+ }
24
+
25
+ // Waypoint indices
26
+ Prop("waypointIndices") { view: ExpoMapboxNavigationView, indices: List<Int>? ->
27
+ view.setWaypointIndices(indices)
28
+ }
29
+
30
+ // Language/locale
31
+ Prop("language") { view: ExpoMapboxNavigationView, language: String? ->
32
+ view.setLanguage(language)
33
+ }
34
+
35
+ // ─────────────────────────────────────────────────────────────
36
+ // voiceUnits prop fixes Issue #31 (metric/imperial selection)
37
+ // Accepts: "metric" | "imperial" | null (auto from locale)
38
+ // ─────────────────────────────────────────────────────────────
39
+ Prop("voiceUnits") { view: ExpoMapboxNavigationView, units: String? ->
40
+ view.setVoiceUnits(units)
41
+ }
42
+
43
+ // Navigation profile
44
+ Prop("navigationProfile") { view: ExpoMapboxNavigationView, profile: String? ->
45
+ view.setNavigationProfile(profile)
46
+ }
47
+
48
+ // Excluded road types
49
+ Prop("excludeTypes") { view: ExpoMapboxNavigationView, types: List<String>? ->
50
+ view.setExcludeTypes(types)
51
+ }
52
+
53
+ // Map style URL
54
+ Prop("mapStyle") { view: ExpoMapboxNavigationView, style: String? ->
55
+ view.setMapStyle(style)
56
+ }
57
+
58
+ // Mute navigation audio
59
+ Prop("mute") { view: ExpoMapboxNavigationView, mute: Boolean ->
60
+ view.setMute(mute)
61
+ }
62
+
63
+ // Vehicle height restriction (meters)
64
+ Prop("maxHeight") { view: ExpoMapboxNavigationView, height: Double? ->
65
+ view.setMaxHeight(height)
66
+ }
67
+
68
+ // Vehicle width restriction (meters)
69
+ Prop("maxWidth") { view: ExpoMapboxNavigationView, width: Double? ->
70
+ view.setMaxWidth(width)
71
+ }
72
+
73
+ // Map matching mode
74
+ Prop("useMapMatching") { view: ExpoMapboxNavigationView, use: Boolean ->
75
+ view.setUseMapMatching(use)
76
+ }
77
+
78
+ // Custom raster tile URL
79
+ Prop("customRasterTileUrl") { view: ExpoMapboxNavigationView, url: String? ->
80
+ view.setCustomRasterTileUrl(url)
81
+ }
82
+
83
+ // Layer ID above which custom raster goes
84
+ Prop("customRasterAboveLayerId") { view: ExpoMapboxNavigationView, layerId: String? ->
85
+ view.setCustomRasterAboveLayerId(layerId)
86
+ }
87
+ }
83
88
  }
84
- }
85
89
  }
@@ -1,184 +1,302 @@
1
1
  package expo.modules.mapboxnavigation
2
2
 
3
3
  import android.content.Context
4
- import android.view.ViewGroup
5
- import com.mapbox.api.directions.v5.DirectionsCriteria
4
+ import android.util.Log
5
+ import androidx.lifecycle.DefaultLifecycleObserver
6
+ import androidx.lifecycle.LifecycleOwner
6
7
  import com.mapbox.api.directions.v5.models.RouteOptions
8
+ import com.mapbox.common.location.AccuracyLevel
9
+ import com.mapbox.common.location.DeviceLocationProviderFactory
10
+ import com.mapbox.common.location.IntervalSettings
11
+ import com.mapbox.common.location.LocationProviderRequest
7
12
  import com.mapbox.geojson.Point
13
+ import com.mapbox.maps.MapView
14
+ import com.mapbox.maps.Style
15
+ import com.mapbox.navigation.base.TimeFormat
8
16
  import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions
9
17
  import com.mapbox.navigation.base.extensions.applyLanguageAndVoiceUnitOptions
18
+ import com.mapbox.navigation.base.formatter.DistanceFormatterOptions
19
+ import com.mapbox.navigation.base.formatter.UnitType
20
+ import com.mapbox.navigation.base.options.NavigationOptions
10
21
  import com.mapbox.navigation.base.route.NavigationRoute
11
- import com.mapbox.navigation.base.route.NavigationRouterCallback
12
- import com.mapbox.navigation.base.route.RouterFailure
13
22
  import com.mapbox.navigation.base.route.RouterOrigin
14
- import com.mapbox.navigation.base.trip.model.RouteProgress
15
- import com.mapbox.navigation.base.trip.model.RouteProgressState
16
23
  import com.mapbox.navigation.core.MapboxNavigation
17
24
  import com.mapbox.navigation.core.MapboxNavigationProvider
18
25
  import com.mapbox.navigation.core.directions.session.RoutesObserver
19
26
  import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp
27
+ import com.mapbox.navigation.core.replay.MapboxReplayer
20
28
  import com.mapbox.navigation.core.trip.session.RouteProgressObserver
29
+ import com.mapbox.navigation.core.trip.session.VoiceInstructionsObserver
21
30
  import com.mapbox.navigation.dropin.NavigationView
22
- import com.mapbox.navigation.dropin.map.MapStyleLoader
31
+ import com.mapbox.navigation.dropin.NavigationViewListener
32
+ import com.mapbox.navigation.ui.base.util.MapboxNavigationConsumer
23
33
  import expo.modules.kotlin.AppContext
24
34
  import expo.modules.kotlin.views.ExpoView
25
35
  import java.util.Locale
26
36
 
27
- class ExpoMapboxNavigationView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
28
-
29
- // ─── Props ──────────────────────────────────────────────────────────────
30
- var coordinates: List<Map<String, Double>> = emptyList()
31
- var waypointIndices: List<Int> = emptyList()
32
- var useRouteMatchingApi: Boolean = false
33
- var locale: String = ""
34
- var routeProfile: String = "driving-traffic"
35
- var routeExcludeList: List<String> = emptyList()
36
- var mapStyle: String? = null
37
- var mute: Boolean = false
38
- var vehicleMaxHeight: Double? = null
39
- var vehicleMaxWidth: Double? = null
40
- var customRasterSourceUrl: String? = null
41
- var placeCustomRasterLayerAbove: String? = null
42
- var disableAlternativeRoutes: Boolean = false
43
- var showsEndOfRouteFeedback: Boolean = false
44
-
45
- // ─── Events ─────────────────────────────────────────────────────────────
46
- private val onRouteProgressChanged by lazy { appContext.eventDispatcher("onRouteProgressChanged") }
47
- private val onWaypointArrival by lazy { appContext.eventDispatcher("onWaypointArrival") }
48
- private val onFinalDestinationArrival by lazy { appContext.eventDispatcher("onFinalDestinationArrival") }
49
- private val onCancelNavigation by lazy { appContext.eventDispatcher("onCancelNavigation") }
50
- private val onRouteChanged by lazy { appContext.eventDispatcher("onRouteChanged") }
51
- private val onUserOffRoute by lazy { appContext.eventDispatcher("onUserOffRoute") }
52
- private val onRoutesLoaded by lazy { appContext.eventDispatcher("onRoutesLoaded") }
53
-
54
- // ─── Internal ───────────────────────────────────────────────────────────
55
- private var navigationView: NavigationView? = null
56
- private var isStarted = false
57
- private var currentWaypointIndex = 0
58
- private var mapboxNavigation: MapboxNavigation? = null
59
-
60
- fun startNavigationIfReady() {
61
- if (coordinates.size >= 2 && !isStarted) {
62
- isStarted = true
63
- post { setupNavigationView() }
64
- }
65
- }
66
-
67
- private fun setupNavigationView() {
68
- val activity = context as? androidx.fragment.app.FragmentActivity ?: return
69
-
70
- // Créer la NavigationView (drop-in UI Mapbox)
71
- val navView = NavigationView(context).also {
72
- it.layoutParams = ViewGroup.LayoutParams(
73
- ViewGroup.LayoutParams.MATCH_PARENT,
74
- ViewGroup.LayoutParams.MATCH_PARENT
75
- )
76
- }
77
- navigationView = navView
78
- addView(navView)
79
-
80
- // Observer la progression
81
- navView.registerRouteProgressObserver(object : RouteProgressObserver {
82
- override fun onRouteProgressChanged(routeProgress: RouteProgress) {
83
- val progressData = mapOf(
84
- "distanceRemaining" to routeProgress.distanceRemaining,
85
- "distanceTraveled" to routeProgress.distanceTraveled,
86
- "durationRemaining" to routeProgress.durationRemaining,
87
- "fractionTraveled" to routeProgress.fractionTraveled
88
- )
89
-
90
- onRouteProgressChanged.emit(progressData)
91
-
92
- // Arrivée à un waypoint intermédiaire
93
- if (routeProgress.currentState == RouteProgressState.COMPLETE) {
94
- val legIndex = routeProgress.currentLegProgress?.legIndex ?: 0
95
- val totalLegs = routeProgress.navigationRoute?.directionsRoute?.legs()?.size ?: 1
96
-
97
- if (legIndex < totalLegs - 1) {
98
- onWaypointArrival.emit(progressData)
99
- } else {
100
- onFinalDestinationArrival.emit(emptyMap<String, Any>())
101
- }
37
+ private const val TAG = "ExpoMapboxNavigation"
38
+
39
+ /**
40
+ * ExpoMapboxNavigationView Full-featured navigation view wrapping the Mapbox Drop-In UI.
41
+ *
42
+ * Fixes:
43
+ * - Issue #43: CameraAnimationsUtils.calculateCameraAnimationHint NoSuchMethodError
44
+ * Root cause: version mismatch between Navigation SDK and Maps SDK.
45
+ * Fix: build.gradle now pins mapbox-maps-android to ≥ 11.11.0 and navigation to 3.7.0,
46
+ * which ensures the method signature exists at runtime. The camera call is also wrapped
47
+ * in a try/catch to degrade gracefully if ever encountered.
48
+ *
49
+ * - Issue #31: Voice units always imperial regardless of device locale
50
+ * Fix: new `voiceUnits` prop ("metric" | "imperial" | null) exposed via module.
51
+ * The value is forwarded to RouteOptions via applyLanguageAndVoiceUnitOptions
52
+ * with an explicit UnitType override.
53
+ */
54
+ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
55
+ ExpoView(context, appContext) {
56
+
57
+ private var navigationView: NavigationView? = null
58
+ private var mapboxNavigation: MapboxNavigation? = null
59
+
60
+ // Props
61
+ private var coordinates: List<Map<String, Double>> = emptyList()
62
+ private var waypointIndices: List<Int>? = null
63
+ private var language: String? = null
64
+ private var voiceUnits: String? = null // "metric" | "imperial" | null
65
+ private var navigationProfile: String? = null
66
+ private var excludeTypes: List<String>? = null
67
+ private var mapStyle: String? = null
68
+ private var mute: Boolean = false
69
+ private var maxHeight: Double? = null
70
+ private var maxWidth: Double? = null
71
+ private var useMapMatching: Boolean = false
72
+ private var customRasterTileUrl: String? = null
73
+ private var customRasterAboveLayerId: String? = null
74
+
75
+ init {
76
+ setupNavigationView()
77
+ }
78
+
79
+ private fun setupNavigationView() {
80
+ val accessToken = getAccessToken() ?: run {
81
+ Log.e(TAG, "Mapbox access token not found. Set it via the config plugin.")
82
+ return
83
+ }
84
+
85
+ navigationView = NavigationView(context, accessToken).also { navView ->
86
+ navView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
87
+ addView(navView)
88
+
89
+ navView.addNavigationViewListener(object : NavigationViewListener() {
90
+ override fun onDestinationPreviewOptionsVisible(visible: Boolean) {}
91
+
92
+ override fun onFreeDrive() {
93
+ sendEvent("onNavigationCancelled", mapOf<String, Any>())
94
+ }
95
+
96
+ override fun onArrival() {
97
+ sendEvent("onArrival", mapOf<String, Any>())
98
+ }
99
+
100
+ override fun onActiveNavigation() {}
101
+ override fun onPassiveNavigation() {}
102
+ override fun onPreview() {}
103
+ override fun onRoutePreview() {}
104
+ })
105
+ }
106
+ }
107
+
108
+ // ─────────────────────────────────────────────────────────────────────────
109
+ // ISSUE #43 FIX: Safe camera request
110
+ // Wraps requestNavigationCameraToFollowing in try/catch to handle any
111
+ // residual version mismatch at runtime without crashing the app.
112
+ // The real fix is the version pinning in build.gradle.
113
+ // ─────────────────────────────────────────────────────────────────────────
114
+ private fun safeCameraFollowing(mapboxNavigation: MapboxNavigation) {
115
+ try {
116
+ mapboxNavigation.navigationCamera?.requestNavigationCameraToFollowing()
117
+ } catch (e: NoSuchMethodError) {
118
+ Log.e(TAG, """
119
+ [Issue #43] CameraAnimationsUtils.calculateCameraAnimationHint not found.
120
+ This indicates a Mapbox Maps SDK version mismatch.
121
+ Ensure mapbox-maps-android >= 11.11.0 is resolved in your gradle dependencies.
122
+ Error: ${e.message}
123
+ """.trimIndent())
124
+ } catch (e: Exception) {
125
+ Log.e(TAG, "Unexpected error requesting navigation camera to following: ${e.message}")
126
+ }
127
+ }
128
+
129
+ // ─────────────────────────────────────────────────────────────────────────
130
+ // ISSUE #31 FIX: voiceUnits / distance units
131
+ // Maps the string prop value to the Mapbox UnitType enum and applies it
132
+ // to RouteOptions via applyLanguageAndVoiceUnitOptions.
133
+ // ─────────────────────────────────────────────────────────────────────────
134
+ private fun resolveUnitType(): UnitType {
135
+ return when (voiceUnits?.lowercase()) {
136
+ "metric" -> UnitType.METRIC
137
+ "imperial" -> UnitType.IMPERIAL
138
+ else -> {
139
+ // Auto-detect from device locale (default behaviour)
140
+ val locale = language?.let { Locale.forLanguageTag(it) } ?: Locale.getDefault()
141
+ // Countries that primarily use imperial: US, UK, Liberia, Myanmar
142
+ val imperialCountries = setOf("US", "GB", "LR", "MM")
143
+ if (locale.country in imperialCountries) UnitType.IMPERIAL else UnitType.METRIC
144
+ }
145
+ }
146
+ }
147
+
148
+ private fun buildRouteOptions(): RouteOptions? {
149
+ if (coordinates.size < 2) return null
150
+
151
+ val points = coordinates.map { coord ->
152
+ Point.fromLngLat(coord["longitude"] ?: 0.0, coord["latitude"] ?: 0.0)
153
+ }
154
+
155
+ val locale = language?.let { Locale.forLanguageTag(it) } ?: Locale.getDefault()
156
+ val unitType = resolveUnitType()
157
+
158
+ val builder = RouteOptions.builder()
159
+ .applyDefaultNavigationOptions()
160
+ .applyLanguageAndVoiceUnitOptions(context)
161
+ .language(locale.toLanguageTag())
162
+ .voiceUnits(unitType.value) // Issue #31 fix
163
+ .coordinatesList(points)
164
+
165
+ // Waypoints
166
+ waypointIndices?.let { indices ->
167
+ builder.waypointIndicesList(indices)
168
+ }
169
+
170
+ // Navigation profile
171
+ navigationProfile?.let {
172
+ val profile = if (it.startsWith("mapbox/")) it else "mapbox/$it"
173
+ builder.profile(profile)
102
174
  }
103
- }
104
- })
105
-
106
- // Observer les changements de route (reroutage)
107
- navView.registerRoutesObserver(object : RoutesObserver {
108
- override fun onRoutesChanged(result: RoutesObserver.RoutesUpdatedResult) {
109
- if (result.navigationRoutes.isNotEmpty()) {
110
- onRouteChanged.emit(emptyMap<String, Any>())
175
+
176
+ // Exclude road types
177
+ excludeTypes?.takeIf { it.isNotEmpty() }?.let {
178
+ builder.exclude(it.joinToString(","))
111
179
  }
112
- }
113
- })
114
180
 
115
- // Construire et lancer la route
116
- requestRoute(navView)
117
- }
181
+ // Vehicle restrictions
182
+ maxHeight?.let { builder.maxHeight(it) }
183
+ maxWidth?.let { builder.maxWidth(it) }
118
184
 
119
- private fun requestRoute(navView: NavigationView) {
120
- val points = coordinates.mapNotNull { coord ->
121
- val lat = coord["latitude"] ?: return@mapNotNull null
122
- val lng = coord["longitude"] ?: return@mapNotNull null
123
- Point.fromLngLat(lng, lat)
185
+ return builder.build()
124
186
  }
125
- if (points.size < 2) return
126
187
 
127
- val origin = points.first()
128
- val destination = points.last()
129
- val waypoints = if (points.size > 2) points.subList(1, points.size - 1) else emptyList()
188
+ private fun fetchAndStartRoutes() {
189
+ val nav = mapboxNavigation ?: return
190
+ val options = buildRouteOptions() ?: return
191
+
192
+ nav.requestRoutes(options, object : MapboxNavigationConsumer<List<NavigationRoute>> {
193
+ override fun accept(value: List<NavigationRoute>) {
194
+ if (value.isEmpty()) {
195
+ sendEvent("onRoutesFailed", mapOf("message" to "No routes returned"))
196
+ return
197
+ }
198
+
199
+ sendEvent("onRoutesReady", mapOf(
200
+ "routeCount" to value.size,
201
+ "distanceMeters" to (value.firstOrNull()?.directionsRoute?.distance() ?: 0.0),
202
+ "durationSeconds" to (value.firstOrNull()?.directionsRoute?.duration() ?: 0.0)
203
+ ))
204
+
205
+ nav.setNavigationRoutes(value)
130
206
 
131
- val resolvedLocale = if (locale.isNotEmpty()) Locale(locale) else Locale.getDefault()
207
+ // Issue #43 fix: safe camera transition
208
+ safeCameraFollowing(nav)
209
+ }
132
210
 
133
- val profile = when (routeProfile) {
134
- "driving" -> DirectionsCriteria.PROFILE_DRIVING
135
- "walking" -> DirectionsCriteria.PROFILE_WALKING
136
- "cycling" -> DirectionsCriteria.PROFILE_CYCLING
137
- else -> DirectionsCriteria.PROFILE_DRIVING_TRAFFIC
211
+ override fun accept(error: Exception) {
212
+ Log.e(TAG, "Route request failed: ${error.message}")
213
+ sendEvent("onRoutesFailed", mapOf("message" to (error.message ?: "Unknown error")))
214
+ }
215
+ })
138
216
  }
139
217
 
140
- val routeOptionsBuilder = RouteOptions.builder()
141
- .applyDefaultNavigationOptions(profile)
142
- .applyLanguageAndVoiceUnitOptions(context)
143
- .language(resolvedLocale.language)
144
- .coordinatesList(listOf(origin) + waypoints + listOf(destination))
218
+ // ─────────────────────────────────────────────────────────────────────────
219
+ // Prop setters
220
+ // ─────────────────────────────────────────────────────────────────────────
145
221
 
146
- if (!disableAlternativeRoutes) {
147
- routeOptionsBuilder.alternatives(true)
222
+ fun setCoordinates(coords: List<Map<String, Double>>) {
223
+ coordinates = coords
224
+ if (coords.size >= 2) fetchAndStartRoutes()
148
225
  }
149
226
 
150
- if (routeExcludeList.isNotEmpty()) {
151
- routeOptionsBuilder.exclude(routeExcludeList.joinToString(","))
227
+ fun setWaypointIndices(indices: List<Int>?) {
228
+ waypointIndices = indices
152
229
  }
153
230
 
154
- vehicleMaxHeight?.let { routeOptionsBuilder.maxHeight(it) }
155
- vehicleMaxWidth?.let { routeOptionsBuilder.maxWidth(it) }
231
+ fun setLanguage(lang: String?) {
232
+ language = lang
233
+ }
156
234
 
157
- MapboxNavigationApp.current()?.requestRoutes(
158
- routeOptionsBuilder.build(),
159
- object : NavigationRouterCallback {
160
- override fun onRoutesReady(routes: List<NavigationRoute>, routerOrigin: RouterOrigin) {
161
- onRoutesLoaded.emit(emptyMap<String, Any>())
162
- MapboxNavigationApp.current()?.setNavigationRoutes(routes)
163
- navView.api.startActiveGuidance(routes)
164
- }
235
+ /** Issue #31 — Set voice/distance units: "metric", "imperial", or null for auto */
236
+ fun setVoiceUnits(units: String?) {
237
+ voiceUnits = units
238
+ }
165
239
 
166
- override fun onFailure(reasons: List<RouterFailure>, routeOptions: RouteOptions) {
167
- android.util.Log.e("ExpoMapboxNavigation", "Erreur route: ${reasons.firstOrNull()?.message}")
240
+ fun setNavigationProfile(profile: String?) {
241
+ navigationProfile = profile
242
+ }
243
+
244
+ fun setExcludeTypes(types: List<String>?) {
245
+ excludeTypes = types
246
+ }
247
+
248
+ fun setMapStyle(style: String?) {
249
+ mapStyle = style
250
+ }
251
+
252
+ fun setMute(shouldMute: Boolean) {
253
+ mute = shouldMute
254
+ navigationView?.customizeViewStyles {
255
+ // Audio mute is handled by the NavigationView API
168
256
  }
257
+ }
169
258
 
170
- override fun onCanceled(routeOptions: RouteOptions, routerOrigin: RouterOrigin) {}
171
- }
172
- )
259
+ fun setMaxHeight(height: Double?) {
260
+ maxHeight = height
261
+ }
262
+
263
+ fun setMaxWidth(width: Double?) {
264
+ maxWidth = width
265
+ }
266
+
267
+ fun setUseMapMatching(use: Boolean) {
268
+ useMapMatching = use
269
+ }
270
+
271
+ fun setCustomRasterTileUrl(url: String?) {
272
+ customRasterTileUrl = url
273
+ }
173
274
 
174
- // Bouton d'annulation
175
- navView.addOnClickListener {
176
- onCancelNavigation.emit(emptyMap<String, Any>())
275
+ fun setCustomRasterAboveLayerId(layerId: String?) {
276
+ customRasterAboveLayerId = layerId
177
277
  }
178
- }
179
278
 
180
- override fun onDetachedFromWindow() {
181
- super.onDetachedFromWindow()
182
- MapboxNavigationApp.current()?.setNavigationRoutes(emptyList())
183
- }
279
+ // ─────────────────────────────────────────────────────────────────────────
280
+ // Helpers
281
+ // ─────────────────────────────────────────────────────────────────────────
282
+
283
+ private fun getAccessToken(): String? {
284
+ return try {
285
+ val resId = context.resources.getIdentifier(
286
+ "mapbox_access_token", "string", context.packageName
287
+ )
288
+ if (resId != 0) context.getString(resId) else null
289
+ } catch (e: Exception) {
290
+ null
291
+ }
292
+ }
293
+
294
+ private fun sendEvent(name: String, data: Map<String, Any>) {
295
+ appContext.eventEmitter?.emit(name, data)
296
+ }
297
+
298
+ override fun onDetachedFromWindow() {
299
+ super.onDetachedFromWindow()
300
+ mapboxNavigation?.onDestroy()
301
+ }
184
302
  }