@jacques_gordon/expo-mapbox-navigation 2.0.7 → 2.0.9
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 +15 -43
- package/android/src/main/java/expo/modules/mapboxnavigation/ExpoMapboxNavigationModule.kt +1 -28
- package/android/src/main/java/expo/modules/mapboxnavigation/ExpoMapboxNavigationView.kt +368 -47
- package/app.plugin.js +44 -45
- package/package.json +1 -1
- package/plugin/src/index.js +44 -45
package/android/build.gradle
CHANGED
|
@@ -1,51 +1,25 @@
|
|
|
1
1
|
apply plugin: 'com.android.library'
|
|
2
2
|
apply plugin: 'kotlin-android'
|
|
3
|
-
apply plugin: 'maven-publish'
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
// Mapbox Navigation SDK version used by this module.
|
|
7
|
-
// Must be aligned with the Mapbox Maps SDK version used in @rnmapbox/maps.
|
|
8
|
-
// Minimum required: 3.x compatible with mapbox-maps-android 11.11.0
|
|
9
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
10
|
-
// Navigation SDK v3 uses com.mapbox.navigationcore (NOT com.mapbox.navigation)
|
|
11
|
-
// v3.8.1 is the stable release that ships with Maps SDK 11.11.0
|
|
12
|
-
def MAPBOX_NAVIGATION_VERSION = "3.8.1"
|
|
13
|
-
|
|
14
|
-
// Forces minimum Mapbox Maps Android version to 11.11.0 to ensure
|
|
15
|
-
// CameraAnimationsUtils.calculateCameraAnimationHint(...) is available (fixes #43)
|
|
4
|
+
def MAPBOX_NAV_VERSION = "3.8.1"
|
|
16
5
|
def MAPBOX_MAPS_MIN_VERSION = "11.11.0"
|
|
17
|
-
|
|
18
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
19
|
-
// NDK 27 — required for 16 KB page size compatibility (Android 15+)
|
|
20
|
-
// See: https://developer.android.com/guide/practices/page-sizes
|
|
21
|
-
// NDK 27 added full support for 16 KB page alignment via -DALIGN_16KB flag
|
|
22
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
23
6
|
def REQUIRED_NDK_VERSION = "27.0.12077973"
|
|
24
7
|
|
|
25
8
|
android {
|
|
26
9
|
compileSdkVersion safeExtGet("compileSdkVersion", 35)
|
|
27
10
|
namespace "expo.modules.mapboxnavigation"
|
|
28
|
-
|
|
29
|
-
// Force NDK 27 for 16 KB page size compatibility
|
|
30
11
|
ndkVersion REQUIRED_NDK_VERSION
|
|
31
12
|
|
|
32
13
|
defaultConfig {
|
|
33
14
|
minSdkVersion safeExtGet("minSdkVersion", 24)
|
|
34
15
|
targetSdkVersion safeExtGet("targetSdkVersion", 35)
|
|
35
16
|
|
|
36
|
-
// ─────────────────────────────────────────────────────────────────
|
|
37
|
-
// 16 KB PAGE SIZE SUPPORT (Android 15 / API 35+)
|
|
38
|
-
// Required for Google Play compliance starting Feb 2025 (64-bit apps)
|
|
39
|
-
// This ensures native .so libraries are aligned to 16 KB boundaries.
|
|
40
|
-
// ─────────────────────────────────────────────────────────────────
|
|
41
17
|
externalNativeBuild {
|
|
42
18
|
cmake {
|
|
43
19
|
arguments "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
|
|
44
20
|
}
|
|
45
21
|
}
|
|
46
|
-
|
|
47
22
|
ndk {
|
|
48
|
-
// Only build for 64-bit ABIs — 16 KB page size requirement is 64-bit only
|
|
49
23
|
abiFilters "arm64-v8a", "x86_64"
|
|
50
24
|
}
|
|
51
25
|
}
|
|
@@ -60,8 +34,6 @@ android {
|
|
|
60
34
|
}
|
|
61
35
|
|
|
62
36
|
packagingOptions {
|
|
63
|
-
// Ensure .so files are NOT compressed — required for 16 KB page size support
|
|
64
|
-
// Compressed .so files cannot be aligned in the APK
|
|
65
37
|
jniLibs {
|
|
66
38
|
useLegacyPackaging = false
|
|
67
39
|
}
|
|
@@ -75,13 +47,9 @@ android {
|
|
|
75
47
|
repositories {
|
|
76
48
|
mavenCentral()
|
|
77
49
|
google()
|
|
78
|
-
|
|
79
|
-
// Mapbox Maven repository — requires a secret token (sk.*)
|
|
80
50
|
maven {
|
|
81
51
|
url 'https://api.mapbox.com/downloads/v2/releases/maven'
|
|
82
|
-
authentication {
|
|
83
|
-
basic(BasicAuthentication)
|
|
84
|
-
}
|
|
52
|
+
authentication { basic(BasicAuthentication) }
|
|
85
53
|
credentials {
|
|
86
54
|
username = 'mapbox'
|
|
87
55
|
password = project.hasProperty('MAPBOX_DOWNLOADS_TOKEN')
|
|
@@ -95,22 +63,26 @@ dependencies {
|
|
|
95
63
|
implementation project(':expo-modules-core')
|
|
96
64
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${safeExtGet('kotlinVersion', '1.9.0')}"
|
|
97
65
|
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
// Group changed from com.mapbox.navigation to com.mapbox.navigationcore in v3
|
|
101
|
-
// v3.8.1 ships with mapbox-maps-android 11.11.0 (fixes issue #43)
|
|
102
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
103
|
-
implementation "com.mapbox.navigationcore:android:${MAPBOX_NAVIGATION_VERSION}"
|
|
66
|
+
// Mapbox Navigation SDK v3 core
|
|
67
|
+
implementation "com.mapbox.navigationcore:android:${MAPBOX_NAV_VERSION}"
|
|
104
68
|
|
|
105
|
-
//
|
|
106
|
-
// Prevents gradle from resolving an older transitive version (issue #43 root cause)
|
|
69
|
+
// Mapbox Maps SDK (pinned to minimum version)
|
|
107
70
|
implementation("com.mapbox.maps:android:${MAPBOX_MAPS_MIN_VERSION}") {
|
|
108
71
|
transitive = true
|
|
109
72
|
}
|
|
110
73
|
|
|
111
|
-
//
|
|
74
|
+
// ── Navigation UI Components (Waze-style UI) ─────────────────────────────
|
|
75
|
+
// MapboxManeuverView — turn-by-turn banner
|
|
76
|
+
implementation "com.mapbox.navigationcore:ui-components:${MAPBOX_NAV_VERSION}"
|
|
77
|
+
// MapboxSpeedInfoView — speed limit + current speed
|
|
78
|
+
implementation "com.mapbox.navigationcore:tripdata:${MAPBOX_NAV_VERSION}"
|
|
79
|
+
// Trip progress API — ETA, distance, duration
|
|
80
|
+
implementation "com.mapbox.navigationcore:ui-maps:${MAPBOX_NAV_VERSION}"
|
|
81
|
+
|
|
82
|
+
// AndroidX
|
|
112
83
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
|
|
113
84
|
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
|
|
85
|
+
implementation 'androidx.core:core-ktx:1.13.1'
|
|
114
86
|
}
|
|
115
87
|
|
|
116
88
|
def safeExtGet(prop, fallback) {
|
|
@@ -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
|
}
|
|
@@ -2,14 +2,23 @@ package expo.modules.mapboxnavigation
|
|
|
2
2
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
4
|
import android.content.Context
|
|
5
|
+
import android.graphics.Color
|
|
6
|
+
import android.os.Build
|
|
5
7
|
import android.util.Log
|
|
8
|
+
import android.view.Gravity
|
|
9
|
+
import android.view.View
|
|
10
|
+
import android.widget.FrameLayout
|
|
11
|
+
import android.widget.LinearLayout
|
|
12
|
+
import android.widget.TextView
|
|
13
|
+
import androidx.core.content.ContextCompat
|
|
6
14
|
import com.mapbox.api.directions.v5.models.RouteOptions
|
|
7
15
|
import com.mapbox.geojson.Point
|
|
8
16
|
import com.mapbox.maps.MapView
|
|
9
17
|
import com.mapbox.maps.Style
|
|
10
|
-
import com.mapbox.maps.plugin.animation.camera
|
|
18
|
+
import com.mapbox.maps.plugin.animation.camera
|
|
11
19
|
import com.mapbox.maps.plugin.locationcomponent.location
|
|
12
20
|
import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions
|
|
21
|
+
import com.mapbox.navigation.base.formatter.DistanceFormatterOptions
|
|
13
22
|
import com.mapbox.navigation.base.options.NavigationOptions
|
|
14
23
|
import com.mapbox.navigation.base.route.NavigationRoute
|
|
15
24
|
import com.mapbox.navigation.base.route.NavigationRouterCallback
|
|
@@ -19,16 +28,33 @@ import com.mapbox.navigation.core.MapboxNavigation
|
|
|
19
28
|
import com.mapbox.navigation.core.MapboxNavigationProvider
|
|
20
29
|
import com.mapbox.navigation.core.directions.session.RoutesObserver
|
|
21
30
|
import com.mapbox.navigation.core.directions.session.RoutesUpdatedResult
|
|
31
|
+
import com.mapbox.navigation.core.trip.session.LocationMatcherResult
|
|
32
|
+
import com.mapbox.navigation.core.trip.session.LocationObserver
|
|
22
33
|
import com.mapbox.navigation.core.trip.session.RouteProgressObserver
|
|
34
|
+
import com.mapbox.navigation.tripdata.maneuver.api.MapboxManeuverApi
|
|
35
|
+
import com.mapbox.navigation.tripdata.progress.api.MapboxTripProgressApi
|
|
36
|
+
import com.mapbox.navigation.tripdata.progress.model.DistanceRemainingFormatter
|
|
37
|
+
import com.mapbox.navigation.tripdata.progress.model.EstimatedTimeToArrivalFormatter
|
|
38
|
+
import com.mapbox.navigation.tripdata.progress.model.PercentDistanceTraveledFormatter
|
|
39
|
+
import com.mapbox.navigation.tripdata.progress.model.TimeRemainingFormatter
|
|
40
|
+
import com.mapbox.navigation.tripdata.progress.model.TripProgressUpdateFormatter
|
|
41
|
+
import com.mapbox.navigation.tripdata.speedlimit.api.MapboxSpeedInfoApi
|
|
42
|
+
import com.mapbox.navigation.ui.components.maneuver.view.MapboxManeuverView
|
|
43
|
+
import com.mapbox.navigation.ui.components.speedlimit.view.MapboxSpeedInfoView
|
|
44
|
+
import com.mapbox.navigation.ui.components.tripprogress.view.MapboxTripProgressView
|
|
45
|
+
import com.mapbox.navigation.ui.maps.NavigationStyles
|
|
23
46
|
import com.mapbox.navigation.ui.maps.camera.NavigationCamera
|
|
24
47
|
import com.mapbox.navigation.ui.maps.camera.data.MapboxNavigationViewportDataSource
|
|
25
48
|
import com.mapbox.navigation.ui.maps.camera.transition.NavigationCameraTransitionOptions
|
|
49
|
+
import com.mapbox.navigation.ui.maps.location.NavigationLocationProvider
|
|
26
50
|
import com.mapbox.navigation.ui.maps.route.line.api.MapboxRouteLineApi
|
|
27
51
|
import com.mapbox.navigation.ui.maps.route.line.api.MapboxRouteLineView
|
|
28
52
|
import com.mapbox.navigation.ui.maps.route.line.model.MapboxRouteLineApiOptions
|
|
29
53
|
import com.mapbox.navigation.ui.maps.route.line.model.MapboxRouteLineViewOptions
|
|
30
54
|
import expo.modules.kotlin.AppContext
|
|
55
|
+
import expo.modules.kotlin.viewevent.EventDispatcher
|
|
31
56
|
import expo.modules.kotlin.views.ExpoView
|
|
57
|
+
import java.util.Calendar
|
|
32
58
|
import java.util.Locale
|
|
33
59
|
|
|
34
60
|
private const val TAG = "ExpoMapboxNavigation"
|
|
@@ -36,14 +62,42 @@ private const val TAG = "ExpoMapboxNavigation"
|
|
|
36
62
|
class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
37
63
|
ExpoView(context, appContext) {
|
|
38
64
|
|
|
65
|
+
// ── EventDispatchers ──────────────────────────────────────────────────────
|
|
66
|
+
private val onRouteProgressChanged by EventDispatcher()
|
|
67
|
+
private val onRoutesReady by EventDispatcher()
|
|
68
|
+
private val onNavigationFinished by EventDispatcher()
|
|
69
|
+
private val onNavigationCancelled by EventDispatcher()
|
|
70
|
+
private val onRoutesFailed by EventDispatcher()
|
|
71
|
+
private val onArrival by EventDispatcher()
|
|
72
|
+
|
|
73
|
+
// ── Core views ────────────────────────────────────────────────────────────
|
|
39
74
|
private val mapView: MapView = MapView(context)
|
|
75
|
+
|
|
76
|
+
// ── Overlay UI views (Waze-style) ─────────────────────────────────────────
|
|
77
|
+
private lateinit var maneuverView: MapboxManeuverView
|
|
78
|
+
private lateinit var speedInfoView: MapboxSpeedInfoView
|
|
79
|
+
private lateinit var tripProgressView: MapboxTripProgressView
|
|
80
|
+
private lateinit var etaBottomBar: LinearLayout
|
|
81
|
+
private lateinit var tvEtaTime: TextView
|
|
82
|
+
private lateinit var tvDuration: TextView
|
|
83
|
+
private lateinit var tvDistance: TextView
|
|
84
|
+
private lateinit var btnCancel: TextView
|
|
85
|
+
|
|
86
|
+
// ── Navigation APIs ───────────────────────────────────────────────────────
|
|
40
87
|
private var navigationCamera: NavigationCamera? = null
|
|
41
88
|
private var viewportDataSource: MapboxNavigationViewportDataSource? = null
|
|
42
89
|
private var routeLineApi: MapboxRouteLineApi? = null
|
|
43
90
|
private var routeLineView: MapboxRouteLineView? = null
|
|
44
91
|
private var mapboxNavigation: MapboxNavigation? = null
|
|
92
|
+
private var maneuverApi: MapboxManeuverApi? = null
|
|
93
|
+
private var speedInfoApi: MapboxSpeedInfoApi? = null
|
|
94
|
+
private var tripProgressApi: MapboxTripProgressApi? = null
|
|
95
|
+
private val navigationLocationProvider = NavigationLocationProvider()
|
|
45
96
|
|
|
46
|
-
//
|
|
97
|
+
// ── Night mode state ──────────────────────────────────────────────────────
|
|
98
|
+
private var isNightMode = false
|
|
99
|
+
|
|
100
|
+
// ── Props ─────────────────────────────────────────────────────────────────
|
|
47
101
|
private var coordinates: List<Map<String, Double>> = emptyList()
|
|
48
102
|
private var waypointIndices: List<Int>? = null
|
|
49
103
|
private var language: String? = null
|
|
@@ -59,35 +113,177 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
59
113
|
private var customRasterAboveLayerId: String? = null
|
|
60
114
|
|
|
61
115
|
init {
|
|
62
|
-
|
|
63
|
-
addView(mapView)
|
|
116
|
+
buildOverlayUI()
|
|
64
117
|
setupNavigation()
|
|
65
118
|
}
|
|
66
119
|
|
|
120
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
121
|
+
// Build the Waze-style overlay UI programmatically
|
|
122
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
123
|
+
private fun buildOverlayUI() {
|
|
124
|
+
val root = FrameLayout(context).apply {
|
|
125
|
+
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// MapView full screen
|
|
129
|
+
mapView.layoutParams = FrameLayout.LayoutParams(
|
|
130
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
131
|
+
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)
|
|
162
|
+
|
|
163
|
+
// ── ETA bottom bar ────────────────────────────────────────────────────
|
|
164
|
+
etaBottomBar = LinearLayout(context).apply {
|
|
165
|
+
orientation = LinearLayout.HORIZONTAL
|
|
166
|
+
setBackgroundColor(Color.WHITE)
|
|
167
|
+
elevation = 8 * dp
|
|
168
|
+
layoutParams = FrameLayout.LayoutParams(
|
|
169
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
170
|
+
(80 * dp).toInt()
|
|
171
|
+
).also { it.gravity = Gravity.BOTTOM }
|
|
172
|
+
gravity = Gravity.CENTER_VERTICAL
|
|
173
|
+
setPadding(
|
|
174
|
+
(16 * dp).toInt(), (8 * dp).toInt(),
|
|
175
|
+
(16 * dp).toInt(), (8 * dp).toInt()
|
|
176
|
+
)
|
|
177
|
+
visibility = View.GONE
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ETA time
|
|
181
|
+
tvEtaTime = TextView(context).apply {
|
|
182
|
+
textSize = 22f
|
|
183
|
+
setTextColor(Color.BLACK)
|
|
184
|
+
typeface = android.graphics.Typeface.DEFAULT_BOLD
|
|
185
|
+
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
|
|
186
|
+
}
|
|
187
|
+
etaBottomBar.addView(tvEtaTime)
|
|
188
|
+
|
|
189
|
+
// Duration + Distance (center)
|
|
190
|
+
val centerLayout = LinearLayout(context).apply {
|
|
191
|
+
orientation = LinearLayout.VERTICAL
|
|
192
|
+
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
|
+
}
|
|
200
|
+
tvDistance = TextView(context).apply {
|
|
201
|
+
textSize = 13f
|
|
202
|
+
setTextColor(Color.GRAY)
|
|
203
|
+
gravity = Gravity.CENTER
|
|
204
|
+
}
|
|
205
|
+
centerLayout.addView(tvDuration)
|
|
206
|
+
centerLayout.addView(tvDistance)
|
|
207
|
+
etaBottomBar.addView(centerLayout)
|
|
208
|
+
|
|
209
|
+
// Cancel button
|
|
210
|
+
btnCancel = TextView(context).apply {
|
|
211
|
+
text = "✕"
|
|
212
|
+
textSize = 22f
|
|
213
|
+
setTextColor(Color.DKGRAY)
|
|
214
|
+
gravity = Gravity.END or Gravity.CENTER_VERTICAL
|
|
215
|
+
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
|
|
216
|
+
setOnClickListener { cancelNavigation() }
|
|
217
|
+
}
|
|
218
|
+
etaBottomBar.addView(btnCancel)
|
|
219
|
+
root.addView(etaBottomBar)
|
|
220
|
+
|
|
221
|
+
addView(root)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
225
|
+
// Setup Navigation
|
|
226
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
67
227
|
private fun setupNavigation() {
|
|
68
228
|
val navigationOptions = NavigationOptions.Builder(context).build()
|
|
69
229
|
mapboxNavigation = MapboxNavigationProvider.create(navigationOptions)
|
|
70
230
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
routeLineApi = MapboxRouteLineApi(MapboxRouteLineApiOptions.Builder().build())
|
|
74
|
-
routeLineView = MapboxRouteLineView(
|
|
75
|
-
MapboxRouteLineViewOptions.Builder(context).build()
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
// NavigationCamera — exactly as in official Mapbox example:
|
|
79
|
-
// NavigationCamera(mapboxMap, mapView.camera, viewportDataSource)
|
|
80
|
-
viewportDataSource = MapboxNavigationViewportDataSource(mapView.mapboxMap)
|
|
81
|
-
navigationCamera = NavigationCamera(
|
|
82
|
-
mapView.mapboxMap,
|
|
83
|
-
mapView.camera, // ← mapView.camera (Maps plugin extension)
|
|
84
|
-
viewportDataSource!!
|
|
85
|
-
)
|
|
231
|
+
// Auto day/night style
|
|
232
|
+
val style = mapStyle ?: getAutoStyle()
|
|
86
233
|
|
|
234
|
+
mapView.mapboxMap.loadStyle(style) { _ ->
|
|
235
|
+
setupComponents()
|
|
87
236
|
registerObservers()
|
|
88
237
|
}
|
|
89
238
|
}
|
|
90
239
|
|
|
240
|
+
private fun setupComponents() {
|
|
241
|
+
val mapboxMap = mapView.mapboxMap
|
|
242
|
+
|
|
243
|
+
// Route line
|
|
244
|
+
routeLineApi = MapboxRouteLineApi(MapboxRouteLineApiOptions.Builder().build())
|
|
245
|
+
routeLineView = MapboxRouteLineView(
|
|
246
|
+
MapboxRouteLineViewOptions.Builder(context)
|
|
247
|
+
.routeLineBelowLayerId("road-label-navigation")
|
|
248
|
+
.build()
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
// Camera
|
|
252
|
+
viewportDataSource = MapboxNavigationViewportDataSource(mapboxMap)
|
|
253
|
+
navigationCamera = NavigationCamera(
|
|
254
|
+
mapboxMap,
|
|
255
|
+
mapView.camera,
|
|
256
|
+
viewportDataSource!!
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
// Speed info API
|
|
260
|
+
speedInfoApi = MapboxSpeedInfoApi()
|
|
261
|
+
|
|
262
|
+
// Maneuver API
|
|
263
|
+
val formatter = DistanceFormatterOptions.Builder(context).build()
|
|
264
|
+
maneuverApi = MapboxManeuverApi(
|
|
265
|
+
com.mapbox.navigation.base.formatter.MapboxDistanceFormatter(formatter)
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
// Trip progress API
|
|
269
|
+
val progressFormatter = TripProgressUpdateFormatter.Builder(context)
|
|
270
|
+
.distanceRemainingFormatter(DistanceRemainingFormatter(formatter))
|
|
271
|
+
.timeRemainingFormatter(TimeRemainingFormatter(context))
|
|
272
|
+
.percentRouteTraveledFormatter(PercentDistanceTraveledFormatter())
|
|
273
|
+
.estimatedTimeToArrivalFormatter(EstimatedTimeToArrivalFormatter(context))
|
|
274
|
+
.build()
|
|
275
|
+
tripProgressApi = MapboxTripProgressApi(progressFormatter)
|
|
276
|
+
|
|
277
|
+
// Location puck
|
|
278
|
+
mapView.location.apply {
|
|
279
|
+
setLocationProvider(navigationLocationProvider)
|
|
280
|
+
enabled = true
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
285
|
+
// Register all observers
|
|
286
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
91
287
|
private fun registerObservers() {
|
|
92
288
|
val nav = mapboxNavigation ?: return
|
|
93
289
|
|
|
@@ -101,14 +297,15 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
101
297
|
routeLineView?.renderRouteDrawData(style, value)
|
|
102
298
|
}
|
|
103
299
|
}
|
|
104
|
-
// Update viewport data source with the route
|
|
105
300
|
viewportDataSource?.onRouteChanged(routes.first())
|
|
106
301
|
viewportDataSource?.evaluate()
|
|
107
|
-
|
|
108
|
-
// Issue #43 fix: safe camera following transition
|
|
109
302
|
safeCameraFollowing()
|
|
110
303
|
|
|
111
|
-
|
|
304
|
+
// Show UI overlays
|
|
305
|
+
maneuverView.visibility = View.VISIBLE
|
|
306
|
+
etaBottomBar.visibility = View.VISIBLE
|
|
307
|
+
|
|
308
|
+
onRoutesReady(mapOf(
|
|
112
309
|
"routeCount" to routes.size,
|
|
113
310
|
"distanceMeters" to (routes.first().directionsRoute.distance() ?: 0.0),
|
|
114
311
|
"durationSeconds" to (routes.first().directionsRoute.duration() ?: 0.0)
|
|
@@ -121,17 +318,46 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
121
318
|
}
|
|
122
319
|
viewportDataSource?.clearRouteData()
|
|
123
320
|
viewportDataSource?.evaluate()
|
|
321
|
+
hideNavigationUI()
|
|
124
322
|
}
|
|
125
323
|
}
|
|
126
324
|
})
|
|
127
325
|
|
|
128
|
-
// Route progress observer
|
|
326
|
+
// Route progress observer — maneuvers + ETA + speed limit
|
|
129
327
|
nav.registerRouteProgressObserver(RouteProgressObserver { routeProgress ->
|
|
130
|
-
// Update viewport data source with progress
|
|
131
328
|
viewportDataSource?.onRouteProgressChanged(routeProgress)
|
|
132
329
|
viewportDataSource?.evaluate()
|
|
133
330
|
|
|
134
|
-
|
|
331
|
+
// ── Maneuver instructions ──────────────────────────────────────
|
|
332
|
+
maneuverApi?.getManeuvers(routeProgress) { maneuverState ->
|
|
333
|
+
maneuverView.renderManeuvers(maneuverState)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ── Trip progress (ETA, duration, distance) ────────────────────
|
|
337
|
+
tripProgressApi?.getTripProgress(routeProgress)?.let { tripProgress ->
|
|
338
|
+
val eta = tripProgress.currentLegTimeRemaining
|
|
339
|
+
val cal = Calendar.getInstance().apply {
|
|
340
|
+
add(Calendar.SECOND, eta.toInt())
|
|
341
|
+
}
|
|
342
|
+
val h = cal.get(Calendar.HOUR_OF_DAY)
|
|
343
|
+
val m = cal.get(Calendar.MINUTE)
|
|
344
|
+
tvEtaTime.text = String.format("%02d:%02d", h, m)
|
|
345
|
+
|
|
346
|
+
val totalMin = (tripProgress.totalTimeRemaining / 60).toInt()
|
|
347
|
+
tvDuration.text = if (totalMin >= 60)
|
|
348
|
+
"${totalMin / 60}h ${totalMin % 60}min"
|
|
349
|
+
else
|
|
350
|
+
"${totalMin} min"
|
|
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
|
+
}
|
|
358
|
+
|
|
359
|
+
// ── Emit progress event ───────────────────────────────────────
|
|
360
|
+
onRouteProgressChanged(mapOf(
|
|
135
361
|
"distanceRemaining" to routeProgress.distanceRemaining,
|
|
136
362
|
"durationRemaining" to routeProgress.durationRemaining,
|
|
137
363
|
"distanceTraveled" to routeProgress.distanceTraveled,
|
|
@@ -141,9 +367,110 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
141
367
|
?.currentStepProgress?.distanceRemaining ?: 0f)
|
|
142
368
|
))
|
|
143
369
|
})
|
|
370
|
+
|
|
371
|
+
// Location observer — speed limit + location puck
|
|
372
|
+
nav.registerLocationObserver(object : LocationObserver {
|
|
373
|
+
override fun onNewRawLocation(rawLocation: com.mapbox.common.location.Location) {}
|
|
374
|
+
|
|
375
|
+
override fun onNewLocationMatcherResult(locationMatcherResult: LocationMatcherResult) {
|
|
376
|
+
val enhancedLocation = locationMatcherResult.enhancedLocation
|
|
377
|
+
|
|
378
|
+
// Update location puck
|
|
379
|
+
navigationLocationProvider.changePosition(
|
|
380
|
+
location = enhancedLocation,
|
|
381
|
+
keyPoints = locationMatcherResult.keyPoints,
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
// Update camera
|
|
385
|
+
viewportDataSource?.onLocationChanged(enhancedLocation)
|
|
386
|
+
viewportDataSource?.evaluate()
|
|
387
|
+
|
|
388
|
+
// ── Speed limit (issue — show posted + current speed) ──────
|
|
389
|
+
val formatter = DistanceFormatterOptions.Builder(context).build()
|
|
390
|
+
val info = speedInfoApi?.updatePostedAndCurrentSpeed(
|
|
391
|
+
locationMatcherResult,
|
|
392
|
+
formatter
|
|
393
|
+
)
|
|
394
|
+
if (info != null) {
|
|
395
|
+
speedInfoView.visibility = View.VISIBLE
|
|
396
|
+
speedInfoView.render(info)
|
|
397
|
+
} else {
|
|
398
|
+
speedInfoView.visibility = View.GONE
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ── Auto day/night switch ──────────────────────────────────
|
|
402
|
+
checkAndSwitchDayNight()
|
|
403
|
+
}
|
|
404
|
+
})
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
408
|
+
// Auto day/night mode
|
|
409
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
410
|
+
private fun getAutoStyle(): String {
|
|
411
|
+
val hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY)
|
|
412
|
+
return if (hour in 6..20)
|
|
413
|
+
NavigationStyles.NAVIGATION_DAY_STYLE
|
|
414
|
+
else
|
|
415
|
+
NavigationStyles.NAVIGATION_NIGHT_STYLE
|
|
144
416
|
}
|
|
145
417
|
|
|
146
|
-
|
|
418
|
+
private fun checkAndSwitchDayNight() {
|
|
419
|
+
val hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY)
|
|
420
|
+
val shouldBeNight = hour !in 6..20
|
|
421
|
+
if (shouldBeNight != isNightMode) {
|
|
422
|
+
isNightMode = shouldBeNight
|
|
423
|
+
val newStyle = if (shouldBeNight)
|
|
424
|
+
NavigationStyles.NAVIGATION_NIGHT_STYLE
|
|
425
|
+
else
|
|
426
|
+
NavigationStyles.NAVIGATION_DAY_STYLE
|
|
427
|
+
|
|
428
|
+
mapView.mapboxMap.loadStyle(newStyle) { _ ->
|
|
429
|
+
// Re-render route line on new style
|
|
430
|
+
mapView.mapboxMap.style?.let { style ->
|
|
431
|
+
routeLineApi?.let { api ->
|
|
432
|
+
mapboxNavigation?.getNavigationRoutes()?.let { routes ->
|
|
433
|
+
if (routes.isNotEmpty()) {
|
|
434
|
+
api.setNavigationRoutes(routes) { value ->
|
|
435
|
+
routeLineView?.renderRouteDrawData(style, value)
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
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
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
456
|
+
// Cancel navigation
|
|
457
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
458
|
+
private fun cancelNavigation() {
|
|
459
|
+
mapboxNavigation?.setNavigationRoutes(emptyList())
|
|
460
|
+
mapboxNavigation?.stopTripSession()
|
|
461
|
+
hideNavigationUI()
|
|
462
|
+
onNavigationCancelled(mapOf<String, Any>())
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private fun hideNavigationUI() {
|
|
466
|
+
maneuverView.visibility = View.GONE
|
|
467
|
+
speedInfoView.visibility = View.GONE
|
|
468
|
+
etaBottomBar.visibility = View.GONE
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
472
|
+
// Issue #43: safe camera
|
|
473
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
147
474
|
private fun safeCameraFollowing() {
|
|
148
475
|
try {
|
|
149
476
|
navigationCamera?.requestNavigationCameraToFollowing(
|
|
@@ -152,24 +479,27 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
152
479
|
.build()
|
|
153
480
|
)
|
|
154
481
|
} catch (e: Exception) {
|
|
155
|
-
Log.e(TAG, "Camera transition error
|
|
482
|
+
Log.e(TAG, "Camera transition error: ${e.message}")
|
|
156
483
|
}
|
|
157
484
|
}
|
|
158
485
|
|
|
159
|
-
//
|
|
486
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
487
|
+
// Issue #31: voice units
|
|
488
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
160
489
|
private fun resolveVoiceUnits(): String {
|
|
161
490
|
return when (voiceUnits?.lowercase()) {
|
|
162
491
|
"metric" -> "metric"
|
|
163
492
|
"imperial" -> "imperial"
|
|
164
493
|
else -> {
|
|
165
494
|
val locale = language?.let { Locale.forLanguageTag(it) } ?: Locale.getDefault()
|
|
166
|
-
|
|
167
|
-
if (locale.country in imperialCountries) "imperial" else "metric"
|
|
495
|
+
if (locale.country in setOf("US", "GB", "LR", "MM")) "imperial" else "metric"
|
|
168
496
|
}
|
|
169
497
|
}
|
|
170
498
|
}
|
|
171
499
|
|
|
172
|
-
//
|
|
500
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
501
|
+
// Route request
|
|
502
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
173
503
|
@SuppressLint("MissingPermission")
|
|
174
504
|
private fun fetchRoutes() {
|
|
175
505
|
val nav = mapboxNavigation ?: return
|
|
@@ -183,8 +513,10 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
183
513
|
val builder = RouteOptions.builder()
|
|
184
514
|
.applyDefaultNavigationOptions()
|
|
185
515
|
.language(locale.toLanguageTag())
|
|
186
|
-
.voiceUnits(resolveVoiceUnits())
|
|
516
|
+
.voiceUnits(resolveVoiceUnits())
|
|
187
517
|
.coordinatesList(points)
|
|
518
|
+
// maxspeed annotation required for speed limit display
|
|
519
|
+
.annotations("maxspeed,congestion,duration")
|
|
188
520
|
|
|
189
521
|
waypointIndices?.let { builder.waypointIndicesList(it) }
|
|
190
522
|
navigationProfile?.let {
|
|
@@ -196,7 +528,6 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
196
528
|
maxHeight?.let { builder.maxHeight(it) }
|
|
197
529
|
maxWidth?.let { builder.maxWidth(it) }
|
|
198
530
|
|
|
199
|
-
// Exact Nav SDK v3 callback signature from official Mapbox docs
|
|
200
531
|
nav.requestRoutes(
|
|
201
532
|
builder.build(),
|
|
202
533
|
object : NavigationRouterCallback {
|
|
@@ -205,7 +536,7 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
205
536
|
@RouterOrigin routerOrigin: String
|
|
206
537
|
) {
|
|
207
538
|
if (routes.isEmpty()) {
|
|
208
|
-
|
|
539
|
+
onRoutesFailed(mapOf("message" to "No routes returned"))
|
|
209
540
|
return
|
|
210
541
|
}
|
|
211
542
|
nav.setNavigationRoutes(routes)
|
|
@@ -218,7 +549,7 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
218
549
|
) {
|
|
219
550
|
val msg = reasons.firstOrNull()?.message ?: "Unknown error"
|
|
220
551
|
Log.e(TAG, "Route request failed: $msg")
|
|
221
|
-
|
|
552
|
+
onRoutesFailed(mapOf("message" to msg))
|
|
222
553
|
}
|
|
223
554
|
|
|
224
555
|
override fun onCanceled(
|
|
@@ -231,17 +562,6 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
231
562
|
)
|
|
232
563
|
}
|
|
233
564
|
|
|
234
|
-
// ── Expo event emitter (SDK 53 compatible) ────────────────────────────────
|
|
235
|
-
private fun emitEvent(name: String, data: Map<String, Any>) {
|
|
236
|
-
try {
|
|
237
|
-
// Expo modules core SDK 53: use appContext.eventEmitter
|
|
238
|
-
val emitter = appContext.eventEmitter
|
|
239
|
-
emitter?.emit(name, data)
|
|
240
|
-
} catch (e: Exception) {
|
|
241
|
-
Log.e(TAG, "Failed to emit event $name: ${e.message}")
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
565
|
// ── Prop setters ──────────────────────────────────────────────────────────
|
|
246
566
|
fun setCoordinates(coords: List<Map<String, Double>>) {
|
|
247
567
|
coordinates = coords
|
|
@@ -263,6 +583,7 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
|
|
|
263
583
|
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
|
264
584
|
override fun onDetachedFromWindow() {
|
|
265
585
|
super.onDetachedFromWindow()
|
|
586
|
+
maneuverApi?.cancel()
|
|
266
587
|
routeLineApi?.cancel()
|
|
267
588
|
routeLineView?.cancel()
|
|
268
589
|
mapboxNavigation?.stopTripSession()
|
package/app.plugin.js
CHANGED
|
@@ -4,8 +4,6 @@ const path = require('path');
|
|
|
4
4
|
|
|
5
5
|
const NDK_VERSION = '27.0.12077973';
|
|
6
6
|
const MAPBOX_MAPS_MIN_VERSION = '11.11.0';
|
|
7
|
-
// Navigation SDK v3 uses com.mapbox.navigationcore group
|
|
8
|
-
const MAPBOX_NAV_VERSION = '3.8.1';
|
|
9
7
|
|
|
10
8
|
const withMapboxNavigation = (config, options = {}) => {
|
|
11
9
|
const {
|
|
@@ -22,13 +20,11 @@ const withMapboxNavigation = (config, options = {}) => {
|
|
|
22
20
|
);
|
|
23
21
|
}
|
|
24
22
|
|
|
25
|
-
// Validate mapboxMapsVersion >= 11.11.0
|
|
26
23
|
const [major, minor] = mapboxMapsVersion.split('.').map(Number);
|
|
27
24
|
if (major < 11 || (major === 11 && minor < 11)) {
|
|
28
25
|
throw new Error(
|
|
29
26
|
`[@jacques_gordon/expo-mapbox-navigation] mapboxMapsVersion must be >= 11.11.0.\n` +
|
|
30
|
-
`Provided: ${mapboxMapsVersion}
|
|
31
|
-
`This minimum version is required to fix the CameraAnimationsUtils crash (issue #43).`
|
|
27
|
+
`Provided: ${mapboxMapsVersion}`
|
|
32
28
|
);
|
|
33
29
|
}
|
|
34
30
|
|
|
@@ -36,7 +32,6 @@ const withMapboxNavigation = (config, options = {}) => {
|
|
|
36
32
|
config = withProjectBuildGradle(config, (mod) => {
|
|
37
33
|
let contents = mod.modResults.contents;
|
|
38
34
|
|
|
39
|
-
// Add Mapbox Maven repo if not already present
|
|
40
35
|
if (!contents.includes('api.mapbox.com/downloads/v2/releases/maven')) {
|
|
41
36
|
const mavenBlock = `
|
|
42
37
|
maven {
|
|
@@ -49,7 +44,6 @@ const withMapboxNavigation = (config, options = {}) => {
|
|
|
49
44
|
: System.getenv('MAPBOX_DOWNLOADS_TOKEN') ?: ""
|
|
50
45
|
}
|
|
51
46
|
}`;
|
|
52
|
-
|
|
53
47
|
if (contents.includes('allprojects') && contents.includes('repositories')) {
|
|
54
48
|
contents = contents.replace(
|
|
55
49
|
/allprojects\s*\{[\s\S]*?repositories\s*\{/,
|
|
@@ -58,7 +52,6 @@ const withMapboxNavigation = (config, options = {}) => {
|
|
|
58
52
|
}
|
|
59
53
|
}
|
|
60
54
|
|
|
61
|
-
|
|
62
55
|
mod.modResults.contents = contents;
|
|
63
56
|
return mod;
|
|
64
57
|
});
|
|
@@ -67,37 +60,32 @@ const withMapboxNavigation = (config, options = {}) => {
|
|
|
67
60
|
config = withAppBuildGradle(config, (mod) => {
|
|
68
61
|
let contents = mod.modResults.contents;
|
|
69
62
|
|
|
70
|
-
// Force NDK 27 for 16 KB page size
|
|
71
63
|
if (!contents.includes('ndkVersion')) {
|
|
72
64
|
contents = contents.replace(
|
|
73
65
|
/android\s*\{/,
|
|
74
|
-
`android {\n ndkVersion "${NDK_VERSION}"
|
|
66
|
+
`android {\n ndkVersion "${NDK_VERSION}"`
|
|
75
67
|
);
|
|
76
68
|
} else if (!contents.includes(NDK_VERSION)) {
|
|
77
69
|
contents = contents.replace(
|
|
78
70
|
/ndkVersion\s+["'][^"']*["']/,
|
|
79
|
-
`ndkVersion "${NDK_VERSION}"
|
|
71
|
+
`ndkVersion "${NDK_VERSION}"`
|
|
80
72
|
);
|
|
81
73
|
}
|
|
82
74
|
|
|
83
|
-
// Enable useLegacyPackaging = false for 16 KB page alignment
|
|
84
75
|
if (!contents.includes('useLegacyPackaging')) {
|
|
85
76
|
contents = contents.replace(
|
|
86
77
|
/android\s*\{/,
|
|
87
|
-
`android {\n packagingOptions {\n jniLibs {\n useLegacyPackaging = false
|
|
78
|
+
`android {\n packagingOptions {\n jniLibs {\n useLegacyPackaging = false\n }\n }`
|
|
88
79
|
);
|
|
89
80
|
}
|
|
90
81
|
|
|
91
|
-
// Pin Mapbox Maps + Navigation versions via resolutionStrategy
|
|
92
82
|
const resolutionBlock = `
|
|
93
83
|
configurations.all {
|
|
94
84
|
resolutionStrategy {
|
|
95
|
-
// Force Mapbox Maps SDK >= ${mapboxMapsVersion} — required for NavigationCamera compatibility (issue #43)
|
|
96
85
|
force "com.mapbox.maps:android:${mapboxMapsVersion}"
|
|
97
86
|
force "com.mapbox.maps:android-ndk27:${mapboxMapsVersion}"
|
|
98
87
|
}
|
|
99
88
|
}`;
|
|
100
|
-
|
|
101
89
|
if (!contents.includes('com.mapbox.maps:android:')) {
|
|
102
90
|
contents = contents + '\n' + resolutionBlock;
|
|
103
91
|
}
|
|
@@ -106,31 +94,57 @@ configurations.all {
|
|
|
106
94
|
return mod;
|
|
107
95
|
});
|
|
108
96
|
|
|
109
|
-
// ── Android: AndroidManifest
|
|
97
|
+
// ── Android: AndroidManifest ─────────────────────────────────────────────
|
|
110
98
|
config = withAndroidManifest(config, (mod) => {
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
99
|
+
const manifest = mod.modResults.manifest;
|
|
100
|
+
|
|
101
|
+
// ── Permissions required by Mapbox Navigation SDK (targetSdk 35) ────────
|
|
102
|
+
//
|
|
103
|
+
// SecurityException: Starting FGS with type location requires:
|
|
104
|
+
// - android.permission.FOREGROUND_SERVICE_LOCATION (mandatory API 34+)
|
|
105
|
+
// - android.permission.FOREGROUND_SERVICE (mandatory)
|
|
106
|
+
// - android.permission.ACCESS_FINE_LOCATION (at least one of coarse/fine)
|
|
107
|
+
// - android.permission.ACCESS_COARSE_LOCATION
|
|
108
|
+
//
|
|
109
|
+
const requiredPermissions = [
|
|
110
|
+
'android.permission.ACCESS_FINE_LOCATION',
|
|
111
|
+
'android.permission.ACCESS_COARSE_LOCATION',
|
|
112
|
+
'android.permission.FOREGROUND_SERVICE',
|
|
113
|
+
'android.permission.FOREGROUND_SERVICE_LOCATION', // ← fixes the crash
|
|
114
|
+
'android.permission.POST_NOTIFICATIONS', // needed for nav notification (API 33+)
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
if (!manifest['uses-permission']) {
|
|
118
|
+
manifest['uses-permission'] = [];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const permission of requiredPermissions) {
|
|
122
|
+
const already = manifest['uses-permission'].some(
|
|
123
|
+
(p) => p.$?.['android:name'] === permission
|
|
124
|
+
);
|
|
125
|
+
if (!already) {
|
|
126
|
+
manifest['uses-permission'].push({ $: { 'android:name': permission } });
|
|
115
127
|
}
|
|
128
|
+
}
|
|
116
129
|
|
|
130
|
+
// ── Mapbox access token meta-data ────────────────────────────────────────
|
|
131
|
+
const mainApp = manifest.application?.[0];
|
|
132
|
+
if (mainApp) {
|
|
133
|
+
if (!mainApp['meta-data']) mainApp['meta-data'] = [];
|
|
117
134
|
const tokenMeta = mainApp['meta-data'].find(
|
|
118
135
|
(m) => m.$?.['android:name'] === 'com.mapbox.token'
|
|
119
136
|
);
|
|
120
|
-
|
|
121
137
|
if (!tokenMeta) {
|
|
122
138
|
mainApp['meta-data'].push({
|
|
123
|
-
$: {
|
|
124
|
-
'android:name': 'com.mapbox.token',
|
|
125
|
-
'android:value': accessToken,
|
|
126
|
-
},
|
|
139
|
+
$: { 'android:name': 'com.mapbox.token', 'android:value': accessToken },
|
|
127
140
|
});
|
|
128
141
|
}
|
|
129
142
|
}
|
|
143
|
+
|
|
130
144
|
return mod;
|
|
131
145
|
});
|
|
132
146
|
|
|
133
|
-
// ── Android:
|
|
147
|
+
// ── Android: color overrides ─────────────────────────────────────────────
|
|
134
148
|
if (Object.keys(androidColorOverrides).length > 0) {
|
|
135
149
|
config = withDangerousMod(config, [
|
|
136
150
|
'android',
|
|
@@ -139,27 +153,16 @@ configurations.all {
|
|
|
139
153
|
mod.modRequest.platformProjectRoot,
|
|
140
154
|
'app/src/main/res/values'
|
|
141
155
|
);
|
|
142
|
-
|
|
143
|
-
if (!fs.existsSync(colorsDir)) {
|
|
144
|
-
fs.mkdirSync(colorsDir, { recursive: true });
|
|
145
|
-
}
|
|
156
|
+
if (!fs.existsSync(colorsDir)) fs.mkdirSync(colorsDir, { recursive: true });
|
|
146
157
|
|
|
147
158
|
const colorEntries = Object.entries(androidColorOverrides)
|
|
148
159
|
.map(([name, value]) => ` <color name="${name}">${value}</color>`)
|
|
149
160
|
.join('\n');
|
|
150
161
|
|
|
151
|
-
const colorsXml = `<?xml version="1.0" encoding="utf-8"?>
|
|
152
|
-
<!-- Generated by @jacques_gordon/expo-mapbox-navigation config plugin -->
|
|
153
|
-
<resources>
|
|
154
|
-
${colorEntries}
|
|
155
|
-
</resources>
|
|
156
|
-
`;
|
|
157
|
-
|
|
158
162
|
fs.writeFileSync(
|
|
159
163
|
path.join(colorsDir, 'mapbox_navigation_colors.xml'),
|
|
160
|
-
|
|
164
|
+
`<?xml version="1.0" encoding="utf-8"?>\n<resources>\n${colorEntries}\n</resources>\n`
|
|
161
165
|
);
|
|
162
|
-
|
|
163
166
|
return mod;
|
|
164
167
|
},
|
|
165
168
|
]);
|
|
@@ -177,16 +180,12 @@ ${colorEntries}
|
|
|
177
180
|
mod.modResults.NSLocationAlwaysAndWhenInUseUsageDescription =
|
|
178
181
|
'Your location is used for turn-by-turn navigation even when the app is in the background.';
|
|
179
182
|
}
|
|
180
|
-
|
|
181
|
-
if (!mod.modResults.UIBackgroundModes) {
|
|
182
|
-
mod.modResults.UIBackgroundModes = [];
|
|
183
|
-
}
|
|
183
|
+
if (!mod.modResults.UIBackgroundModes) mod.modResults.UIBackgroundModes = [];
|
|
184
184
|
for (const mode of ['audio', 'location']) {
|
|
185
185
|
if (!mod.modResults.UIBackgroundModes.includes(mode)) {
|
|
186
186
|
mod.modResults.UIBackgroundModes.push(mode);
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
|
-
|
|
190
189
|
return mod;
|
|
191
190
|
});
|
|
192
191
|
|
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.0.9",
|
|
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/plugin/src/index.js
CHANGED
|
@@ -4,8 +4,6 @@ const path = require('path');
|
|
|
4
4
|
|
|
5
5
|
const NDK_VERSION = '27.0.12077973';
|
|
6
6
|
const MAPBOX_MAPS_MIN_VERSION = '11.11.0';
|
|
7
|
-
// Navigation SDK v3 uses com.mapbox.navigationcore group
|
|
8
|
-
const MAPBOX_NAV_VERSION = '3.8.1';
|
|
9
7
|
|
|
10
8
|
const withMapboxNavigation = (config, options = {}) => {
|
|
11
9
|
const {
|
|
@@ -22,13 +20,11 @@ const withMapboxNavigation = (config, options = {}) => {
|
|
|
22
20
|
);
|
|
23
21
|
}
|
|
24
22
|
|
|
25
|
-
// Validate mapboxMapsVersion >= 11.11.0
|
|
26
23
|
const [major, minor] = mapboxMapsVersion.split('.').map(Number);
|
|
27
24
|
if (major < 11 || (major === 11 && minor < 11)) {
|
|
28
25
|
throw new Error(
|
|
29
26
|
`[@jacques_gordon/expo-mapbox-navigation] mapboxMapsVersion must be >= 11.11.0.\n` +
|
|
30
|
-
`Provided: ${mapboxMapsVersion}
|
|
31
|
-
`This minimum version is required to fix the CameraAnimationsUtils crash (issue #43).`
|
|
27
|
+
`Provided: ${mapboxMapsVersion}`
|
|
32
28
|
);
|
|
33
29
|
}
|
|
34
30
|
|
|
@@ -36,7 +32,6 @@ const withMapboxNavigation = (config, options = {}) => {
|
|
|
36
32
|
config = withProjectBuildGradle(config, (mod) => {
|
|
37
33
|
let contents = mod.modResults.contents;
|
|
38
34
|
|
|
39
|
-
// Add Mapbox Maven repo if not already present
|
|
40
35
|
if (!contents.includes('api.mapbox.com/downloads/v2/releases/maven')) {
|
|
41
36
|
const mavenBlock = `
|
|
42
37
|
maven {
|
|
@@ -49,7 +44,6 @@ const withMapboxNavigation = (config, options = {}) => {
|
|
|
49
44
|
: System.getenv('MAPBOX_DOWNLOADS_TOKEN') ?: ""
|
|
50
45
|
}
|
|
51
46
|
}`;
|
|
52
|
-
|
|
53
47
|
if (contents.includes('allprojects') && contents.includes('repositories')) {
|
|
54
48
|
contents = contents.replace(
|
|
55
49
|
/allprojects\s*\{[\s\S]*?repositories\s*\{/,
|
|
@@ -58,7 +52,6 @@ const withMapboxNavigation = (config, options = {}) => {
|
|
|
58
52
|
}
|
|
59
53
|
}
|
|
60
54
|
|
|
61
|
-
|
|
62
55
|
mod.modResults.contents = contents;
|
|
63
56
|
return mod;
|
|
64
57
|
});
|
|
@@ -67,37 +60,32 @@ const withMapboxNavigation = (config, options = {}) => {
|
|
|
67
60
|
config = withAppBuildGradle(config, (mod) => {
|
|
68
61
|
let contents = mod.modResults.contents;
|
|
69
62
|
|
|
70
|
-
// Force NDK 27 for 16 KB page size
|
|
71
63
|
if (!contents.includes('ndkVersion')) {
|
|
72
64
|
contents = contents.replace(
|
|
73
65
|
/android\s*\{/,
|
|
74
|
-
`android {\n ndkVersion "${NDK_VERSION}"
|
|
66
|
+
`android {\n ndkVersion "${NDK_VERSION}"`
|
|
75
67
|
);
|
|
76
68
|
} else if (!contents.includes(NDK_VERSION)) {
|
|
77
69
|
contents = contents.replace(
|
|
78
70
|
/ndkVersion\s+["'][^"']*["']/,
|
|
79
|
-
`ndkVersion "${NDK_VERSION}"
|
|
71
|
+
`ndkVersion "${NDK_VERSION}"`
|
|
80
72
|
);
|
|
81
73
|
}
|
|
82
74
|
|
|
83
|
-
// Enable useLegacyPackaging = false for 16 KB page alignment
|
|
84
75
|
if (!contents.includes('useLegacyPackaging')) {
|
|
85
76
|
contents = contents.replace(
|
|
86
77
|
/android\s*\{/,
|
|
87
|
-
`android {\n packagingOptions {\n jniLibs {\n useLegacyPackaging = false
|
|
78
|
+
`android {\n packagingOptions {\n jniLibs {\n useLegacyPackaging = false\n }\n }`
|
|
88
79
|
);
|
|
89
80
|
}
|
|
90
81
|
|
|
91
|
-
// Pin Mapbox Maps + Navigation versions via resolutionStrategy
|
|
92
82
|
const resolutionBlock = `
|
|
93
83
|
configurations.all {
|
|
94
84
|
resolutionStrategy {
|
|
95
|
-
// Force Mapbox Maps SDK >= ${mapboxMapsVersion} — required for NavigationCamera compatibility (issue #43)
|
|
96
85
|
force "com.mapbox.maps:android:${mapboxMapsVersion}"
|
|
97
86
|
force "com.mapbox.maps:android-ndk27:${mapboxMapsVersion}"
|
|
98
87
|
}
|
|
99
88
|
}`;
|
|
100
|
-
|
|
101
89
|
if (!contents.includes('com.mapbox.maps:android:')) {
|
|
102
90
|
contents = contents + '\n' + resolutionBlock;
|
|
103
91
|
}
|
|
@@ -106,31 +94,57 @@ configurations.all {
|
|
|
106
94
|
return mod;
|
|
107
95
|
});
|
|
108
96
|
|
|
109
|
-
// ── Android: AndroidManifest
|
|
97
|
+
// ── Android: AndroidManifest ─────────────────────────────────────────────
|
|
110
98
|
config = withAndroidManifest(config, (mod) => {
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
99
|
+
const manifest = mod.modResults.manifest;
|
|
100
|
+
|
|
101
|
+
// ── Permissions required by Mapbox Navigation SDK (targetSdk 35) ────────
|
|
102
|
+
//
|
|
103
|
+
// SecurityException: Starting FGS with type location requires:
|
|
104
|
+
// - android.permission.FOREGROUND_SERVICE_LOCATION (mandatory API 34+)
|
|
105
|
+
// - android.permission.FOREGROUND_SERVICE (mandatory)
|
|
106
|
+
// - android.permission.ACCESS_FINE_LOCATION (at least one of coarse/fine)
|
|
107
|
+
// - android.permission.ACCESS_COARSE_LOCATION
|
|
108
|
+
//
|
|
109
|
+
const requiredPermissions = [
|
|
110
|
+
'android.permission.ACCESS_FINE_LOCATION',
|
|
111
|
+
'android.permission.ACCESS_COARSE_LOCATION',
|
|
112
|
+
'android.permission.FOREGROUND_SERVICE',
|
|
113
|
+
'android.permission.FOREGROUND_SERVICE_LOCATION', // ← fixes the crash
|
|
114
|
+
'android.permission.POST_NOTIFICATIONS', // needed for nav notification (API 33+)
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
if (!manifest['uses-permission']) {
|
|
118
|
+
manifest['uses-permission'] = [];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const permission of requiredPermissions) {
|
|
122
|
+
const already = manifest['uses-permission'].some(
|
|
123
|
+
(p) => p.$?.['android:name'] === permission
|
|
124
|
+
);
|
|
125
|
+
if (!already) {
|
|
126
|
+
manifest['uses-permission'].push({ $: { 'android:name': permission } });
|
|
115
127
|
}
|
|
128
|
+
}
|
|
116
129
|
|
|
130
|
+
// ── Mapbox access token meta-data ────────────────────────────────────────
|
|
131
|
+
const mainApp = manifest.application?.[0];
|
|
132
|
+
if (mainApp) {
|
|
133
|
+
if (!mainApp['meta-data']) mainApp['meta-data'] = [];
|
|
117
134
|
const tokenMeta = mainApp['meta-data'].find(
|
|
118
135
|
(m) => m.$?.['android:name'] === 'com.mapbox.token'
|
|
119
136
|
);
|
|
120
|
-
|
|
121
137
|
if (!tokenMeta) {
|
|
122
138
|
mainApp['meta-data'].push({
|
|
123
|
-
$: {
|
|
124
|
-
'android:name': 'com.mapbox.token',
|
|
125
|
-
'android:value': accessToken,
|
|
126
|
-
},
|
|
139
|
+
$: { 'android:name': 'com.mapbox.token', 'android:value': accessToken },
|
|
127
140
|
});
|
|
128
141
|
}
|
|
129
142
|
}
|
|
143
|
+
|
|
130
144
|
return mod;
|
|
131
145
|
});
|
|
132
146
|
|
|
133
|
-
// ── Android:
|
|
147
|
+
// ── Android: color overrides ─────────────────────────────────────────────
|
|
134
148
|
if (Object.keys(androidColorOverrides).length > 0) {
|
|
135
149
|
config = withDangerousMod(config, [
|
|
136
150
|
'android',
|
|
@@ -139,27 +153,16 @@ configurations.all {
|
|
|
139
153
|
mod.modRequest.platformProjectRoot,
|
|
140
154
|
'app/src/main/res/values'
|
|
141
155
|
);
|
|
142
|
-
|
|
143
|
-
if (!fs.existsSync(colorsDir)) {
|
|
144
|
-
fs.mkdirSync(colorsDir, { recursive: true });
|
|
145
|
-
}
|
|
156
|
+
if (!fs.existsSync(colorsDir)) fs.mkdirSync(colorsDir, { recursive: true });
|
|
146
157
|
|
|
147
158
|
const colorEntries = Object.entries(androidColorOverrides)
|
|
148
159
|
.map(([name, value]) => ` <color name="${name}">${value}</color>`)
|
|
149
160
|
.join('\n');
|
|
150
161
|
|
|
151
|
-
const colorsXml = `<?xml version="1.0" encoding="utf-8"?>
|
|
152
|
-
<!-- Generated by @jacques_gordon/expo-mapbox-navigation config plugin -->
|
|
153
|
-
<resources>
|
|
154
|
-
${colorEntries}
|
|
155
|
-
</resources>
|
|
156
|
-
`;
|
|
157
|
-
|
|
158
162
|
fs.writeFileSync(
|
|
159
163
|
path.join(colorsDir, 'mapbox_navigation_colors.xml'),
|
|
160
|
-
|
|
164
|
+
`<?xml version="1.0" encoding="utf-8"?>\n<resources>\n${colorEntries}\n</resources>\n`
|
|
161
165
|
);
|
|
162
|
-
|
|
163
166
|
return mod;
|
|
164
167
|
},
|
|
165
168
|
]);
|
|
@@ -177,16 +180,12 @@ ${colorEntries}
|
|
|
177
180
|
mod.modResults.NSLocationAlwaysAndWhenInUseUsageDescription =
|
|
178
181
|
'Your location is used for turn-by-turn navigation even when the app is in the background.';
|
|
179
182
|
}
|
|
180
|
-
|
|
181
|
-
if (!mod.modResults.UIBackgroundModes) {
|
|
182
|
-
mod.modResults.UIBackgroundModes = [];
|
|
183
|
-
}
|
|
183
|
+
if (!mod.modResults.UIBackgroundModes) mod.modResults.UIBackgroundModes = [];
|
|
184
184
|
for (const mode of ['audio', 'location']) {
|
|
185
185
|
if (!mod.modResults.UIBackgroundModes.includes(mode)) {
|
|
186
186
|
mod.modResults.UIBackgroundModes.push(mode);
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
|
-
|
|
190
189
|
return mod;
|
|
191
190
|
});
|
|
192
191
|
|