@jacques_gordon/expo-mapbox-navigation 1.0.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 +270 -0
- package/android/.gradle/8.9/checksums/checksums.lock +0 -0
- package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
- package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
- package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/8.9/gc.properties +0 -0
- package/android/.gradle/9.2.0/checksums/checksums.lock +0 -0
- package/android/.gradle/9.2.0/fileChanges/last-build.bin +0 -0
- package/android/.gradle/9.2.0/fileHashes/fileHashes.bin +0 -0
- package/android/.gradle/9.2.0/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/9.2.0/gc.properties +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
- package/android/.gradle/vcs-1/gc.properties +0 -0
- package/android/build.gradle +54 -0
- package/android/src/main/java/expo/modules/mapboxnavigation/ExpoMapboxNavigationModule.kt +85 -0
- package/android/src/main/java/expo/modules/mapboxnavigation/ExpoMapboxNavigationView.kt +184 -0
- package/app.plugin.js +1 -0
- package/build/MapboxNavigation.types.d.ts +132 -0
- package/build/MapboxNavigation.types.js +2 -0
- package/build/MapboxNavigationView.d.ts +30 -0
- package/build/MapboxNavigationView.js +41 -0
- package/build/index.d.ts +2 -0
- package/build/index.js +8 -0
- package/expo-module.config.json +9 -0
- package/ios/ExpoMapboxNavigation.podspec +25 -0
- package/ios/ExpoMapboxNavigationModule.swift +81 -0
- package/ios/ExpoMapboxNavigationView.swift +236 -0
- package/package.json +63 -0
package/README.md
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# @jacques_gordon/expo-mapbox-navigation
|
|
2
|
+
|
|
3
|
+
Plugin Expo pour la navigation Mapbox **turn-by-turn**, compatible **Expo SDK 53** avec **Expo Dev Client**.
|
|
4
|
+
|
|
5
|
+
Supporte **iOS** et **Android**, avec waypoints multiples, instructions vocales, reroutage automatique, restrictions de gabarit de véhicule et personnalisation de la carte.
|
|
6
|
+
|
|
7
|
+
> ⚠️ Nécessite **Expo Dev Client** — incompatible avec Expo Go.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Fonctionnalités
|
|
12
|
+
|
|
13
|
+
- ✅ Navigation turn-by-turn complète (iOS & Android)
|
|
14
|
+
- ✅ Waypoints multiples
|
|
15
|
+
- ✅ Instructions vocales
|
|
16
|
+
- ✅ Reroutage automatique
|
|
17
|
+
- ✅ Détection hors-route
|
|
18
|
+
- ✅ Restrictions de gabarit de véhicule (hauteur / largeur)
|
|
19
|
+
- ✅ Map Matching API (trajet explicite selon les coordonnées)
|
|
20
|
+
- ✅ Routes alternatives
|
|
21
|
+
- ✅ Style de carte personnalisable
|
|
22
|
+
- ✅ Couche raster personnalisée
|
|
23
|
+
- ✅ Multilangue
|
|
24
|
+
- ✅ Compatible avec `@rnmapbox/maps`
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
### 1. Installer `@rnmapbox/maps` (dépendance requise)
|
|
31
|
+
|
|
32
|
+
Suivre les [instructions officielles de @rnmapbox/maps](https://rnmapbox.github.io/docs/install).
|
|
33
|
+
La version recommandée est `11.11.0`. **Important : définir explicitement `RNMapboxMapsVersion`.**
|
|
34
|
+
|
|
35
|
+
### 2. Installer le package
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install @jacques_gordon/expo-mapbox-navigation
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 3. Configurer le plugin dans `app.json`
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"expo": {
|
|
46
|
+
"plugins": [
|
|
47
|
+
[
|
|
48
|
+
"expo-build-properties",
|
|
49
|
+
{
|
|
50
|
+
"ios": {
|
|
51
|
+
"useFrameworks": "static"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
[
|
|
56
|
+
"@jacques_gordon/expo-mapbox-navigation",
|
|
57
|
+
{
|
|
58
|
+
"accessToken": "pk.eyJ1IjoiVk9UUkVfVE9LRU4iLCJhIjoiY...",
|
|
59
|
+
"mapboxMapsVersion": "11.11.0",
|
|
60
|
+
"androidColorOverrides": {
|
|
61
|
+
"mapbox_main_maneuver_background_color": "#1E88E5"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 4. Configurer les tokens secrets
|
|
71
|
+
|
|
72
|
+
#### iOS — `~/.netrc`
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
machine api.mapbox.com
|
|
76
|
+
login mapbox
|
|
77
|
+
password sk.eyJ1IjoiVk9UUkVfVE9LRU5fU0VDUkVUIiwiYSI6Ii4uLiJ9...
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
#### Android — `~/.gradle/gradle.properties`
|
|
81
|
+
|
|
82
|
+
```properties
|
|
83
|
+
MAPBOX_DOWNLOADS_TOKEN=sk.eyJ1IjoiVk9UUkVfVE9LRU5fU0VDUkVUIiwiYSI6Ii4uLiJ9...
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 5. Prebuild + Dev Client
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
npx expo prebuild --clean
|
|
90
|
+
npx expo run:ios # ou run:android
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Utilisation
|
|
96
|
+
|
|
97
|
+
### Exemple de base
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
import { MapboxNavigationView } from '@jacques_gordon/expo-mapbox-navigation';
|
|
101
|
+
|
|
102
|
+
export default function NavigationScreen() {
|
|
103
|
+
return (
|
|
104
|
+
<MapboxNavigationView
|
|
105
|
+
style={{ flex: 1 }}
|
|
106
|
+
coordinates={[
|
|
107
|
+
{ latitude: 48.8566, longitude: 2.3522 }, // Paris — départ
|
|
108
|
+
{ latitude: 43.2965, longitude: 5.3698 }, // Marseille — destination
|
|
109
|
+
]}
|
|
110
|
+
locale="fr"
|
|
111
|
+
onFinalDestinationArrival={() => console.log('Arrivé !')}
|
|
112
|
+
onCancelNavigation={() => navigation.goBack()}
|
|
113
|
+
/>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Exemple complet (compatible avec la page de navigation)
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
import { MapboxNavigationView } from '@jacques_gordon/expo-mapbox-navigation';
|
|
122
|
+
|
|
123
|
+
export default function NavigationScreen({ navigation, reservation, currentPosition }) {
|
|
124
|
+
const coordinates = [
|
|
125
|
+
currentPosition, // départ (position actuelle)
|
|
126
|
+
{ latitude: 45.7640, longitude: 4.8357 }, // Lyon — waypoint intermédiaire
|
|
127
|
+
reservation.address_to_lat_long, // destination finale
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<MapboxNavigationView
|
|
132
|
+
style={{ flex: 1 }}
|
|
133
|
+
coordinates={coordinates}
|
|
134
|
+
locale="fr"
|
|
135
|
+
vehicleMaxHeight={5.0}
|
|
136
|
+
vehicleMaxWidth={2.5}
|
|
137
|
+
showsEndOfRouteFeedback
|
|
138
|
+
onRouteProgressChanged={async (event) => {
|
|
139
|
+
const { distanceRemaining, durationRemaining, distanceTraveled, fractionTraveled } =
|
|
140
|
+
event.nativeEvent;
|
|
141
|
+
|
|
142
|
+
// Notifier le client que le chauffeur approche
|
|
143
|
+
if (distanceRemaining <= 60) {
|
|
144
|
+
AXIOS_POST('reservations/livetracking/drivernear', { reservation });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const eta = new Date(Date.now() + durationRemaining * 1000);
|
|
148
|
+
dispatch(setNavigationData({ eta, distanceRemaining, durationRemaining }));
|
|
149
|
+
}}
|
|
150
|
+
onWaypointArrival={(event) => {
|
|
151
|
+
console.log('Waypoint atteint !', event.nativeEvent);
|
|
152
|
+
}}
|
|
153
|
+
onFinalDestinationArrival={async () => {
|
|
154
|
+
await playSound(SOUNDS.ROAD_RECALCULATE);
|
|
155
|
+
showToast({ toast_type: 'validation', title: 'Arrivée', body: 'Destination atteinte !' });
|
|
156
|
+
}}
|
|
157
|
+
onCancelNavigation={() => navigation.goBack()}
|
|
158
|
+
onRouteChanged={() => console.log('Reroutage en cours...')}
|
|
159
|
+
onUserOffRoute={() => console.log('Hors route !')}
|
|
160
|
+
onRoutesLoaded={() => console.log('Routes chargées')}
|
|
161
|
+
/>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Props
|
|
169
|
+
|
|
170
|
+
### Route
|
|
171
|
+
|
|
172
|
+
| Prop | Type | Requis | Description |
|
|
173
|
+
|------|------|--------|-------------|
|
|
174
|
+
| `coordinates` | `Coordinate[]` | **Oui** | Tableau de `{latitude, longitude}`. Minimum 2 points. |
|
|
175
|
+
| `waypointIndices` | `number[]` | Non | Indices des points considérés comme waypoints. Par défaut: tous. |
|
|
176
|
+
|
|
177
|
+
### Options de routing
|
|
178
|
+
|
|
179
|
+
| Prop | Type | Défaut | Description |
|
|
180
|
+
|------|------|--------|-------------|
|
|
181
|
+
| `locale` | `string` | locale appareil | Langue des instructions. Ex: `"fr"`, `"en"`, `"de"` |
|
|
182
|
+
| `routeProfile` | `string` | `"mapbox/driving-traffic"` | Profil de routing. Android: sans `"mapbox/"`. |
|
|
183
|
+
| `routeExcludeList` | `string[]` | `[]` | Routes à exclure: `"toll"`, `"ferry"`, `"motorway"` |
|
|
184
|
+
| `useRouteMatchingApi` | `boolean` | `false` | Utilise l'API Map Matching au lieu de l'API Directions |
|
|
185
|
+
| `disableAlternativeRoutes` | `boolean` | `false` | Désactive les routes alternatives |
|
|
186
|
+
| `mute` | `boolean` | `false` | Coupe le son de navigation au démarrage |
|
|
187
|
+
| `vehicleMaxHeight` | `number` | — | Hauteur max du véhicule en mètres |
|
|
188
|
+
| `vehicleMaxWidth` | `number` | — | Largeur max du véhicule en mètres |
|
|
189
|
+
|
|
190
|
+
### Personnalisation visuelle
|
|
191
|
+
|
|
192
|
+
| Prop | Type | Défaut | Description |
|
|
193
|
+
|------|------|--------|-------------|
|
|
194
|
+
| `mapStyle` | `string` | style par défaut | URL du style Mapbox |
|
|
195
|
+
| `customRasterSourceUrl` | `string` | — | URL template raster personnalisée (ex: OpenStreetMap) |
|
|
196
|
+
| `placeCustomRasterLayerAbove` | `string` | — | ID du layer au-dessus duquel placer la couche raster |
|
|
197
|
+
| `showsEndOfRouteFeedback` | `boolean` | `false` | Affiche l'UI de feedback à l'arrivée |
|
|
198
|
+
|
|
199
|
+
### Callbacks
|
|
200
|
+
|
|
201
|
+
| Prop | Paramètres | Description |
|
|
202
|
+
|------|-----------|-------------|
|
|
203
|
+
| `onRouteProgressChanged` | `{ nativeEvent: RouteProgress }` | Progression sur la route (appelé fréquemment) |
|
|
204
|
+
| `onWaypointArrival` | `{ nativeEvent: RouteProgress }` (Android only) | Arrivée à un waypoint intermédiaire |
|
|
205
|
+
| `onFinalDestinationArrival` | — | Arrivée à la destination finale |
|
|
206
|
+
| `onCancelNavigation` | — | Annulation de la navigation par l'utilisateur |
|
|
207
|
+
| `onRouteChanged` | — | Route modifiée ou reroutage |
|
|
208
|
+
| `onUserOffRoute` | — | L'utilisateur est sorti de la route |
|
|
209
|
+
| `onRoutesLoaded` | — | Routes chargées et prêtes |
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Types TypeScript
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
interface Coordinate {
|
|
217
|
+
latitude: number;
|
|
218
|
+
longitude: number;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
interface RouteProgress {
|
|
222
|
+
distanceRemaining: number; // mètres restants
|
|
223
|
+
distanceTraveled: number; // mètres parcourus
|
|
224
|
+
durationRemaining: number; // secondes restantes
|
|
225
|
+
fractionTraveled: number; // 0.0 → 1.0
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Plugin `app.json` — Options complètes
|
|
232
|
+
|
|
233
|
+
```json
|
|
234
|
+
[
|
|
235
|
+
"@jacques_gordon/expo-mapbox-navigation",
|
|
236
|
+
{
|
|
237
|
+
"accessToken": "pk.eyJ1...",
|
|
238
|
+
"mapboxMapsVersion": "11.11.0",
|
|
239
|
+
"androidColorOverrides": {
|
|
240
|
+
"mapbox_main_maneuver_background_color": "#1E88E5",
|
|
241
|
+
"mapbox_sub_maneuver_background_color": "#1565C0",
|
|
242
|
+
"mapbox_banner_background_color": "#FFFFFF"
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
]
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Notes importantes
|
|
249
|
+
|
|
250
|
+
- Nécessite **Expo Dev Client** (Expo Go non supporté)
|
|
251
|
+
- iOS minimum : **14.0**
|
|
252
|
+
- Android minimum SDK : **24**
|
|
253
|
+
- Le SDK Mapbox Navigation est propriétaire — facturation via votre compte Mapbox
|
|
254
|
+
- Pour iOS, les `.xcframework` du SDK Mapbox Navigation doivent être compilés et inclus dans `ios/Frameworks/`
|
|
255
|
+
(voir [instructions dans le README du repo source](https://github.com/uju777/expo-mapbox-navigation))
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Obtenir vos tokens Mapbox
|
|
260
|
+
|
|
261
|
+
1. Créer un compte sur [mapbox.com](https://www.mapbox.com/)
|
|
262
|
+
2. Aller dans [Account > Tokens](https://account.mapbox.com/access-tokens/)
|
|
263
|
+
3. **Token public** (`pk.eyJ1...`) — token par défaut ou nouveau token
|
|
264
|
+
4. **Token secret** (`sk.eyJ1...`) — nouveau token avec le scope `Downloads:Read`
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## Licence
|
|
269
|
+
|
|
270
|
+
MIT
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
File without changes
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
apply plugin: 'com.android.library'
|
|
2
|
+
apply plugin: 'kotlin-android'
|
|
3
|
+
apply plugin: 'maven-publish'
|
|
4
|
+
|
|
5
|
+
buildscript {
|
|
6
|
+
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
|
7
|
+
if (expoModulesCorePlugin.exists()) {
|
|
8
|
+
apply from: expoModulesCorePlugin
|
|
9
|
+
applyKotlinExpoModulesCorePlugin()
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
android {
|
|
14
|
+
compileSdkVersion 34
|
|
15
|
+
namespace "expo.modules.mapboxnavigation"
|
|
16
|
+
|
|
17
|
+
defaultConfig {
|
|
18
|
+
minSdkVersion 24
|
|
19
|
+
targetSdkVersion 34
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
compileOptions {
|
|
23
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
24
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
kotlinOptions {
|
|
28
|
+
jvmTarget = "17"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
repositories {
|
|
33
|
+
maven {
|
|
34
|
+
url 'https://api.mapbox.com/downloads/v2/releases/maven'
|
|
35
|
+
authentication {
|
|
36
|
+
basic(BasicAuthentication)
|
|
37
|
+
}
|
|
38
|
+
credentials {
|
|
39
|
+
username = "mapbox"
|
|
40
|
+
password = project.properties['MAPBOX_DOWNLOADS_TOKEN'] ?: System.getenv("MAPBOX_DOWNLOADS_TOKEN") ?: ""
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
google()
|
|
44
|
+
mavenCentral()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
dependencies {
|
|
48
|
+
implementation project(':expo-modules-core')
|
|
49
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlinVersion()}"
|
|
50
|
+
|
|
51
|
+
// Mapbox Navigation SDK v3 + Drop-in UI
|
|
52
|
+
api "com.mapbox.navigation:android:3.3.0"
|
|
53
|
+
api "com.mapbox.navigation:ui-dropin:3.3.0"
|
|
54
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
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
|
+
|
|
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
|
+
)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
package expo.modules.mapboxnavigation
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.view.ViewGroup
|
|
5
|
+
import com.mapbox.api.directions.v5.DirectionsCriteria
|
|
6
|
+
import com.mapbox.api.directions.v5.models.RouteOptions
|
|
7
|
+
import com.mapbox.geojson.Point
|
|
8
|
+
import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions
|
|
9
|
+
import com.mapbox.navigation.base.extensions.applyLanguageAndVoiceUnitOptions
|
|
10
|
+
import com.mapbox.navigation.base.route.NavigationRoute
|
|
11
|
+
import com.mapbox.navigation.base.route.NavigationRouterCallback
|
|
12
|
+
import com.mapbox.navigation.base.route.RouterFailure
|
|
13
|
+
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
|
+
import com.mapbox.navigation.core.MapboxNavigation
|
|
17
|
+
import com.mapbox.navigation.core.MapboxNavigationProvider
|
|
18
|
+
import com.mapbox.navigation.core.directions.session.RoutesObserver
|
|
19
|
+
import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp
|
|
20
|
+
import com.mapbox.navigation.core.trip.session.RouteProgressObserver
|
|
21
|
+
import com.mapbox.navigation.dropin.NavigationView
|
|
22
|
+
import com.mapbox.navigation.dropin.map.MapStyleLoader
|
|
23
|
+
import expo.modules.kotlin.AppContext
|
|
24
|
+
import expo.modules.kotlin.views.ExpoView
|
|
25
|
+
import java.util.Locale
|
|
26
|
+
|
|
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
|
+
}
|
|
102
|
+
}
|
|
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>())
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
// Construire et lancer la route
|
|
116
|
+
requestRoute(navView)
|
|
117
|
+
}
|
|
118
|
+
|
|
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)
|
|
124
|
+
}
|
|
125
|
+
if (points.size < 2) return
|
|
126
|
+
|
|
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()
|
|
130
|
+
|
|
131
|
+
val resolvedLocale = if (locale.isNotEmpty()) Locale(locale) else Locale.getDefault()
|
|
132
|
+
|
|
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
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
val routeOptionsBuilder = RouteOptions.builder()
|
|
141
|
+
.applyDefaultNavigationOptions(profile)
|
|
142
|
+
.applyLanguageAndVoiceUnitOptions(context)
|
|
143
|
+
.language(resolvedLocale.language)
|
|
144
|
+
.coordinatesList(listOf(origin) + waypoints + listOf(destination))
|
|
145
|
+
|
|
146
|
+
if (!disableAlternativeRoutes) {
|
|
147
|
+
routeOptionsBuilder.alternatives(true)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (routeExcludeList.isNotEmpty()) {
|
|
151
|
+
routeOptionsBuilder.exclude(routeExcludeList.joinToString(","))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
vehicleMaxHeight?.let { routeOptionsBuilder.maxHeight(it) }
|
|
155
|
+
vehicleMaxWidth?.let { routeOptionsBuilder.maxWidth(it) }
|
|
156
|
+
|
|
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
|
+
}
|
|
165
|
+
|
|
166
|
+
override fun onFailure(reasons: List<RouterFailure>, routeOptions: RouteOptions) {
|
|
167
|
+
android.util.Log.e("ExpoMapboxNavigation", "Erreur route: ${reasons.firstOrNull()?.message}")
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
override fun onCanceled(routeOptions: RouteOptions, routerOrigin: RouterOrigin) {}
|
|
171
|
+
}
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
// Bouton d'annulation
|
|
175
|
+
navView.addOnClickListener {
|
|
176
|
+
onCancelNavigation.emit(emptyMap<String, Any>())
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
override fun onDetachedFromWindow() {
|
|
181
|
+
super.onDetachedFromWindow()
|
|
182
|
+
MapboxNavigationApp.current()?.setNavigationRoutes(emptyList())
|
|
183
|
+
}
|
|
184
|
+
}
|
package/app.plugin.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('./plugin/build/index').default;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
2
|
+
export interface Coordinate {
|
|
3
|
+
latitude: number;
|
|
4
|
+
longitude: number;
|
|
5
|
+
}
|
|
6
|
+
export interface RouteProgress {
|
|
7
|
+
/** Distance restante en mètres */
|
|
8
|
+
distanceRemaining: number;
|
|
9
|
+
/** Distance parcourue en mètres */
|
|
10
|
+
distanceTraveled: number;
|
|
11
|
+
/** Durée restante en secondes */
|
|
12
|
+
durationRemaining: number;
|
|
13
|
+
/** Fraction parcourue (0.0 → 1.0) */
|
|
14
|
+
fractionTraveled: number;
|
|
15
|
+
}
|
|
16
|
+
export interface NativeRouteProgressEvent {
|
|
17
|
+
nativeEvent: RouteProgress;
|
|
18
|
+
}
|
|
19
|
+
export interface NativeWaypointEvent {
|
|
20
|
+
nativeEvent: RouteProgress;
|
|
21
|
+
}
|
|
22
|
+
export interface MapboxNavigationViewProps {
|
|
23
|
+
/** Style du composant (ex: { flex: 1 }) */
|
|
24
|
+
style?: StyleProp<ViewStyle>;
|
|
25
|
+
/**
|
|
26
|
+
* Tableau de coordonnées { latitude, longitude }.
|
|
27
|
+
* Minimum 2 points (départ + destination).
|
|
28
|
+
* Requis.
|
|
29
|
+
*/
|
|
30
|
+
coordinates: Coordinate[];
|
|
31
|
+
/**
|
|
32
|
+
* Indices dans `coordinates` considérés comme des waypoints/destinations intermédiaires.
|
|
33
|
+
* Par défaut, tous les points sont des waypoints.
|
|
34
|
+
* Au moins le premier et le dernier index doivent être inclus.
|
|
35
|
+
*/
|
|
36
|
+
waypointIndices?: number[];
|
|
37
|
+
/**
|
|
38
|
+
* Utilise l'API Map Matching de Mapbox au lieu de l'API Directions standard.
|
|
39
|
+
* Utile pour forcer un trajet suivant exactement les coordonnées fournies.
|
|
40
|
+
* Défaut: false
|
|
41
|
+
*/
|
|
42
|
+
useRouteMatchingApi?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Locale/langue pour les labels de la carte, les directions et la voix.
|
|
45
|
+
* Exemples: "fr", "en", "de", "nl".
|
|
46
|
+
* Par défaut: locale de l'appareil.
|
|
47
|
+
*/
|
|
48
|
+
locale?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Profil de routing Mapbox.
|
|
51
|
+
* iOS: "mapbox/driving-traffic" | "mapbox/driving" | "mapbox/walking" | "mapbox/cycling"
|
|
52
|
+
* Android: "driving-traffic" | "driving" | "walking" | "cycling" (sans "mapbox/" prefix)
|
|
53
|
+
* Défaut: "mapbox/driving-traffic"
|
|
54
|
+
*/
|
|
55
|
+
routeProfile?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Types de routes à exclure du calcul d'itinéraire.
|
|
58
|
+
* Exemples: ["toll", "ferry", "motorway"]
|
|
59
|
+
*/
|
|
60
|
+
routeExcludeList?: string[];
|
|
61
|
+
/**
|
|
62
|
+
* Style de la carte Mapbox.
|
|
63
|
+
* Exemples: "mapbox://styles/mapbox/navigation-day-v1"
|
|
64
|
+
*/
|
|
65
|
+
mapStyle?: string;
|
|
66
|
+
/**
|
|
67
|
+
* Coupe le son de navigation au démarrage.
|
|
68
|
+
* Défaut: false
|
|
69
|
+
*/
|
|
70
|
+
mute?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Hauteur maximale du véhicule en mètres.
|
|
73
|
+
* Permet d'éviter les routes avec restriction de hauteur.
|
|
74
|
+
*/
|
|
75
|
+
vehicleMaxHeight?: number;
|
|
76
|
+
/**
|
|
77
|
+
* Largeur maximale du véhicule en mètres.
|
|
78
|
+
* Permet d'éviter les routes avec restriction de largeur.
|
|
79
|
+
*/
|
|
80
|
+
vehicleMaxWidth?: number;
|
|
81
|
+
/**
|
|
82
|
+
* URL d'une source raster personnalisée pour la carte.
|
|
83
|
+
* Doit être une URL template avec {x}, {y}, {z}.
|
|
84
|
+
* Exemple: "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
|
|
85
|
+
*/
|
|
86
|
+
customRasterSourceUrl?: string;
|
|
87
|
+
/**
|
|
88
|
+
* ID du layer au-dessus duquel placer le layer raster personnalisé.
|
|
89
|
+
*/
|
|
90
|
+
placeCustomRasterLayerAbove?: string;
|
|
91
|
+
/**
|
|
92
|
+
* Désactive le calcul et l'affichage des routes alternatives.
|
|
93
|
+
* Défaut: false
|
|
94
|
+
*/
|
|
95
|
+
disableAlternativeRoutes?: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Affiche l'UI de feedback en fin de route.
|
|
98
|
+
* Défaut: false
|
|
99
|
+
*/
|
|
100
|
+
showsEndOfRouteFeedback?: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Appelé régulièrement avec la progression sur la route.
|
|
103
|
+
* L'objet est dans event.nativeEvent.
|
|
104
|
+
*/
|
|
105
|
+
onRouteProgressChanged?: (event: NativeRouteProgressEvent) => void;
|
|
106
|
+
/**
|
|
107
|
+
* Appelé quand l'utilisateur arrive à un waypoint intermédiaire.
|
|
108
|
+
* Sur Android uniquement: fournit les données de progression dans event.nativeEvent.
|
|
109
|
+
*/
|
|
110
|
+
onWaypointArrival?: (event: NativeWaypointEvent) => void;
|
|
111
|
+
/**
|
|
112
|
+
* Appelé quand l'utilisateur arrive à la destination finale.
|
|
113
|
+
*/
|
|
114
|
+
onFinalDestinationArrival?: () => void;
|
|
115
|
+
/**
|
|
116
|
+
* Appelé quand l'utilisateur appuie sur le bouton d'annulation.
|
|
117
|
+
* La navigation ne se ferme pas automatiquement — gérer manuellement (ex: navigation.goBack()).
|
|
118
|
+
*/
|
|
119
|
+
onCancelNavigation?: (event?: any) => void;
|
|
120
|
+
/**
|
|
121
|
+
* Appelé quand la route change ou qu'un reroutage est effectué.
|
|
122
|
+
*/
|
|
123
|
+
onRouteChanged?: () => void;
|
|
124
|
+
/**
|
|
125
|
+
* Appelé quand l'utilisateur sort de la route.
|
|
126
|
+
*/
|
|
127
|
+
onUserOffRoute?: () => void;
|
|
128
|
+
/**
|
|
129
|
+
* Appelé quand les routes sont chargées et prêtes.
|
|
130
|
+
*/
|
|
131
|
+
onRoutesLoaded?: () => void;
|
|
132
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { MapboxNavigationViewProps } from './MapboxNavigation.types';
|
|
3
|
+
/**
|
|
4
|
+
* MapboxNavigationView
|
|
5
|
+
*
|
|
6
|
+
* Composant de navigation Mapbox turn-by-turn pour Expo SDK 53+.
|
|
7
|
+
* Requiert Expo Dev Client (incompatible avec Expo Go).
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* <MapboxNavigationView
|
|
12
|
+
* style={{ flex: 1 }}
|
|
13
|
+
* coordinates={[
|
|
14
|
+
* { latitude: 48.8566, longitude: 2.3522 }, // Paris (départ)
|
|
15
|
+
* { latitude: 45.7640, longitude: 4.8357 }, // Lyon (waypoint)
|
|
16
|
+
* { latitude: 43.2965, longitude: 5.3698 }, // Marseille (destination)
|
|
17
|
+
* ]}
|
|
18
|
+
* locale="fr"
|
|
19
|
+
* vehicleMaxHeight={5.0}
|
|
20
|
+
* vehicleMaxWidth={2.5}
|
|
21
|
+
* onRouteProgressChanged={(event) => {
|
|
22
|
+
* const { distanceRemaining, durationRemaining } = event.nativeEvent;
|
|
23
|
+
* console.log(`${(distanceRemaining / 1000).toFixed(1)} km restants`);
|
|
24
|
+
* }}
|
|
25
|
+
* onFinalDestinationArrival={() => console.log('Arrivé !')}
|
|
26
|
+
* onCancelNavigation={() => navigation.goBack()}
|
|
27
|
+
* />
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export default function MapboxNavigationView(props: MapboxNavigationViewProps): React.JSX.Element;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.default = MapboxNavigationView;
|
|
7
|
+
const react_1 = __importDefault(require("react"));
|
|
8
|
+
const expo_modules_core_1 = require("expo-modules-core");
|
|
9
|
+
const NativeView = (0, expo_modules_core_1.requireNativeViewManager)('ExpoMapboxNavigation');
|
|
10
|
+
/**
|
|
11
|
+
* MapboxNavigationView
|
|
12
|
+
*
|
|
13
|
+
* Composant de navigation Mapbox turn-by-turn pour Expo SDK 53+.
|
|
14
|
+
* Requiert Expo Dev Client (incompatible avec Expo Go).
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```tsx
|
|
18
|
+
* <MapboxNavigationView
|
|
19
|
+
* style={{ flex: 1 }}
|
|
20
|
+
* coordinates={[
|
|
21
|
+
* { latitude: 48.8566, longitude: 2.3522 }, // Paris (départ)
|
|
22
|
+
* { latitude: 45.7640, longitude: 4.8357 }, // Lyon (waypoint)
|
|
23
|
+
* { latitude: 43.2965, longitude: 5.3698 }, // Marseille (destination)
|
|
24
|
+
* ]}
|
|
25
|
+
* locale="fr"
|
|
26
|
+
* vehicleMaxHeight={5.0}
|
|
27
|
+
* vehicleMaxWidth={2.5}
|
|
28
|
+
* onRouteProgressChanged={(event) => {
|
|
29
|
+
* const { distanceRemaining, durationRemaining } = event.nativeEvent;
|
|
30
|
+
* console.log(`${(distanceRemaining / 1000).toFixed(1)} km restants`);
|
|
31
|
+
* }}
|
|
32
|
+
* onFinalDestinationArrival={() => console.log('Arrivé !')}
|
|
33
|
+
* onCancelNavigation={() => navigation.goBack()}
|
|
34
|
+
* />
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
function MapboxNavigationView(props) {
|
|
38
|
+
return (<NativeView
|
|
39
|
+
// Valeurs par défaut
|
|
40
|
+
mute={false} useRouteMatchingApi={false} disableAlternativeRoutes={false} showsEndOfRouteFeedback={false} waypointIndices={[]} routeExcludeList={[]} {...props}/>);
|
|
41
|
+
}
|
package/build/index.d.ts
ADDED
package/build/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.MapboxNavigationView = void 0;
|
|
7
|
+
var MapboxNavigationView_1 = require("./MapboxNavigationView");
|
|
8
|
+
Object.defineProperty(exports, "MapboxNavigationView", { enumerable: true, get: function () { return __importDefault(MapboxNavigationView_1).default; } });
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Pod::Spec.new do |s|
|
|
2
|
+
s.name = 'ExpoMapboxNavigation'
|
|
3
|
+
s.version = '1.0.0'
|
|
4
|
+
s.summary = 'Expo module for Mapbox turn-by-turn navigation — iOS & Android'
|
|
5
|
+
s.homepage = 'https://github.com/YOUR_GITHUB/expo-mapbox-navigation'
|
|
6
|
+
s.license = { :type => 'MIT' }
|
|
7
|
+
s.authors = { 'Your Name' => 'you@example.com' }
|
|
8
|
+
s.platforms = { :ios => '14.0' }
|
|
9
|
+
s.swift_version = '5.7'
|
|
10
|
+
s.source = { :git => '' }
|
|
11
|
+
|
|
12
|
+
s.static_framework = true
|
|
13
|
+
|
|
14
|
+
s.dependency 'ExpoModulesCore'
|
|
15
|
+
# MapboxNavigationUIKit + MapboxNavigationCore are bundled as .xcframework
|
|
16
|
+
# See README for how to build them from source
|
|
17
|
+
|
|
18
|
+
s.source_files = '*.{swift,h,m,mm,cpp}'
|
|
19
|
+
s.preserve_paths = 'Frameworks/**/*'
|
|
20
|
+
|
|
21
|
+
s.pod_target_xcconfig = {
|
|
22
|
+
'FRAMEWORK_SEARCH_PATHS' => '$(inherited) $(PODS_TARGET_SRCROOT)/Frameworks/**',
|
|
23
|
+
'OTHER_LDFLAGS' => '$(inherited) -framework MapboxNavigationUIKit -framework MapboxNavigationCore -framework MapboxNavigationNative -framework MapboxDirections'
|
|
24
|
+
}
|
|
25
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
|
|
3
|
+
public class ExpoMapboxNavigationModule: Module {
|
|
4
|
+
public func definition() -> ModuleDefinition {
|
|
5
|
+
Name("ExpoMapboxNavigation")
|
|
6
|
+
|
|
7
|
+
View(ExpoMapboxNavigationView.self) {
|
|
8
|
+
|
|
9
|
+
// ─── Props ────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
Prop("coordinates") { (view: ExpoMapboxNavigationView, value: [[String: Double]]) in
|
|
12
|
+
view.coordinates = value
|
|
13
|
+
view.startNavigationIfReady()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
Prop("waypointIndices") { (view: ExpoMapboxNavigationView, value: [Int]) in
|
|
17
|
+
view.waypointIndices = value
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
Prop("useRouteMatchingApi") { (view: ExpoMapboxNavigationView, value: Bool) in
|
|
21
|
+
view.useRouteMatchingApi = value
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
Prop("locale") { (view: ExpoMapboxNavigationView, value: String) in
|
|
25
|
+
view.locale = value
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
Prop("routeProfile") { (view: ExpoMapboxNavigationView, value: String) in
|
|
29
|
+
view.routeProfile = value
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
Prop("routeExcludeList") { (view: ExpoMapboxNavigationView, value: [String]) in
|
|
33
|
+
view.routeExcludeList = value
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
Prop("mapStyle") { (view: ExpoMapboxNavigationView, value: String) in
|
|
37
|
+
view.mapStyle = value
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
Prop("mute") { (view: ExpoMapboxNavigationView, value: Bool) in
|
|
41
|
+
view.mute = value
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
Prop("vehicleMaxHeight") { (view: ExpoMapboxNavigationView, value: Double) in
|
|
45
|
+
view.vehicleMaxHeight = value
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
Prop("vehicleMaxWidth") { (view: ExpoMapboxNavigationView, value: Double) in
|
|
49
|
+
view.vehicleMaxWidth = value
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
Prop("customRasterSourceUrl") { (view: ExpoMapboxNavigationView, value: String) in
|
|
53
|
+
view.customRasterSourceUrl = value
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
Prop("placeCustomRasterLayerAbove") { (view: ExpoMapboxNavigationView, value: String) in
|
|
57
|
+
view.placeCustomRasterLayerAbove = value
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
Prop("disableAlternativeRoutes") { (view: ExpoMapboxNavigationView, value: Bool) in
|
|
61
|
+
view.disableAlternativeRoutes = value
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
Prop("showsEndOfRouteFeedback") { (view: ExpoMapboxNavigationView, value: Bool) in
|
|
65
|
+
view.showsEndOfRouteFeedback = value
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ─── Events ───────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
Events(
|
|
71
|
+
"onRouteProgressChanged",
|
|
72
|
+
"onWaypointArrival",
|
|
73
|
+
"onFinalDestinationArrival",
|
|
74
|
+
"onCancelNavigation",
|
|
75
|
+
"onRouteChanged",
|
|
76
|
+
"onUserOffRoute",
|
|
77
|
+
"onRoutesLoaded"
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import MapboxNavigationCore
|
|
3
|
+
import MapboxNavigationUIKit
|
|
4
|
+
import MapboxMaps
|
|
5
|
+
import MapboxDirections
|
|
6
|
+
|
|
7
|
+
class ExpoMapboxNavigationView: ExpoView {
|
|
8
|
+
|
|
9
|
+
// ─── Props ──────────────────────────────────────────────────────────────
|
|
10
|
+
var coordinates: [[String: Double]] = []
|
|
11
|
+
var waypointIndices: [Int] = []
|
|
12
|
+
var useRouteMatchingApi: Bool = false
|
|
13
|
+
var locale: String = ""
|
|
14
|
+
var routeProfile: String = "mapbox/driving-traffic"
|
|
15
|
+
var routeExcludeList: [String] = []
|
|
16
|
+
var mapStyle: String? = nil
|
|
17
|
+
var mute: Bool = false
|
|
18
|
+
var vehicleMaxHeight: Double? = nil
|
|
19
|
+
var vehicleMaxWidth: Double? = nil
|
|
20
|
+
var customRasterSourceUrl: String? = nil
|
|
21
|
+
var placeCustomRasterLayerAbove: String? = nil
|
|
22
|
+
var disableAlternativeRoutes: Bool = false
|
|
23
|
+
var showsEndOfRouteFeedback: Bool = false
|
|
24
|
+
|
|
25
|
+
// ─── Events ─────────────────────────────────────────────────────────────
|
|
26
|
+
let onRouteProgressChanged = EventDispatcher()
|
|
27
|
+
let onWaypointArrival = EventDispatcher()
|
|
28
|
+
let onFinalDestinationArrival = EventDispatcher()
|
|
29
|
+
let onCancelNavigation = EventDispatcher()
|
|
30
|
+
let onRouteChanged = EventDispatcher()
|
|
31
|
+
let onUserOffRoute = EventDispatcher()
|
|
32
|
+
let onRoutesLoaded = EventDispatcher()
|
|
33
|
+
|
|
34
|
+
// ─── Internal ───────────────────────────────────────────────────────────
|
|
35
|
+
private var navigationViewController: NavigationViewController?
|
|
36
|
+
private var isNavigationStarted = false
|
|
37
|
+
private var currentWaypointIndex = 0
|
|
38
|
+
private var totalWaypoints = 0
|
|
39
|
+
|
|
40
|
+
required init(appContext: AppContext? = nil) {
|
|
41
|
+
super.init(appContext: appContext)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
func startNavigationIfReady() {
|
|
45
|
+
guard coordinates.count >= 2, !isNavigationStarted else { return }
|
|
46
|
+
isNavigationStarted = true
|
|
47
|
+
setupNavigation()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
override func layoutSubviews() {
|
|
51
|
+
super.layoutSubviews()
|
|
52
|
+
startNavigationIfReady()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ─── Navigation Setup ───────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
private func setupNavigation() {
|
|
58
|
+
// Construire les waypoints
|
|
59
|
+
var allWaypoints: [Waypoint] = []
|
|
60
|
+
|
|
61
|
+
for (index, coord) in coordinates.enumerated() {
|
|
62
|
+
guard let lat = coord["latitude"], let lng = coord["longitude"] else { continue }
|
|
63
|
+
let location = CLLocationCoordinate2D(latitude: lat, longitude: lng)
|
|
64
|
+
let wp = Waypoint(coordinate: location)
|
|
65
|
+
|
|
66
|
+
// Marquer les waypoints intermédiaires vs simples pass-through
|
|
67
|
+
if !waypointIndices.isEmpty {
|
|
68
|
+
wp.separatesLegs = waypointIndices.contains(index)
|
|
69
|
+
} else {
|
|
70
|
+
wp.separatesLegs = true
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
allWaypoints.append(wp)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
totalWaypoints = allWaypoints.filter { $0.separatesLegs }.count
|
|
77
|
+
|
|
78
|
+
// Choisir le profil de routing
|
|
79
|
+
let profile: ProfileIdentifier
|
|
80
|
+
switch routeProfile {
|
|
81
|
+
case "mapbox/driving": profile = .automobile
|
|
82
|
+
case "mapbox/walking": profile = .walking
|
|
83
|
+
case "mapbox/cycling": profile = .cycling
|
|
84
|
+
default: profile = .automobileAvoidingTraffic
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Options de route
|
|
88
|
+
let routeOptions = NavigationRouteOptions(waypoints: allWaypoints, profileIdentifier: profile)
|
|
89
|
+
|
|
90
|
+
let resolvedLocale = locale.isEmpty ? Locale.current.identifier : locale
|
|
91
|
+
routeOptions.locale = Locale(identifier: resolvedLocale)
|
|
92
|
+
routeOptions.includesSpokenInstructions = true
|
|
93
|
+
routeOptions.includesVisualInstructions = true
|
|
94
|
+
|
|
95
|
+
// Exclusions
|
|
96
|
+
if !routeExcludeList.isEmpty {
|
|
97
|
+
routeOptions.roadClassesToAvoid = buildRoadClassesToAvoid(routeExcludeList)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Restrictions de véhicule
|
|
101
|
+
if let maxH = vehicleMaxHeight {
|
|
102
|
+
routeOptions.maximumHeight = Measurement(value: maxH, unit: .meters)
|
|
103
|
+
}
|
|
104
|
+
if let maxW = vehicleMaxWidth {
|
|
105
|
+
routeOptions.maximumWidth = Measurement(value: maxW, unit: .meters)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if disableAlternativeRoutes {
|
|
109
|
+
routeOptions.includesAlternativeRoutes = false
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Calcul de la route
|
|
113
|
+
Directions.shared.calculate(routeOptions) { [weak self] (_, result) in
|
|
114
|
+
guard let self = self else { return }
|
|
115
|
+
switch result {
|
|
116
|
+
case .failure(let error):
|
|
117
|
+
print("[ExpoMapboxNavigation] Erreur calcul route: \(error.localizedDescription)")
|
|
118
|
+
case .success(let response):
|
|
119
|
+
guard let routes = response.routes, !routes.isEmpty else { return }
|
|
120
|
+
self.onRoutesLoaded([:])
|
|
121
|
+
self.presentNavigationUI(routes: routes, options: routeOptions)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private func presentNavigationUI(routes: [Route], options: NavigationRouteOptions) {
|
|
127
|
+
DispatchQueue.main.async { [weak self] in
|
|
128
|
+
guard let self = self else { return }
|
|
129
|
+
|
|
130
|
+
let navigationService = MapboxNavigationService(
|
|
131
|
+
routes: routes,
|
|
132
|
+
customRoutingProvider: NavigationSettings.shared.directions,
|
|
133
|
+
credentials: NavigationSettings.shared.directions.credentials,
|
|
134
|
+
simulating: .onPoorGPS
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
let navigationOptions = NavigationOptions(navigationService: navigationService)
|
|
138
|
+
let navVC = NavigationViewController(
|
|
139
|
+
for: routes,
|
|
140
|
+
routeIndex: 0,
|
|
141
|
+
routeOptions: options,
|
|
142
|
+
navigationOptions: navigationOptions
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
navVC.delegate = self
|
|
146
|
+
navVC.showsEndOfRouteFeedback = self.showsEndOfRouteFeedback
|
|
147
|
+
navVC.showsReportFeedback = false
|
|
148
|
+
|
|
149
|
+
if self.mute {
|
|
150
|
+
navVC.navigationService.router.reroutesProactively = false
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
self.navigationViewController = navVC
|
|
154
|
+
|
|
155
|
+
guard let parentVC = self.parentViewController else { return }
|
|
156
|
+
parentVC.addChild(navVC)
|
|
157
|
+
navVC.view.frame = self.bounds
|
|
158
|
+
navVC.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
159
|
+
self.addSubview(navVC.view)
|
|
160
|
+
navVC.didMove(toParent: parentVC)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private func buildRoadClassesToAvoid(_ list: [String]) -> RoadClasses {
|
|
165
|
+
var classes = RoadClasses()
|
|
166
|
+
for item in list {
|
|
167
|
+
switch item {
|
|
168
|
+
case "toll": classes.insert(.toll)
|
|
169
|
+
case "ferry": classes.insert(.ferry)
|
|
170
|
+
case "motorway": classes.insert(.motorway)
|
|
171
|
+
default: break
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return classes
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private var parentViewController: UIViewController? {
|
|
178
|
+
var responder: UIResponder? = self
|
|
179
|
+
while let r = responder {
|
|
180
|
+
if let vc = r as? UIViewController { return vc }
|
|
181
|
+
responder = r.next
|
|
182
|
+
}
|
|
183
|
+
return nil
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ─── NavigationViewControllerDelegate ───────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
extension ExpoMapboxNavigationView: NavigationViewControllerDelegate {
|
|
190
|
+
|
|
191
|
+
func navigationViewController(
|
|
192
|
+
_ vc: NavigationViewController,
|
|
193
|
+
didUpdate progress: RouteProgress,
|
|
194
|
+
with location: CLLocation,
|
|
195
|
+
rawLocation: CLLocation
|
|
196
|
+
) {
|
|
197
|
+
onRouteProgressChanged([
|
|
198
|
+
"distanceRemaining": progress.currentLegProgress.distanceRemaining,
|
|
199
|
+
"distanceTraveled": progress.distanceTraveled,
|
|
200
|
+
"durationRemaining": progress.durationRemaining,
|
|
201
|
+
"fractionTraveled": progress.fractionTraveled
|
|
202
|
+
])
|
|
203
|
+
|
|
204
|
+
// Détection hors route
|
|
205
|
+
if progress.currentLegProgress.userHasArrivedAtWaypoint {
|
|
206
|
+
currentWaypointIndex += 1
|
|
207
|
+
if currentWaypointIndex < totalWaypoints - 1 {
|
|
208
|
+
onWaypointArrival([
|
|
209
|
+
"distanceRemaining": progress.currentLegProgress.distanceRemaining,
|
|
210
|
+
"distanceTraveled": progress.distanceTraveled,
|
|
211
|
+
"durationRemaining": progress.durationRemaining,
|
|
212
|
+
"fractionTraveled": progress.fractionTraveled
|
|
213
|
+
])
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
func navigationViewControllerDidFinishRouting(_ vc: NavigationViewController) {
|
|
219
|
+
onFinalDestinationArrival([:])
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
func navigationViewControllerDidDismiss(_ vc: NavigationViewController, byCanceling canceled: Bool) {
|
|
223
|
+
if canceled {
|
|
224
|
+
onCancelNavigation([:])
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
func navigationViewController(_ vc: NavigationViewController, didRerouteAlong route: Route) {
|
|
229
|
+
onRouteChanged([:])
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
func navigationViewController(_ vc: NavigationViewController, shouldRerouteFrom location: CLLocation) -> Bool {
|
|
233
|
+
onUserOffRoute([:])
|
|
234
|
+
return true
|
|
235
|
+
}
|
|
236
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jacques_gordon/expo-mapbox-navigation",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Expo module for Mapbox turn-by-turn navigation — iOS & Android, compatible with Expo SDK 53 Dev Client",
|
|
5
|
+
"main": "build/index.js",
|
|
6
|
+
"types": "build/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"build",
|
|
9
|
+
"ios",
|
|
10
|
+
"android",
|
|
11
|
+
"plugin/build",
|
|
12
|
+
"app.plugin.js",
|
|
13
|
+
"expo-module.config.json",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "expo-module build",
|
|
18
|
+
"clean": "expo-module clean",
|
|
19
|
+
"lint": "expo-module lint",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"prepare": "expo-module prepare",
|
|
22
|
+
"prepublishOnly": "expo-module prepublishOnly"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"expo",
|
|
26
|
+
"mapbox",
|
|
27
|
+
"navigation",
|
|
28
|
+
"turn-by-turn",
|
|
29
|
+
"react-native",
|
|
30
|
+
"expo-module",
|
|
31
|
+
"gps",
|
|
32
|
+
"waypoints"
|
|
33
|
+
],
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/jacquesngomeheffa/expo-mapbox-navigation.git"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/jacquesngomeheffa/expo-mapbox-navigation/issues"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/jacquesngomeheffa/expo-mapbox-navigation#readme",
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"expo": ">=53.0.0",
|
|
45
|
+
"react": "*",
|
|
46
|
+
"react-native": "*"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@expo/config-plugins": "^9.0.0",
|
|
50
|
+
"@types/react": "^19.2.17",
|
|
51
|
+
"expo-module-scripts": "^56.0.3",
|
|
52
|
+
"expo-modules-core": "^2.0.0",
|
|
53
|
+
"typescript": "^5.3.0"
|
|
54
|
+
},
|
|
55
|
+
"expo": {
|
|
56
|
+
"fyi": "https://expo.fyi/",
|
|
57
|
+
"config": "./plugin/build/index.js"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"react": "^19.2.7",
|
|
61
|
+
"react-native": "^0.86.0"
|
|
62
|
+
}
|
|
63
|
+
}
|