@baeckerherz/expo-mapbox-navigation 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +176 -0
- package/android/build.gradle +74 -0
- package/android/src/main/java/expo/modules/mapboxnavigation/ExpoMapboxNavigationModule.kt +66 -0
- package/android/src/main/java/expo/modules/mapboxnavigation/ExpoMapboxNavigationView.kt +164 -0
- package/expo-module.config.json +9 -0
- package/ios/ExpoMapboxNavigation.podspec +26 -0
- package/ios/ExpoMapboxNavigationModule.swift +66 -0
- package/ios/ExpoMapboxNavigationView.swift +377 -0
- package/package.json +52 -0
- package/plugin/build/index.d.ts +11 -0
- package/plugin/build/index.js +36 -0
- package/plugin/build/withMapboxNavGradle.d.ts +6 -0
- package/plugin/build/withMapboxNavGradle.js +31 -0
- package/plugin/build/withMapboxNavPodfile.d.ts +7 -0
- package/plugin/build/withMapboxNavPodfile.js +79 -0
- package/plugin/build/withMapboxNavSPM.d.ts +13 -0
- package/plugin/build/withMapboxNavSPM.js +80 -0
- package/plugin/src/index.ts +63 -0
- package/plugin/src/withMapboxNavGradle.ts +34 -0
- package/plugin/src/withMapboxNavPodfile.ts +56 -0
- package/plugin/src/withMapboxNavSPM.ts +124 -0
- package/src/ExpoMapboxNavigation.tsx +16 -0
- package/src/index.ts +8 -0
- package/src/types.ts +122 -0
package/README.md
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# @baeckerherz/expo-mapbox-navigation
|
|
2
|
+
|
|
3
|
+
> **WARNING: This is a prototype and actively under development.** APIs may change without notice. Not recommended for production use yet. Contributions and feedback are welcome.
|
|
4
|
+
|
|
5
|
+
Expo module wrapping [Mapbox Navigation SDK v3](https://docs.mapbox.com/ios/navigation/guides/) for iOS and Android. A clean, maintainable alternative to existing community wrappers.
|
|
6
|
+
|
|
7
|
+
## Why this exists
|
|
8
|
+
|
|
9
|
+
Existing React Native / Expo wrappers for Mapbox Navigation have significant issues:
|
|
10
|
+
|
|
11
|
+
- **@badatgil/expo-mapbox-navigation**: Bundles vendored `.xcframework` files for iOS (fragile, requires manual rebuild for every Mapbox SDK update). Android assembles the navigation UI from ~30 individual components in ~1100 lines of Kotlin instead of using Mapbox's drop-in view.
|
|
12
|
+
- **@homee/react-native-mapbox-navigation**: Abandoned (no activity since 2022), locked to Nav SDK v2.1.1, no Expo support, crashes on Android 13+.
|
|
13
|
+
|
|
14
|
+
## Architecture
|
|
15
|
+
|
|
16
|
+
### iOS: SPM via Config Plugin (not vendored xcframeworks)
|
|
17
|
+
|
|
18
|
+
Mapbox Navigation SDK v3 for iOS dropped CocoaPods and only supports Swift Package Manager. Since Expo uses CocoaPods, we use an **Expo config plugin** that injects SPM package references into the Xcode project's `.pbxproj` at prebuild time. Version bumps are a single string change -- no manual xcframework rebuilding.
|
|
19
|
+
|
|
20
|
+
### Android: Drop-in NavigationView
|
|
21
|
+
|
|
22
|
+
Android Nav SDK v3 provides a `NavigationView` drop-in component. We wrap it directly instead of rebuilding the UI from scratch.
|
|
23
|
+
|
|
24
|
+
### Both platforms: Expo Module API
|
|
25
|
+
|
|
26
|
+
Uses `expo-modules-core` for native bridging, giving us:
|
|
27
|
+
- Fabric / New Architecture compatibility
|
|
28
|
+
- Type-safe props and events in Swift/Kotlin
|
|
29
|
+
- Clean `EventDispatcher` pattern
|
|
30
|
+
- Works in Expo and bare RN projects
|
|
31
|
+
|
|
32
|
+
## API
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
import { MapboxNavigation } from '@baeckerherz/expo-mapbox-navigation';
|
|
36
|
+
|
|
37
|
+
<MapboxNavigation
|
|
38
|
+
coordinates={[
|
|
39
|
+
{ latitude: 47.2692, longitude: 11.4041 },
|
|
40
|
+
{ latitude: 48.2082, longitude: 16.3738 },
|
|
41
|
+
]}
|
|
42
|
+
locale="de"
|
|
43
|
+
onRouteProgressChanged={(e) => {
|
|
44
|
+
console.log(e.nativeEvent.distanceRemaining);
|
|
45
|
+
}}
|
|
46
|
+
onCancelNavigation={() => navigation.goBack()}
|
|
47
|
+
onFinalDestinationArrival={() => console.log('Arrived!')}
|
|
48
|
+
style={{ flex: 1 }}
|
|
49
|
+
/>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Props
|
|
53
|
+
|
|
54
|
+
| Prop | Type | Description |
|
|
55
|
+
|------|------|-------------|
|
|
56
|
+
| `coordinates` | `Array<{ latitude, longitude }>` | Route waypoints (min 2). First = origin, last = destination. |
|
|
57
|
+
| `waypointIndices` | `number[]` | Which coordinates are full waypoints vs. via-points. |
|
|
58
|
+
| `locale` | `string` | Language for voice guidance and UI. Default: device locale. |
|
|
59
|
+
| `routeProfile` | `string` | Routing profile. Default: `"mapbox/driving-traffic"`. |
|
|
60
|
+
| `mute` | `boolean` | Mute voice guidance. |
|
|
61
|
+
|
|
62
|
+
### Events
|
|
63
|
+
|
|
64
|
+
| Event | Payload | Description |
|
|
65
|
+
|-------|---------|-------------|
|
|
66
|
+
| `onRouteProgressChanged` | `{ distanceRemaining, durationRemaining, distanceTraveled, fractionTraveled }` | Fires as the user progresses along the route. |
|
|
67
|
+
| `onCancelNavigation` | — | User tapped cancel / back. |
|
|
68
|
+
| `onWaypointArrival` | `{ waypointIndex }` | Arrived at an intermediate waypoint. |
|
|
69
|
+
| `onFinalDestinationArrival` | — | Arrived at the final destination. |
|
|
70
|
+
| `onRouteChanged` | — | Route was recalculated (reroute). |
|
|
71
|
+
| `onUserOffRoute` | — | User went off the planned route. |
|
|
72
|
+
| `onError` | `{ message }` | Navigation error occurred. |
|
|
73
|
+
|
|
74
|
+
## Installation (for consumers)
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npx expo install @baeckerherz/expo-mapbox-navigation
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Add the plugin to `app.config.ts`:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
plugins: [
|
|
84
|
+
["@baeckerherz/expo-mapbox-navigation/plugin", {
|
|
85
|
+
mapboxAccessToken: "pk.eyJ1...",
|
|
86
|
+
mapboxSecretToken: "sk.eyJ1...", // for SPM download auth
|
|
87
|
+
navigationSdkVersion: "3.5.0",
|
|
88
|
+
}],
|
|
89
|
+
]
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Rebuild native:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npx expo prebuild --clean
|
|
96
|
+
npx expo run:ios
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Development
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
cd @baeckerherz/expo-mapbox-navigation
|
|
103
|
+
yarn install
|
|
104
|
+
cd example && npx expo run:ios
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Project Structure
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
src/ TypeScript API (component, types, exports)
|
|
111
|
+
ios/ Swift native module + podspec
|
|
112
|
+
android/ Kotlin native module + build.gradle
|
|
113
|
+
plugin/ Expo config plugins (SPM injection, Gradle setup)
|
|
114
|
+
example/ Test app
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Prerequisites
|
|
118
|
+
|
|
119
|
+
1. A [Mapbox account](https://account.mapbox.com/) with Navigation SDK access
|
|
120
|
+
2. A public access token (`pk.xxx`) and a secret/download token (`sk.xxx`)
|
|
121
|
+
3. For iOS: `~/.netrc` must contain Mapbox credentials for SPM package resolution:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
machine api.mapbox.com
|
|
125
|
+
login mapbox
|
|
126
|
+
password sk.eyJ1...YOUR_SECRET_TOKEN
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## How to test the example app
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
cd @baeckerherz/expo-mapbox-navigation/example
|
|
133
|
+
yarn install
|
|
134
|
+
npx expo prebuild --clean
|
|
135
|
+
npx expo run:ios --device
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The example app navigates from your current location to Innsbruck Hauptbahnhof with German voice guidance.
|
|
139
|
+
|
|
140
|
+
## Publishing to npm
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
npm login
|
|
144
|
+
npm publish --access public
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Consumers install with:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
npx expo install @baeckerherz/expo-mapbox-navigation
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Key differences from existing wrappers
|
|
154
|
+
|
|
155
|
+
| | This module | @badatgil/expo-mapbox-navigation | @homee/react-native-mapbox-navigation |
|
|
156
|
+
|---|---|---|---|
|
|
157
|
+
| iOS SDK integration | SPM via config plugin (clean version bumps) | Vendored .xcframeworks (manual rebuild per update) | CocoaPods pinned to Nav SDK v2 |
|
|
158
|
+
| Android approach | Drop-in NavigationView | Custom UI from ~30 components (~1100 LOC) | Custom UI (~500 LOC) |
|
|
159
|
+
| Nav SDK version | v3 (current) | v3 | v2 (legacy) |
|
|
160
|
+
| Expo Module API | Yes (Fabric-ready) | Yes | No (legacy bridge) |
|
|
161
|
+
| Multi-waypoint | Yes | Yes | No (origin + destination only) |
|
|
162
|
+
| Maintenance | Active | Semi-active | Abandoned |
|
|
163
|
+
|
|
164
|
+
## Status
|
|
165
|
+
|
|
166
|
+
**Prototype** — not production-ready. This is a proof of concept to validate:
|
|
167
|
+
|
|
168
|
+
1. SPM injection via config plugin works reliably with Xcode + CocoaPods
|
|
169
|
+
2. Drop-in NavigationView/NavigationViewController integration is sufficient
|
|
170
|
+
3. Event bridging covers the required use cases
|
|
171
|
+
|
|
172
|
+
### Known risks
|
|
173
|
+
|
|
174
|
+
- **SPM + CocoaPods coexistence**: Xcode may have trouble resolving both dependency managers. If this fails, fallback is to vendor `.xcframework` files (Approach A from the plan).
|
|
175
|
+
- **Android NavigationView completeness**: The drop-in `NavigationView` in Android Nav SDK v3 may require additional configuration for full feature parity with iOS.
|
|
176
|
+
- **Mapbox licensing**: The Navigation SDK requires a commercial Mapbox license for production use. This wrapper does not change that requirement.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
apply plugin: 'com.android.library'
|
|
2
|
+
apply plugin: 'kotlin-android'
|
|
3
|
+
apply plugin: 'maven-publish'
|
|
4
|
+
|
|
5
|
+
group = 'expo.modules.mapboxnavigation'
|
|
6
|
+
version = '0.1.0'
|
|
7
|
+
|
|
8
|
+
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
|
9
|
+
if (expoModulesCorePlugin.exists()) {
|
|
10
|
+
apply from: expoModulesCorePlugin
|
|
11
|
+
applyKotlinExpoModulesCorePlugin()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
buildscript {
|
|
15
|
+
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
|
16
|
+
if (expoModulesCorePlugin.exists()) {
|
|
17
|
+
apply from: expoModulesCorePlugin
|
|
18
|
+
applyKotlinExpoModulesCorePlugin()
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
android {
|
|
23
|
+
namespace "expo.modules.mapboxnavigation"
|
|
24
|
+
|
|
25
|
+
compileSdkVersion safeExtGet("compileSdkVersion", 34)
|
|
26
|
+
|
|
27
|
+
defaultConfig {
|
|
28
|
+
minSdkVersion safeExtGet("minSdkVersion", 23)
|
|
29
|
+
targetSdkVersion safeExtGet("targetSdkVersion", 34)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
publishing {
|
|
33
|
+
singleVariant("release") {
|
|
34
|
+
withSourcesJar()
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
lintOptions {
|
|
39
|
+
abortOnError false
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
compileOptions {
|
|
43
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
44
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
kotlinOptions {
|
|
48
|
+
jvmTarget = JavaVersion.VERSION_17.majorVersion
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
repositories {
|
|
53
|
+
mavenCentral()
|
|
54
|
+
maven {
|
|
55
|
+
url = uri("https://api.mapbox.com/downloads/v2/releases/maven")
|
|
56
|
+
authentication {
|
|
57
|
+
basic(BasicAuthentication)
|
|
58
|
+
}
|
|
59
|
+
credentials {
|
|
60
|
+
username = "mapbox"
|
|
61
|
+
password = project.findProperty('MAPBOX_DOWNLOADS_TOKEN') ?: System.getenv('MAPBOX_DOWNLOADS_TOKEN') ?: ""
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
dependencies {
|
|
67
|
+
implementation project(':expo-modules-core')
|
|
68
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
|
|
69
|
+
|
|
70
|
+
// Mapbox Navigation SDK v3 - drop-in UI
|
|
71
|
+
implementation "com.mapbox.navigationcore:android:3.5.0"
|
|
72
|
+
implementation "com.mapbox.navigationcore:dropin:3.5.0"
|
|
73
|
+
implementation "com.mapbox.navigationcore:ui-maps:3.5.0"
|
|
74
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
package expo.modules.mapboxnavigation
|
|
2
|
+
|
|
3
|
+
import expo.modules.kotlin.modules.Module
|
|
4
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
|
5
|
+
|
|
6
|
+
class ExpoMapboxNavigationModule : Module() {
|
|
7
|
+
override fun definition() = ModuleDefinition {
|
|
8
|
+
Name("ExpoMapboxNavigation")
|
|
9
|
+
|
|
10
|
+
View(ExpoMapboxNavigationView::class) {
|
|
11
|
+
Events(
|
|
12
|
+
"onRouteProgressChanged",
|
|
13
|
+
"onCancelNavigation",
|
|
14
|
+
"onWaypointArrival",
|
|
15
|
+
"onFinalDestinationArrival",
|
|
16
|
+
"onRouteChanged",
|
|
17
|
+
"onUserOffRoute",
|
|
18
|
+
"onError"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
Prop("coordinates") { view: ExpoMapboxNavigationView, coordinates: List<Map<String, Double>> ->
|
|
22
|
+
view.setCoordinates(coordinates)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
Prop("waypointIndices") { view: ExpoMapboxNavigationView, indices: List<Int> ->
|
|
26
|
+
view.waypointIndices = indices
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
Prop("locale") { view: ExpoMapboxNavigationView, locale: String ->
|
|
30
|
+
view.navigationLocale = locale
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
Prop("routeProfile") { view: ExpoMapboxNavigationView, profile: String ->
|
|
34
|
+
view.routeProfile = profile
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
Prop("mute") { view: ExpoMapboxNavigationView, mute: Boolean ->
|
|
38
|
+
view.isMuted = mute
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
Prop("mapStyle") { view: ExpoMapboxNavigationView, style: String ->
|
|
42
|
+
view.mapStyleURL = style
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
Prop("themeMode") { view: ExpoMapboxNavigationView, mode: String ->
|
|
46
|
+
view.themeMode = mode
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
Prop("accentColor") { view: ExpoMapboxNavigationView, color: String ->
|
|
50
|
+
view.accentColorHex = color
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
Prop("routeColor") { view: ExpoMapboxNavigationView, color: String ->
|
|
54
|
+
view.routeColorHex = color
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
Prop("bannerBackgroundColor") { view: ExpoMapboxNavigationView, color: String ->
|
|
58
|
+
view.bannerBackgroundColorHex = color
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
Prop("bannerTextColor") { view: ExpoMapboxNavigationView, color: String ->
|
|
62
|
+
view.bannerTextColorHex = color
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
package expo.modules.mapboxnavigation
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.widget.FrameLayout
|
|
5
|
+
import expo.modules.kotlin.AppContext
|
|
6
|
+
import expo.modules.kotlin.viewevent.EventDispatcher
|
|
7
|
+
import expo.modules.kotlin.views.ExpoView
|
|
8
|
+
import com.mapbox.geojson.Point
|
|
9
|
+
import com.mapbox.navigation.base.route.NavigationRoute
|
|
10
|
+
import com.mapbox.navigation.core.MapboxNavigation
|
|
11
|
+
import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp
|
|
12
|
+
import com.mapbox.navigation.core.trip.session.RouteProgressObserver
|
|
13
|
+
import com.mapbox.navigation.core.trip.session.OffRouteObserver
|
|
14
|
+
import com.mapbox.navigation.core.directions.session.RoutesObserver
|
|
15
|
+
import com.mapbox.navigation.dropin.NavigationView
|
|
16
|
+
import com.mapbox.api.directions.v5.DirectionsCriteria
|
|
17
|
+
import com.mapbox.api.directions.v5.models.RouteOptions
|
|
18
|
+
|
|
19
|
+
class ExpoMapboxNavigationView(
|
|
20
|
+
context: Context,
|
|
21
|
+
appContext: AppContext
|
|
22
|
+
) : ExpoView(context, appContext) {
|
|
23
|
+
|
|
24
|
+
private val onRouteProgressChanged by EventDispatcher()
|
|
25
|
+
private val onCancelNavigation by EventDispatcher()
|
|
26
|
+
private val onWaypointArrival by EventDispatcher()
|
|
27
|
+
private val onFinalDestinationArrival by EventDispatcher()
|
|
28
|
+
private val onRouteChanged by EventDispatcher()
|
|
29
|
+
private val onUserOffRoute by EventDispatcher()
|
|
30
|
+
private val onError by EventDispatcher()
|
|
31
|
+
|
|
32
|
+
private var coordinates: List<Point> = emptyList()
|
|
33
|
+
var waypointIndices: List<Int>? = null
|
|
34
|
+
var navigationLocale: String? = null
|
|
35
|
+
var routeProfile: String? = null
|
|
36
|
+
var isMuted: Boolean = false
|
|
37
|
+
var mapStyleURL: String? = null
|
|
38
|
+
var themeMode: String? = null
|
|
39
|
+
var accentColorHex: String? = null
|
|
40
|
+
var routeColorHex: String? = null
|
|
41
|
+
var bannerBackgroundColorHex: String? = null
|
|
42
|
+
var bannerTextColorHex: String? = null
|
|
43
|
+
|
|
44
|
+
private var navigationView: NavigationView? = null
|
|
45
|
+
private var hasStartedNavigation = false
|
|
46
|
+
|
|
47
|
+
fun setCoordinates(raw: List<Map<String, Double>>) {
|
|
48
|
+
coordinates = raw.mapNotNull { map ->
|
|
49
|
+
val lat = map["latitude"] ?: return@mapNotNull null
|
|
50
|
+
val lng = map["longitude"] ?: return@mapNotNull null
|
|
51
|
+
Point.fromLngLat(lng, lat)
|
|
52
|
+
}
|
|
53
|
+
startNavigationIfReady()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private fun startNavigationIfReady() {
|
|
57
|
+
if (coordinates.size < 2 || hasStartedNavigation) return
|
|
58
|
+
hasStartedNavigation = true
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
val navView = NavigationView(context)
|
|
62
|
+
navView.layoutParams = FrameLayout.LayoutParams(
|
|
63
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
64
|
+
FrameLayout.LayoutParams.MATCH_PARENT
|
|
65
|
+
)
|
|
66
|
+
addView(navView)
|
|
67
|
+
navigationView = navView
|
|
68
|
+
|
|
69
|
+
val profile = routeProfile ?: DirectionsCriteria.PROFILE_DRIVING_TRAFFIC
|
|
70
|
+
val routeOptions = RouteOptions.builder()
|
|
71
|
+
.coordinatesList(coordinates)
|
|
72
|
+
.profile(profile)
|
|
73
|
+
.alternatives(true)
|
|
74
|
+
.continueStraight(true)
|
|
75
|
+
.overview(DirectionsCriteria.OVERVIEW_FULL)
|
|
76
|
+
.steps(true)
|
|
77
|
+
.voiceInstructions(true)
|
|
78
|
+
.bannerInstructions(true)
|
|
79
|
+
.apply {
|
|
80
|
+
waypointIndices?.let { indices ->
|
|
81
|
+
waypointIndices(indices.joinToString(separator = ";"))
|
|
82
|
+
}
|
|
83
|
+
navigationLocale?.let { locale ->
|
|
84
|
+
language(locale)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
.build()
|
|
88
|
+
|
|
89
|
+
MapboxNavigationApp.current()?.let { navigation ->
|
|
90
|
+
registerObservers(navigation)
|
|
91
|
+
|
|
92
|
+
navigation.requestRoutes(routeOptions, object :
|
|
93
|
+
com.mapbox.navigation.core.directions.session.RoutesRequestCallback {
|
|
94
|
+
override fun onRoutesReady(routes: List<NavigationRoute>) {
|
|
95
|
+
if (routes.isNotEmpty()) {
|
|
96
|
+
navigation.setNavigationRoutes(routes)
|
|
97
|
+
} else {
|
|
98
|
+
onError(mapOf("message" to "No routes found"))
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
override fun onRoutesRequestFailure(
|
|
103
|
+
throwable: Throwable,
|
|
104
|
+
routeOptions: RouteOptions
|
|
105
|
+
) {
|
|
106
|
+
onError(mapOf("message" to "Route request failed: ${throwable.message}"))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
override fun onRoutesRequestCanceled(routeOptions: RouteOptions) {
|
|
110
|
+
onError(mapOf("message" to "Route request cancelled"))
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
} ?: run {
|
|
114
|
+
onError(mapOf("message" to "MapboxNavigation not initialized"))
|
|
115
|
+
}
|
|
116
|
+
} catch (e: Exception) {
|
|
117
|
+
onError(mapOf("message" to "Navigation setup failed: ${e.message}"))
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private fun registerObservers(navigation: MapboxNavigation) {
|
|
122
|
+
navigation.registerRouteProgressObserver(RouteProgressObserver { routeProgress ->
|
|
123
|
+
onRouteProgressChanged(mapOf(
|
|
124
|
+
"distanceRemaining" to routeProgress.distanceRemaining,
|
|
125
|
+
"durationRemaining" to routeProgress.durationRemaining,
|
|
126
|
+
"distanceTraveled" to routeProgress.distanceTraveled,
|
|
127
|
+
"fractionTraveled" to routeProgress.fractionTraveled
|
|
128
|
+
))
|
|
129
|
+
|
|
130
|
+
val currentLegProgress = routeProgress.currentLegProgress
|
|
131
|
+
if (currentLegProgress != null) {
|
|
132
|
+
val distanceToEnd = currentLegProgress.distanceRemaining
|
|
133
|
+
if (distanceToEnd <= 30.0) {
|
|
134
|
+
val legIndex = routeProgress.currentLegProgress?.legIndex ?: 0
|
|
135
|
+
val totalLegs = routeProgress.route.legs()?.size ?: 1
|
|
136
|
+
|
|
137
|
+
if (legIndex == totalLegs - 1) {
|
|
138
|
+
onFinalDestinationArrival(emptyMap<String, Any>())
|
|
139
|
+
} else {
|
|
140
|
+
onWaypointArrival(mapOf("waypointIndex" to legIndex))
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
navigation.registerOffRouteObserver(OffRouteObserver { isOffRoute ->
|
|
147
|
+
if (isOffRoute) {
|
|
148
|
+
onUserOffRoute(emptyMap<String, Any>())
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
navigation.registerRoutesObserver(RoutesObserver { result ->
|
|
153
|
+
if (result.navigationRoutes.isNotEmpty()) {
|
|
154
|
+
onRouteChanged(emptyMap<String, Any>())
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
override fun onDetachedFromWindow() {
|
|
160
|
+
super.onDetachedFromWindow()
|
|
161
|
+
navigationView?.let { removeView(it) }
|
|
162
|
+
navigationView = null
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'ExpoMapboxNavigation'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package['description']
|
|
9
|
+
s.description = package['description']
|
|
10
|
+
s.license = package['license']
|
|
11
|
+
s.author = 'Bäckerherz'
|
|
12
|
+
s.homepage = 'https://github.com/baeckerherz/expo-mapbox-navigation'
|
|
13
|
+
s.platforms = { ios: '15.0' }
|
|
14
|
+
s.source = { git: '' }
|
|
15
|
+
s.static_framework = true
|
|
16
|
+
|
|
17
|
+
s.dependency 'ExpoModulesCore'
|
|
18
|
+
|
|
19
|
+
s.source_files = '**/*.{h,m,mm,swift}'
|
|
20
|
+
s.exclude_files = 'Tests/'
|
|
21
|
+
|
|
22
|
+
s.pod_target_xcconfig = {
|
|
23
|
+
'DEFINES_MODULE' => 'YES',
|
|
24
|
+
'SWIFT_COMPILATION_MODE' => 'wholemodule',
|
|
25
|
+
}
|
|
26
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
|
|
3
|
+
public class ExpoMapboxNavigationModule: Module {
|
|
4
|
+
public func definition() -> ModuleDefinition {
|
|
5
|
+
Name("ExpoMapboxNavigation")
|
|
6
|
+
|
|
7
|
+
View(ExpoMapboxNavigationView.self) {
|
|
8
|
+
Events(
|
|
9
|
+
"onRouteProgressChanged",
|
|
10
|
+
"onCancelNavigation",
|
|
11
|
+
"onWaypointArrival",
|
|
12
|
+
"onFinalDestinationArrival",
|
|
13
|
+
"onRouteChanged",
|
|
14
|
+
"onUserOffRoute",
|
|
15
|
+
"onError"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
// Route
|
|
19
|
+
Prop("coordinates") { (view, coordinates: [[String: Double]]) in
|
|
20
|
+
view.setCoordinates(coordinates)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
Prop("waypointIndices") { (view, indices: [Int]) in
|
|
24
|
+
view.waypointIndices = indices
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
Prop("routeProfile") { (view, profile: String) in
|
|
28
|
+
view.routeProfile = profile
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Localization
|
|
32
|
+
Prop("locale") { (view, locale: String) in
|
|
33
|
+
view.navigationLocale = locale
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
Prop("mute") { (view, mute: Bool) in
|
|
37
|
+
view.isMuted = mute
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Appearance
|
|
41
|
+
Prop("mapStyle") { (view, style: String) in
|
|
42
|
+
view.mapStyleURL = style
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
Prop("themeMode") { (view, mode: String) in
|
|
46
|
+
view.themeMode = mode
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
Prop("accentColor") { (view, color: String) in
|
|
50
|
+
view.accentColorHex = color
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
Prop("routeColor") { (view, color: String) in
|
|
54
|
+
view.routeColorHex = color
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
Prop("bannerBackgroundColor") { (view, color: String) in
|
|
58
|
+
view.bannerBackgroundColorHex = color
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
Prop("bannerTextColor") { (view, color: String) in
|
|
62
|
+
view.bannerTextColorHex = color
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|