diff --git a/.gitignore b/.gitignore index 5ce292510..0660e8993 100644 --- a/.gitignore +++ b/.gitignore @@ -114,3 +114,7 @@ app.*.symbols !/dev/ci/**/Gemfile.lock !**/Podfile.lock !**/example/pubspec.lock +android/gradlew +android/gradle/wrapper/gradle-wrapper.jar +example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +example/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/CHANGELOG.md b/CHANGELOG.md index 7806b304d..40a2ad1e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ > [!IMPORTANT] > Configuring Mapbox's secret token is no longer required when installing our SDKs. +### 2.4.1 +* Integrates Navigation SDK with the Mapbox View. + ### 2.4.0 * Update Maps SDK to 11.8.0 diff --git a/android/build.gradle b/android/build.gradle index 4235537aa..907b9b996 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -61,12 +61,12 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "11" } } @@ -78,8 +78,37 @@ if (file("$rootDir/gradle/ktlint.gradle").exists() && file("$rootDir/gradle/lint } dependencies { - implementation "com.mapbox.maps:android:11.8.0" - implementation "androidx.annotation:annotation:1.5.0" implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2" + implementation "com.google.android.gms:play-services-location:21.0.1" + + implementation "com.mapbox.common:common-ndk27:24.8.0" + implementation "com.mapbox.maps:android-core-ndk27:11.8.0" + implementation "com.mapbox.maps:android-ndk27:11.8.0" + implementation ("com.mapbox.navigationcore:navigation:3.5.0") { + exclude group: "com.mapbox.common", module: "common" + exclude group: "com.mapbox.maps", module: "android" + exclude group: "com.mapbox.maps", module: "base" + exclude group: "com.mapbox.maps", module: "android-core" + } + implementation ("com.mapbox.navigationcore:ui-components:3.5.0") { + exclude group: "com.mapbox.common", module: "common" + exclude group: "com.mapbox.maps", module: "android" + exclude group: "com.mapbox.maps", module: "base" + exclude group: "com.mapbox.maps", module: "android-core" + } + + configurations.all { + resolutionStrategy { + force "com.mapbox.maps:android-ndk27:11.8.0" + force "com.mapbox.maps:base-ndk27:11.8.0" + force "com.mapbox.common:common-ndk27:24.8.0" + force "com.mapbox.maps:android-core-ndk27:11.8.0" + force "com.mapbox.plugin:maps-locationcomponent-ndk27:11.8.0" + force "com.mapbox.plugin:maps-lifecycle-ndk27:11.8.0" + force "com.mapbox.plugin:maps-gestures-ndk27:11.8.0" + force "com.mapbox.plugin:maps-viewport-ndk27:11.8.0" + force "com.mapbox.module:maps-telemetry-ndk27:11.8.0" + } + } } diff --git a/android/gradle.properties b/android/gradle.properties index 4de64f2eb..16330245b 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,3 +1,3 @@ org.gradle.jvmargs=-Xmx4096M android.useAndroidX=true -android.enableJetifier=true +android.enableJetifier=true \ No newline at end of file diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 111c25e79..1af9e0930 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index f9bedf3c1..44ce175db 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,3 +1,4 @@ - + + + diff --git a/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/MapboxMapController.kt b/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/MapboxMapController.kt index acb4f8299..608050443 100644 --- a/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/MapboxMapController.kt +++ b/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/MapboxMapController.kt @@ -26,6 +26,9 @@ import com.mapbox.maps.mapbox_maps.pigeons._AnimationManager import com.mapbox.maps.mapbox_maps.pigeons._CameraManager import com.mapbox.maps.mapbox_maps.pigeons._LocationComponentSettingsInterface import com.mapbox.maps.mapbox_maps.pigeons._MapInterface +import com.mapbox.maps.mapbox_maps.pigeons.NavigationInterface +import com.mapbox.maps.plugin.locationcomponent.location +import com.mapbox.navigation.ui.maps.location.NavigationLocationProvider import io.flutter.embedding.android.FlutterActivity import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.MethodCall @@ -64,9 +67,16 @@ class MapboxMapController( private val attributionController: AttributionController private val scaleBarController: ScaleBarController private val compassController: CompassController + private var navigationController: NavigationController? = null private val eventHandler: MapboxEventHandler + /** + * [NavigationLocationProvider] is a utility class that helps to provide location updates generated by the Navigation SDK + * to the Maps SDK in order to update the user location indicator on the map. + */ + private val navigationLocationProvider = NavigationLocationProvider() + /* Mirrors lifecycle of the parent, with one addition - switches to DESTROYED state when `dispose` is called. @@ -177,14 +187,20 @@ class MapboxMapController( false -> true } lifecycleHelper = LifecycleHelper(lifecycleProvider.getLifecycle()!!, shouldDestroyOnDestroy) - + mapView?.location?.setLocationProvider(navigationLocationProvider) mapView?.setViewTreeLifecycleOwner(lifecycleHelper) + + navigationController = NavigationController(context, mapView!!, lifecycleProvider.getLifecycle()!!) + + NavigationInterface.setUp(messenger, navigationController, channelSuffix) } override fun onFlutterViewDetached() { super.onFlutterViewDetached() lifecycleHelper?.dispose() lifecycleHelper = null + navigationController?.dispose() + navigationController = null mapView!!.setViewTreeLifecycleOwner(null) } @@ -192,6 +208,8 @@ class MapboxMapController( if (mapView == null) { return } + navigationController?.dispose() + navigationController = null lifecycleHelper?.dispose() lifecycleHelper = null mapView = null @@ -209,6 +227,7 @@ class MapboxMapController( CompassSettingsInterface.setUp(messenger, null, channelSuffix) ScaleBarSettingsInterface.setUp(messenger, null, channelSuffix) AttributionSettingsInterface.setUp(messenger, null, channelSuffix) + NavigationInterface.setUp(messenger, null, channelSuffix) } override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { @@ -227,6 +246,14 @@ class MapboxMapController( gestureController.removeListeners() result.success(null) } + "navigation#add_listeners" -> { + navigationController?.addListeners(messenger, channelSuffix) + result.success(null) + } + "navigation#remove_listeners" -> { + navigationController?.removeListeners() + result.success(null) + } "platform#releaseMethodChannels" -> { dispose() result.success(null) diff --git a/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/MapboxMapsPlugin.kt b/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/MapboxMapsPlugin.kt index 6389b3d32..584c4819d 100644 --- a/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/MapboxMapsPlugin.kt +++ b/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/MapboxMapsPlugin.kt @@ -4,6 +4,7 @@ import android.content.Context import androidx.lifecycle.Lifecycle import com.mapbox.maps.mapbox_maps.offline.OfflineMapInstanceManager import com.mapbox.maps.mapbox_maps.offline.OfflineSwitch +import com.mapbox.maps.mapbox_maps.pigeons.NavigationInterface import com.mapbox.maps.mapbox_maps.pigeons._MapboxMapsOptions import com.mapbox.maps.mapbox_maps.pigeons._MapboxOptions import com.mapbox.maps.mapbox_maps.pigeons._OfflineMapInstanceManager @@ -11,6 +12,8 @@ import com.mapbox.maps.mapbox_maps.pigeons._OfflineSwitch import com.mapbox.maps.mapbox_maps.pigeons._SnapshotterInstanceManager import com.mapbox.maps.mapbox_maps.pigeons._TileStoreInstanceManager import com.mapbox.maps.mapbox_maps.snapshot.SnapshotterInstanceManager +import com.mapbox.navigation.base.options.NavigationOptions +import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterAssets import io.flutter.embedding.engine.plugins.activity.ActivityAware @@ -38,6 +41,11 @@ class MapboxMapsPlugin : FlutterPlugin, ActivityAware { ) ) setupStaticChannels(flutterPluginBinding.applicationContext, flutterPluginBinding.binaryMessenger, flutterPluginBinding.flutterAssets) + + MapboxNavigationApp.setup( + NavigationOptions.Builder(flutterPluginBinding.applicationContext) + .build() + ) } private fun setupStaticChannels(context: Context, binaryMessenger: BinaryMessenger, flutterAssets: FlutterAssets) { diff --git a/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/NavigationController.kt b/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/NavigationController.kt new file mode 100644 index 000000000..425929b9b --- /dev/null +++ b/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/NavigationController.kt @@ -0,0 +1,376 @@ +package com.mapbox.maps.mapbox_maps + +import kotlinx.coroutines.launch +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.Resources +import android.util.Log +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.coroutineScope +import com.google.android.gms.location.FusedLocationProviderClient +import com.google.android.gms.location.LocationServices +import com.mapbox.maps.MapView +import com.mapbox.geojson.Point +import com.mapbox.common.location.Location +import com.mapbox.maps.EdgeInsets +import com.mapbox.maps.plugin.animation.camera +import com.mapbox.maps.plugin.locationcomponent.location +import com.mapbox.maps.mapbox_maps.pigeons.* +import com.mapbox.maps.mapbox_maps.mapping.* +import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions +import com.mapbox.navigation.base.route.NavigationRoute +import com.mapbox.navigation.base.route.NavigationRouterCallback +import com.mapbox.navigation.base.route.RouterFailure +import com.mapbox.navigation.base.route.RouterOrigin +import com.mapbox.navigation.core.MapboxNavigation +import com.mapbox.navigation.core.directions.session.RoutesObserver +import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp +import com.mapbox.navigation.core.trip.session.LocationMatcherResult +import com.mapbox.navigation.core.trip.session.LocationObserver +import com.mapbox.navigation.core.trip.session.RouteProgressObserver +import com.mapbox.navigation.ui.maps.camera.NavigationCamera +import com.mapbox.navigation.ui.maps.camera.data.MapboxNavigationViewportDataSource +import com.mapbox.navigation.ui.maps.camera.lifecycle.NavigationBasicGesturesHandler +import com.mapbox.navigation.ui.maps.camera.transition.NavigationCameraTransitionOptions +import com.mapbox.navigation.ui.maps.location.NavigationLocationProvider +import com.mapbox.navigation.ui.maps.route.line.MapboxRouteLineApiExtensions.setNavigationRoutes +import com.mapbox.navigation.ui.maps.route.line.api.MapboxRouteLineApi +import com.mapbox.navigation.ui.maps.route.line.api.MapboxRouteLineView +import com.mapbox.navigation.ui.maps.route.line.model.MapboxRouteLineApiOptions +import com.mapbox.navigation.ui.maps.route.line.model.MapboxRouteLineViewOptions +import com.mapbox.api.directions.v5.models.RouteOptions +import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI +import com.mapbox.navigation.ui.maps.NavigationStyles +import com.mapbox.navigation.ui.maps.route.line.api.RoutesRenderedCallback +import com.mapbox.navigation.ui.maps.route.line.api.RoutesRenderedResult +import io.flutter.plugin.common.BinaryMessenger + +@OptIn(ExperimentalPreviewMapboxNavigationAPI::class) +class NavigationController( + private val context: Context, + private val mapView: MapView, + override val lifecycle: Lifecycle +) : NavigationInterface, + LifecycleOwner { + + private var fltNavigationListener: NavigationListener? = null + + fun addListeners(messenger: BinaryMessenger, channelSuffix: String) { + fltNavigationListener = NavigationListener(messenger, channelSuffix) + } + + fun removeListeners() { + } + + /** + * Used to execute camera transitions based on the data generated by the [viewportDataSource]. + * This includes transitions from route overview to route following and continuously updating the camera as the location changes. + */ + private val navigationCamera: NavigationCamera + + /** + * Produces the camera frames based on the location and routing data for the [navigationCamera] to execute. + */ + private lateinit var viewportDataSource: MapboxNavigationViewportDataSource + + private lateinit var navigationLocationProvider: NavigationLocationProvider + + /* + * Below are generated camera padding values to ensure that the route fits well on screen while + * other elements are overlaid on top of the map (including instruction view, buttons, etc.) + */ + private val pixelDensity = Resources.getSystem().displayMetrics.density + private val overviewPadding: EdgeInsets by lazy { + EdgeInsets( + 140.0 * pixelDensity, + 40.0 * pixelDensity, + 120.0 * pixelDensity, + 40.0 * pixelDensity + ) + } + private val followingPadding: EdgeInsets by lazy { + EdgeInsets( + 180.0 * pixelDensity, + 40.0 * pixelDensity, + 150.0 * pixelDensity, + 40.0 * pixelDensity + ) + } + + /** + * Generates updates for the [routeLineView] with the geometries and properties of the routes that should be drawn on the map. + */ + private val routeLineApi: MapboxRouteLineApi + + /** + * Draws route lines on the map based on the data from the [routeLineApi] + */ + private val routeLineView: MapboxRouteLineView + + /** + * Gets notified with location updates. + * + * Exposes raw updates coming directly from the location services + * and the updates enhanced by the Navigation SDK (cleaned up and matched to the road). + */ + private val locationObserver: LocationObserver + + /** + * Gets notified with progress along the currently active route. + */ + private val routeProgressObserver = RouteProgressObserver { routeProgress -> + // update the camera position to account for the progressed fragment of the route + viewportDataSource.onRouteProgressChanged(routeProgress) + //viewportDataSource.evaluate() + + this.fltNavigationListener?.onRouteProgress(routeProgress.toFLT()) {} + } + + /** + * Gets notified whenever the tracked routes change. + * + * A change can mean: + * - routes get changed with [MapboxNavigation.setNavigationRoutes] + * - routes annotations get refreshed (for example, congestion annotation that indicate the live traffic along the route) + * - driver got off route and a reroute was executed + */ + private val routesObserver: RoutesObserver + + private var mapboxNavigation: MapboxNavigation? + + private fun setNavigationRoutes(routes: List) { + // disable navigation camera + navigationCamera.requestNavigationCameraToIdle() + // set a route to receive route progress updates and provide a route reference + // to the viewport data source (via RoutesObserver) + mapboxNavigation?.setNavigationRoutes(routes) { data -> + if (data.isError) { + println(data.error) + } + } + // enable the camera back + navigationCamera.requestNavigationCameraToOverview() + } + + init { + val mapboxMap = this.mapView.mapboxMap + + this.navigationLocationProvider = + this.mapView.location.getLocationProvider() as NavigationLocationProvider + + // initialize Navigation Camera + viewportDataSource = MapboxNavigationViewportDataSource(mapboxMap) + navigationCamera = NavigationCamera( + mapboxMap, + this.mapView.camera, + viewportDataSource + ) + // set the animations lifecycle listener to ensure the NavigationCamera stops + // automatically following the user location when the map is interacted with + this.mapView.camera.addCameraAnimationsLifecycleListener( + NavigationBasicGesturesHandler(navigationCamera) + ) + navigationCamera.registerNavigationCameraStateChangeObserver { navigationCameraState -> + com.mapbox.maps.mapbox_maps.pigeons.NavigationCameraState.ofRaw(navigationCameraState.ordinal) + ?.let { + fltNavigationListener?.onNavigationCameraStateChanged( + it + ) {} + } + } + // set the padding values depending to correctly frame maneuvers and the puck + viewportDataSource.overviewPadding = overviewPadding + viewportDataSource.followingPadding = followingPadding + + // initialize route line, the routeLineBelowLayerId is specified to place + // the route line below road labels layer on the map + // the value of this option will depend on the style that you are using + // and under which layer the route line should be placed on the map layers stack + val belowLayerId = if (mapView.mapboxMap.style?.styleLayerExists("road-label") == true) { + "road-label" + } else { + "mapbox-location-indicator-layer" + } + + val mapboxRouteLineOptions = MapboxRouteLineViewOptions.Builder(context) + .routeLineBelowLayerId(belowLayerId) + .build() + routeLineApi = MapboxRouteLineApi(MapboxRouteLineApiOptions.Builder().build()) + routeLineView = MapboxRouteLineView(mapboxRouteLineOptions) + + locationObserver = object : LocationObserver { + + override fun onNewRawLocation(rawLocation: Location) { + // not handled + } + + override fun onNewLocationMatcherResult(locationMatcherResult: LocationMatcherResult) { + val enhancedLocation = locationMatcherResult.enhancedLocation + // update location puck's position on the map + navigationLocationProvider.changePosition( + location = enhancedLocation, + keyPoints = locationMatcherResult.keyPoints, + ) + + // update camera position to account for new location + viewportDataSource.onLocationChanged(enhancedLocation) + viewportDataSource.evaluate() + + fltNavigationListener?.onNewLocation(enhancedLocation.toFLT()) {} + } + } + + routesObserver = RoutesObserver { routeUpdateResult -> + lifecycle.coroutineScope.launch { + if (routeUpdateResult.navigationRoutes.isNotEmpty()) { + routeLineApi.setNavigationRoutes( + newRoutes = routeUpdateResult.navigationRoutes, + ).apply { + if (mapView.mapboxMap.style != null) { + routeLineView.renderRouteDrawData( + mapView.mapboxMap.style!!, + this, + mapView.mapboxMap, + object : RoutesRenderedCallback { + override fun onRoutesRendered( + result: RoutesRenderedResult + ) { + println(result) + } + } + ) + } + } + // update the camera position to account for the new route + viewportDataSource.onRouteChanged(routeUpdateResult.navigationRoutes.first()) + viewportDataSource.evaluate() + + fltNavigationListener?.onNavigationRouteRendered() { } + } else { + routeLineApi.clearRouteLine { value -> + routeLineView.renderClearRouteLineValue( + mapView.mapboxMap.style!!, + value + ) + } + // remove the route reference from camera position evaluations + viewportDataSource.clearRouteData() + viewportDataSource.evaluate() + } + } + } + + MapboxNavigationApp.attach(this) + mapboxNavigation = MapboxNavigationApp.current() + + mapboxNavigation!!.registerRoutesObserver(routesObserver) + mapboxNavigation!!.registerLocationObserver(locationObserver) + mapboxNavigation!!.registerRouteProgressObserver(routeProgressObserver) + } + + fun dispose() { + mapboxNavigation!!.stopTripSession() + mapboxNavigation!!.unregisterRoutesObserver(routesObserver) + mapboxNavigation!!.unregisterLocationObserver(locationObserver) + mapboxNavigation!!.unregisterRouteProgressObserver(routeProgressObserver) + routeLineApi.cancel() + routeLineView.cancel() + } + + @SuppressLint("MissingPermission") + override fun setRoute(options: com.mapbox.maps.mapbox_maps.pigeons.RouteOptions, callback: (Result) -> Unit) { + val routeBuilder = RouteOptions.builder() + .applyDefaultNavigationOptions() + + if(options.alternatives != null) { + routeBuilder.alternatives(options.alternatives) + } + if(options.steps != null) { + routeBuilder.alternatives(options.steps) + } + if(options.coordinates != null) { + routeBuilder.coordinatesList(options.coordinates!!) + } + if(options.waypoints != null) { + routeBuilder.coordinatesList(options.waypoints.map { it.point }) + routeBuilder.waypointNamesList(options.waypoints.map { it.name }) + } + if(options.voiceInstructions != null) { + routeBuilder.voiceInstructions(options.voiceInstructions) + } + + mapboxNavigation?.requestRoutes( + routeBuilder.build(), + + object : NavigationRouterCallback { + override fun onRoutesReady( + routes: List, + @RouterOrigin routerOrigin: String + ) { + setNavigationRoutes(routes) + fltNavigationListener?.onNavigationRouteReady() { } + } + + override fun onFailure( + reasons: List, + routeOptions: RouteOptions + ) { + fltNavigationListener?.onNavigationRouteFailed() { } + } + + override fun onCanceled( + routeOptions: RouteOptions, + @RouterOrigin routerOrigin: String + ) { + fltNavigationListener?.onNavigationRouteCancelled() { } + } + } + ) + NavigationStyles.NAVIGATION_DAY_STYLE + callback(Result.success(Unit)) + } + + override fun stopTripSession(callback: (Result) -> Unit) { + // disable navigation camera + navigationCamera.requestNavigationCameraToIdle() + mapboxNavigation?.stopTripSession() + callback(Result.success(Unit)) + } + + @SuppressLint("MissingPermission") + override fun startTripSession(withForegroundService: Boolean, callback: (Result) -> Unit) { + mapboxNavigation!!.startTripSession(withForegroundService) + callback(Result.success(Unit)) + } + + override fun requestNavigationCameraToFollowing(callback: (Result) -> Unit) { + navigationCamera.requestNavigationCameraToFollowing() + callback(Result.success(Unit)) + } + + override fun requestNavigationCameraToOverview(callback: (Result) -> Unit) { + navigationCamera.requestNavigationCameraToOverview() + callback(Result.success(Unit)) + } + + override fun lastLocation(callback: (Result) -> Unit) { + val point = this.navigationLocationProvider.lastLocation + if (point != null) { + callback.invoke(Result.success(point.toFLT())) + return + } + + val locationProviderClient = LocationServices.getFusedLocationProviderClient(this.context) + val locationTask = locationProviderClient.getLastLocation() + locationTask.addOnSuccessListener { location -> + if (location != null) { + callback.invoke(Result.success(location.toFLT())) + } else { + callback.invoke(Result.success(null)) + } + }.addOnFailureListener { _ -> + callback.invoke(Result.success(null)) + } + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/mapping/NavigationMappings.kt b/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/mapping/NavigationMappings.kt new file mode 100644 index 000000000..e232101f7 --- /dev/null +++ b/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/mapping/NavigationMappings.kt @@ -0,0 +1,83 @@ +package com.mapbox.maps.mapbox_maps.mapping + +import com.mapbox.common.location.Location +import com.mapbox.maps.mapbox_maps.pigeons.NavigationLocation +import com.mapbox.maps.mapbox_maps.pigeons.RouteProgress as NavigationRouteProgress +import com.mapbox.maps.mapbox_maps.pigeons.RouteProgressState as NavigationRouteProgressState +import com.mapbox.maps.mapbox_maps.pigeons.UpcomingRoadObject as NavigationUpcomingRoadObject +import com.mapbox.maps.mapbox_maps.pigeons.RoadObject as NavigationRoadObject +import com.mapbox.maps.mapbox_maps.pigeons.RoadObjectLocationType as NavigationRoadObjectLocationType +import com.mapbox.maps.mapbox_maps.pigeons.RoadObjectDistanceInfo as NavigationRoadObjectDistanceInfo +import com.mapbox.navigation.base.trip.model.RouteProgress +import com.mapbox.navigation.base.trip.model.roadobject.RoadObject +import com.mapbox.navigation.base.trip.model.roadobject.UpcomingRoadObject +import com.mapbox.navigation.base.trip.model.roadobject.distanceinfo.RoadObjectDistanceInfo + +fun Location.toFLT(): NavigationLocation { + return NavigationLocation( + verticalAccuracy = this.verticalAccuracy, + horizontalAccuracy = this.horizontalAccuracy, + monotonicTimestamp = this.monotonicTimestamp, + longitude = this.longitude, + bearingAccuracy = this.bearingAccuracy, + timestamp = this.timestamp, + speedAccuracy = this.speedAccuracy, + floor = this.floor, + speed = this.speed, + source = this.source, + altitude = this.altitude, + latitude = this.latitude, + bearing = this.bearing + ) +} + +fun android.location.Location.toFLT(): NavigationLocation { + return NavigationLocation( + longitude = this.longitude, + altitude = this.altitude, + latitude = this.latitude + ) +} + +fun RoadObject.toFLT(): NavigationRoadObject { + return NavigationRoadObject( + id = this.id, + objectType = NavigationRoadObjectLocationType.ofRaw(this.objectType), + length = this.length, + provider = this.provider, + isUrban = this.isUrban + ) +} + +fun RoadObjectDistanceInfo.toFLT(): NavigationRoadObjectDistanceInfo { + return NavigationRoadObjectDistanceInfo( + distanceToStart = this.distanceToStart + ) +} + +fun UpcomingRoadObject.toFLT(): NavigationUpcomingRoadObject { + return NavigationUpcomingRoadObject( + roadObject = this.roadObject.toFLT(), + distanceToStart = this.distanceToStart, + distanceInfo = this.distanceInfo?.toFLT() + ) +} + +fun RouteProgress.toFLT(): NavigationRouteProgress { + return NavigationRouteProgress( + navigationRouteJson = this.navigationRoute.directionsRoute.toJson(), + bannerInstructionsJson = this.bannerInstructions?.toJson(), + voiceInstructionsJson = this.voiceInstructions?.toJson(), + currentState = this.currentState.name.let { NavigationRouteProgressState.valueOf(it) }, + inTunnel = this.inTunnel, + distanceRemaining = this.distanceRemaining.toDouble(), + distanceTraveled = this.distanceTraveled.toDouble(), + durationRemaining = this.durationRemaining.toDouble(), + fractionTraveled = this.fractionTraveled.toDouble(), + remainingWaypoints = this.remainingWaypoints.toLong(), + upcomingRoadObjects = this.upcomingRoadObjects.map { it.toFLT() }, + stale = this.stale, + routeAlternativeId = this.routeAlternativeId, + currentRouteGeometryIndex = this.currentRouteGeometryIndex.toLong(), + ) +} \ No newline at end of file diff --git a/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/pigeons/NavigationMessager.kt b/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/pigeons/NavigationMessager.kt new file mode 100644 index 000000000..ae504c032 --- /dev/null +++ b/android/src/main/kotlin/com/mapbox/maps/mapbox_maps/pigeons/NavigationMessager.kt @@ -0,0 +1,698 @@ +// Autogenerated from Pigeon (v22.6.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon +@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") + +package com.mapbox.maps.mapbox_maps.pigeons + +import android.util.Log +import com.mapbox.maps.mapbox_maps.mapping.turf.FeatureDecoder +import com.mapbox.maps.mapbox_maps.mapping.turf.PointDecoder +import com.mapbox.maps.mapbox_maps.mapping.turf.toList +import io.flutter.plugin.common.BasicMessageChannel +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.MessageCodec +import io.flutter.plugin.common.StandardMessageCodec +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer + +import com.mapbox.geojson.Feature +import com.mapbox.geojson.Point +import com.mapbox.maps.mapbox_maps.mapping.turf.* + +private fun wrapResult(result: Any?): List { + return listOf(result) +} + +private fun wrapError(exception: Throwable): List { + return if (exception is FlutterError) { + listOf( + exception.code, + exception.message, + exception.details + ) + } else { + listOf( + exception.javaClass.simpleName, + exception.toString(), + "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) + ) + } +} + +private fun createConnectionError(channelName: String): FlutterError { + return FlutterError("channel-error", "Unable to establish connection on channel: '$channelName'.", "")} + +enum class NavigationCameraState(val raw: Int) { + IDLE(0), + TRANSITION_TO_FOLLOWING(1), + FOLLOWING(2), + TRANSITION_TO_OVERVIEW(3), + OVERVIEW(4); + + companion object { + fun ofRaw(raw: Int): NavigationCameraState? { + return values().firstOrNull { it.raw == raw } + } + } +} + +enum class RouteProgressState(val raw: Int) { + INITIALIZED(0), + TRACKING(1), + COMPLETE(2), + OFF_ROUTE(3), + UNCERTAIN(4); + + companion object { + fun ofRaw(raw: Int): RouteProgressState? { + return values().firstOrNull { it.raw == raw } + } + } +} + +enum class RoadObjectLocationType(val raw: Int) { + GANTRY(0), + OPEN_LR_LINE(1), + OPEN_LR_POINT(2), + POINT(3), + POLYGON(4), + POLYLINE(5), + ROUTE_ALERT(6), + SUBGRAPH(7); + + companion object { + fun ofRaw(raw: Int): RoadObjectLocationType? { + return values().firstOrNull { it.raw == raw } + } + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class NavigationLocation ( + val latitude: Double? = null, + val longitude: Double? = null, + val timestamp: Long? = null, + val monotonicTimestamp: Long? = null, + val altitude: Double? = null, + val horizontalAccuracy: Double? = null, + val verticalAccuracy: Double? = null, + val speed: Double? = null, + val speedAccuracy: Double? = null, + val bearing: Double? = null, + val bearingAccuracy: Double? = null, + val floor: Long? = null, + val source: String? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): NavigationLocation { + val latitude = pigeonVar_list[0] as Double? + val longitude = pigeonVar_list[1] as Double? + val timestamp = pigeonVar_list[2] as Long? + val monotonicTimestamp = pigeonVar_list[3] as Long? + val altitude = pigeonVar_list[4] as Double? + val horizontalAccuracy = pigeonVar_list[5] as Double? + val verticalAccuracy = pigeonVar_list[6] as Double? + val speed = pigeonVar_list[7] as Double? + val speedAccuracy = pigeonVar_list[8] as Double? + val bearing = pigeonVar_list[9] as Double? + val bearingAccuracy = pigeonVar_list[10] as Double? + val floor = pigeonVar_list[11] as Long? + val source = pigeonVar_list[12] as String? + return NavigationLocation(latitude, longitude, timestamp, monotonicTimestamp, altitude, horizontalAccuracy, verticalAccuracy, speed, speedAccuracy, bearing, bearingAccuracy, floor, source) + } + } + fun toList(): List { + return listOf( + latitude, + longitude, + timestamp, + monotonicTimestamp, + altitude, + horizontalAccuracy, + verticalAccuracy, + speed, + speedAccuracy, + bearing, + bearingAccuracy, + floor, + source, + ) + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class RoadObject ( + val id: String? = null, + val objectType: RoadObjectLocationType? = null, + val length: Double? = null, + val provider: String? = null, + val isUrban: Boolean? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): RoadObject { + val id = pigeonVar_list[0] as String? + val objectType = pigeonVar_list[1] as RoadObjectLocationType? + val length = pigeonVar_list[2] as Double? + val provider = pigeonVar_list[3] as String? + val isUrban = pigeonVar_list[4] as Boolean? + return RoadObject(id, objectType, length, provider, isUrban) + } + } + fun toList(): List { + return listOf( + id, + objectType, + length, + provider, + isUrban, + ) + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class RoadObjectDistanceInfo ( + val distanceToStart: Double? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): RoadObjectDistanceInfo { + val distanceToStart = pigeonVar_list[0] as Double? + return RoadObjectDistanceInfo(distanceToStart) + } + } + fun toList(): List { + return listOf( + distanceToStart, + ) + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class UpcomingRoadObject ( + val roadObject: RoadObject? = null, + val distanceToStart: Double? = null, + val distanceInfo: RoadObjectDistanceInfo? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): UpcomingRoadObject { + val roadObject = pigeonVar_list[0] as RoadObject? + val distanceToStart = pigeonVar_list[1] as Double? + val distanceInfo = pigeonVar_list[2] as RoadObjectDistanceInfo? + return UpcomingRoadObject(roadObject, distanceToStart, distanceInfo) + } + } + fun toList(): List { + return listOf( + roadObject, + distanceToStart, + distanceInfo, + ) + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class RouteProgress ( + val navigationRouteJson: String? = null, + val bannerInstructionsJson: String? = null, + val voiceInstructionsJson: String? = null, + val currentState: RouteProgressState? = null, + val inTunnel: Boolean? = null, + val distanceRemaining: Double? = null, + val distanceTraveled: Double? = null, + val durationRemaining: Double? = null, + val fractionTraveled: Double? = null, + val remainingWaypoints: Long? = null, + val upcomingRoadObjects: List? = null, + val stale: Boolean? = null, + val routeAlternativeId: String? = null, + val currentRouteGeometryIndex: Long? = null, + val inParkingAisle: Boolean? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): RouteProgress { + val navigationRouteJson = pigeonVar_list[0] as String? + val bannerInstructionsJson = pigeonVar_list[1] as String? + val voiceInstructionsJson = pigeonVar_list[2] as String? + val currentState = pigeonVar_list[3] as RouteProgressState? + val inTunnel = pigeonVar_list[4] as Boolean? + val distanceRemaining = pigeonVar_list[5] as Double? + val distanceTraveled = pigeonVar_list[6] as Double? + val durationRemaining = pigeonVar_list[7] as Double? + val fractionTraveled = pigeonVar_list[8] as Double? + val remainingWaypoints = pigeonVar_list[9] as Long? + val upcomingRoadObjects = pigeonVar_list[10] as List? + val stale = pigeonVar_list[11] as Boolean? + val routeAlternativeId = pigeonVar_list[12] as String? + val currentRouteGeometryIndex = pigeonVar_list[13] as Long? + val inParkingAisle = pigeonVar_list[14] as Boolean? + return RouteProgress(navigationRouteJson, bannerInstructionsJson, voiceInstructionsJson, currentState, inTunnel, distanceRemaining, distanceTraveled, durationRemaining, fractionTraveled, remainingWaypoints, upcomingRoadObjects, stale, routeAlternativeId, currentRouteGeometryIndex, inParkingAisle) + } + } + fun toList(): List { + return listOf( + navigationRouteJson, + bannerInstructionsJson, + voiceInstructionsJson, + currentState, + inTunnel, + distanceRemaining, + distanceTraveled, + durationRemaining, + fractionTraveled, + remainingWaypoints, + upcomingRoadObjects, + stale, + routeAlternativeId, + currentRouteGeometryIndex, + inParkingAisle, + ) + } +} +data class Waypoint ( + val point: Point, + val name: String? +) { + companion object { + fun fromList(pigeonVar_list: List): Waypoint { + val point = pigeonVar_list[0] as Point + val name = pigeonVar_list[1] as String? + return Waypoint(point, name) + } + } + fun toList(): List { + return listOf( + point, + name, + ) + } +} +data class RouteOptions ( + val waypoints: List? = null, + val steps: Boolean? = null, + val alternatives: Boolean? = null, + var coordinates: List? = null, + var voiceInstructions: Boolean? = null +) +{ + companion object { + fun fromList(pigeonVar_list: List): RouteOptions { + val waypoints = pigeonVar_list[0] as List? + val steps = pigeonVar_list[1] as Boolean? + val alternatives = pigeonVar_list[2] as Boolean? + val coordinates = pigeonVar_list[3] as List? + val voiceInstructions = pigeonVar_list[4] as Boolean? + return RouteOptions(waypoints, steps, alternatives, coordinates, voiceInstructions) + } + } + fun toList(): List { + return listOf( + waypoints, + steps, + alternatives, + coordinates, + voiceInstructions + ) + } +} +private open class NavigationMessagerPigeonCodec : StandardMessageCodec() { + override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { + return when (type) { + 151.toByte() -> { + return (readValue(buffer) as? List)?.let { + PointDecoder.fromList(it) + } + } + 152.toByte() -> { + return (readValue(buffer) as? List)?.let { + FeatureDecoder.fromList(it) + } + } + 191.toByte() -> { + return (readValue(buffer) as? List)?.let { + NavigationLocation.fromList(it) + } + } + 192.toByte() -> { + return (readValue(buffer) as Long?)?.let { + RouteProgressState.ofRaw(it.toInt()) + } + } + 193.toByte() -> { + return (readValue(buffer) as Long?)?.let { + RoadObjectLocationType.ofRaw(it.toInt()) + } + } + 194.toByte() -> { + return (readValue(buffer) as? List)?.let { + RoadObject.fromList(it) + } + } + 195.toByte() -> { + return (readValue(buffer) as? List)?.let { + RoadObjectDistanceInfo.fromList(it) + } + } + 196.toByte() -> { + return (readValue(buffer) as? List)?.let { + UpcomingRoadObject.fromList(it) + } + } + 197.toByte() -> { + return (readValue(buffer) as? List)?.let { + RouteProgress.fromList(it) + } + } + 198.toByte() -> { + return (readValue(buffer) as Long?)?.let { + NavigationCameraState.ofRaw(it.toInt()) + } + } + 199.toByte() -> { + return (readValue(buffer) as? List)?.let { + Waypoint.fromList(it) + } + } + 200.toByte() -> { + return (readValue(buffer) as? List)?.let { + RouteOptions.fromList(it) + } + } + else -> super.readValueOfType(type, buffer) + } + } + override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { + when (value) { + is Point -> { + stream.write(151) + writeValue(stream, value.toList()) + } + is Feature -> { + stream.write(152) + writeValue(stream, value.toList()) + } + is NavigationLocation -> { + stream.write(191) + writeValue(stream, value.toList()) + } + is RouteProgressState -> { + stream.write(192) + writeValue(stream, value.raw) + } + is RoadObjectLocationType -> { + stream.write(193) + writeValue(stream, value.raw) + } + is RoadObject -> { + stream.write(194) + writeValue(stream, value.toList()) + } + is RoadObjectDistanceInfo -> { + stream.write(195) + writeValue(stream, value.toList()) + } + is UpcomingRoadObject -> { + stream.write(196) + writeValue(stream, value.toList()) + } + is RouteProgress -> { + stream.write(197) + writeValue(stream, value.toList()) + } + is NavigationCameraState -> { + stream.write(198) + writeValue(stream, value.raw) + } + is Waypoint -> { + stream.write(199) + writeValue(stream, value.toList()) + } + is RouteOptions -> { + stream.write(200) + writeValue(stream, value.toList()) + } + else -> super.writeValue(stream, value) + } + } +} + + +/** Generated class from Pigeon that represents Flutter messages that can be called from Kotlin. */ +class NavigationListener(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") { + companion object { + /** The codec used by NavigationListener. */ + val codec: MessageCodec by lazy { + NavigationMessagerPigeonCodec() + } + } + fun onNavigationRouteReady(callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.mapbox_maps_flutter.NavigationListener.onNavigationRouteReady$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(null) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + } else { + callback(Result.success(Unit)) + } + } else { + callback(Result.failure(createConnectionError(channelName))) + } + } + } + fun onNavigationRouteFailed(callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.mapbox_maps_flutter.NavigationListener.onNavigationRouteFailed$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(null) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + } else { + callback(Result.success(Unit)) + } + } else { + callback(Result.failure(createConnectionError(channelName))) + } + } + } + fun onNavigationRouteCancelled(callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.mapbox_maps_flutter.NavigationListener.onNavigationRouteCancelled$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(null) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + } else { + callback(Result.success(Unit)) + } + } else { + callback(Result.failure(createConnectionError(channelName))) + } + } + } + fun onNavigationRouteRendered(callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.mapbox_maps_flutter.NavigationListener.onNavigationRouteRendered$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(null) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + } else { + callback(Result.success(Unit)) + } + } else { + callback(Result.failure(createConnectionError(channelName))) + } + } + } + fun onNewLocation(locationArg: NavigationLocation, callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.mapbox_maps_flutter.NavigationListener.onNewLocation$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(locationArg)) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + } else { + callback(Result.success(Unit)) + } + } else { + callback(Result.failure(createConnectionError(channelName))) + } + } + } + fun onRouteProgress(routeProgressArg: RouteProgress, callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.mapbox_maps_flutter.NavigationListener.onRouteProgress$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(routeProgressArg)) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + } else { + callback(Result.success(Unit)) + } + } else { + callback(Result.failure(createConnectionError(channelName))) + } + } + } + fun onNavigationCameraStateChanged(stateArg: NavigationCameraState, callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.mapbox_maps_flutter.NavigationListener.onNavigationCameraStateChanged$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(stateArg)) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + } else { + callback(Result.success(Unit)) + } + } else { + callback(Result.failure(createConnectionError(channelName))) + } + } + } +} +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface NavigationInterface { + fun setRoute(options: RouteOptions, callback: (Result) -> Unit) + fun stopTripSession(callback: (Result) -> Unit) + fun startTripSession(withForegroundService: Boolean, callback: (Result) -> Unit) + fun requestNavigationCameraToFollowing(callback: (Result) -> Unit) + fun requestNavigationCameraToOverview(callback: (Result) -> Unit) + fun lastLocation(callback: (Result) -> Unit) + + companion object { + /** The codec used by NavigationInterface. */ + val codec: MessageCodec by lazy { + NavigationMessagerPigeonCodec() + } + /** Sets up an instance of `NavigationInterface` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp(binaryMessenger: BinaryMessenger, api: NavigationInterface?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.mapbox_maps_flutter.NavigationInterface.setRoute$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val routeOptionsArg = args[0] as RouteOptions + api.setRoute(routeOptionsArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + reply.reply(wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.mapbox_maps_flutter.NavigationInterface.stopTripSession$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.stopTripSession{ result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + reply.reply(wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.mapbox_maps_flutter.NavigationInterface.startTripSession$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val withForegroundServiceArg = args[0] as Boolean + api.startTripSession(withForegroundServiceArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + reply.reply(wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.mapbox_maps_flutter.NavigationInterface.requestNavigationCameraToFollowing$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.requestNavigationCameraToFollowing{ result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + reply.reply(wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.mapbox_maps_flutter.NavigationInterface.requestNavigationCameraToOverview$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.requestNavigationCameraToOverview{ result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + reply.reply(wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.mapbox_maps_flutter.NavigationInterface.lastLocation$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.lastLocation{ result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index fd226811f..08bc9cea1 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -31,12 +31,12 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "11" } defaultConfig { diff --git a/example/android/app/src/profile/AndroidManifest.xml b/example/android/app/src/profile/AndroidManifest.xml index dfa0ba8d3..f880684a6 100644 --- a/example/android/app/src/profile/AndroidManifest.xml +++ b/example/android/app/src/profile/AndroidManifest.xml @@ -1,5 +1,4 @@ - + diff --git a/example/android/gradle.properties b/example/android/gradle.properties index 3852c19ca..f806e282e 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -2,3 +2,6 @@ org.gradle.jvmargs=-Xmx4096M android.useAndroidX=true android.enableJetifier=false useLocalDependencies=false +android.defaults.buildfeatures.buildconfig=true +android.nonTransitiveRClass=false +android.nonFinalResIds=false \ No newline at end of file diff --git a/example/android/gradle/wrapper/gradle-wrapper.jar b/example/android/gradle/wrapper/gradle-wrapper.jar index 13372aef5..d64cd4917 100644 Binary files a/example/android/gradle/wrapper/gradle-wrapper.jar and b/example/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index f58dbd5d8..b82aa23a4 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ -#Tue Apr 30 20:27:36 EEST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/example/android/gradlew b/example/android/gradlew index 9d82f7891..1aa94a426 100755 --- a/example/android/gradlew +++ b/example/android/gradlew @@ -1,74 +1,127 @@ -#!/usr/bin/env bash +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum -warn ( ) { +warn () { echo "$*" -} +} >&2 -die ( ) { +die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -77,84 +130,120 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=$((i+1)) + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") -} -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/example/android/settings.gradle b/example/android/settings.gradle index f3dd9b24d..66917cbc4 100644 --- a/example/android/settings.gradle +++ b/example/android/settings.gradle @@ -18,7 +18,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "7.3.0" apply false + id "com.android.application" version "8.5.0" apply false id "org.jetbrains.kotlin.android" version "1.8.22" apply false } diff --git a/example/assets/puck_icon.png b/example/assets/puck_icon.png new file mode 100644 index 000000000..e58cc3731 Binary files /dev/null and b/example/assets/puck_icon.png differ diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 7c5696400..1dc6cf765 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 12.0 + 13.0 diff --git a/example/ios/Podfile b/example/ios/Podfile index e85ddf35f..eba0dabf8 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '12.0' +platform :ios, '14.0' use_frameworks! # CocoaPods analytics sends network stats synchronously affecting flutter build latency. diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 429d47b2a..ae19ecc04 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -4,15 +4,26 @@ PODS: - Flutter - mapbox_maps_flutter (2.4.0): - Flutter - - MapboxMaps (~> 11.8.0) + - MapboxMaps (= 11.8.0) + - MapboxNavigationCoreUnofficial (= 3.5.3) - Turf (= 3.0.0) - MapboxCommon (24.8.0) - MapboxCoreMaps (11.8.0): - MapboxCommon (~> 24.8) + - MapboxDirectionsUnofficial (3.5.0): + - Turf (= 3.0.0) - MapboxMaps (11.8.0): - MapboxCommon (= 24.8.0) - MapboxCoreMaps (= 11.8.0) - Turf (= 3.0.0) + - MapboxNavigationCoreUnofficial (3.5.3): + - MapboxDirectionsUnofficial (= 3.5.0) + - MapboxMaps (= 11.8.0) + - MapboxNavigationHelpersUnofficial (= 3.5.0) + - MapboxNavigationNative (= 321.0.0) + - MapboxNavigationHelpersUnofficial (3.5.0) + - MapboxNavigationNative (321.0.0): + - MapboxCommon (~> 24.8.0) - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS @@ -31,7 +42,11 @@ SPEC REPOS: trunk: - MapboxCommon - MapboxCoreMaps + - MapboxDirectionsUnofficial - MapboxMaps + - MapboxNavigationCoreUnofficial + - MapboxNavigationHelpersUnofficial + - MapboxNavigationNative - Turf EXTERNAL SOURCES: @@ -49,14 +64,18 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 - mapbox_maps_flutter: 564903a43401bad6b277ffef34b0ab03329a34c1 + mapbox_maps_flutter: 35092c544fbcf2e79372e9f0011286749b4a47d5 MapboxCommon: 95fe03b74d0d0ca39dc646ca14862deb06875151 MapboxCoreMaps: f2a82182c5f6c6262220b81547c6df708012932b + MapboxDirectionsUnofficial: 4244d39727c60672e45800784e121782d55a60ad MapboxMaps: dbe1869006c5918d62efc6b475fb884947ea2ecd + MapboxNavigationCoreUnofficial: 5cacabfe1ea507be11f84162cb7cde7eaf439f9b + MapboxNavigationHelpersUnofficial: 325ef24b1487c336572dad217e35a41be8199eae + MapboxNavigationNative: 3a300f654f9673c6e4cc5f6743997cc3a4c5ceae path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 Turf: a1604e74adce15c58462c9ae2acdbf049d5be35e -PODFILE CHECKSUM: e9395e37b54f3250ebce302f8de7800b2ba2b828 +PODFILE CHECKSUM: b3192822a93c0d8045ea3b93674ed183e8ddffc6 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 29d453e22..0e572f432 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -236,6 +236,8 @@ Base, ); mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; @@ -551,7 +553,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -632,7 +634,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -681,7 +683,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index 93972b79d..bef02a4ac 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -2,6 +2,8 @@ + CADisableMinimumFrameDurationOnPhone + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable @@ -22,6 +24,18 @@ $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + MBXAccessToken + YOUR_ACCESS_TOKEN + NSLocationAlwaysAndWhenInUseUsageDescription + Always and when in use! + NSLocationAlwaysUsageDescription + Can I haz location always? + NSLocationUsageDescription + Older devices need location. + NSLocationWhenInUseUsageDescription + Need location when in use + UIApplicationSupportsIndirectInputEvents + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -41,18 +55,5 @@ UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone - - - NSLocationWhenInUseUsageDescription - Need location when in use - NSLocationAlwaysAndWhenInUseUsageDescription - Always and when in use! - NSLocationUsageDescription - Older devices need location. - NSLocationAlwaysUsageDescription - Can I haz location always? - UIApplicationSupportsIndirectInputEvents - diff --git a/example/lib/main.dart b/example/lib/main.dart index 91af025f7..81b5043d8 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -4,6 +4,7 @@ import 'package:mapbox_maps_example/animation_example.dart'; import 'package:mapbox_maps_example/camera_example.dart'; import 'package:mapbox_maps_example/circle_annotations_example.dart'; import 'package:mapbox_maps_example/cluster_example.dart'; +import 'package:mapbox_maps_example/navigator_example.dart'; import 'package:mapbox_maps_example/offline_map_example.dart'; import 'package:mapbox_maps_example/model_layer_example.dart'; import 'package:mapbox_maps_example/ornaments_example.dart'; @@ -52,6 +53,7 @@ final List _allPages = [ GesturesExample(), OrnamentsExample(), AnimatedRouteExample(), + NavigatorExample() ]; class MapsDemo extends StatelessWidget { @@ -62,7 +64,7 @@ class MapsDemo extends StatelessWidget { // // Alternatively you can replace `String.fromEnvironment("ACCESS_TOKEN")` // in the following line with your access token directly. - static const String ACCESS_TOKEN = String.fromEnvironment("ACCESS_TOKEN"); + static const String ACCESS_TOKEN = String.fromEnvironment("ACCESS_TOKEN"); void _pushPage(BuildContext context, Example page) async { Navigator.of(context).push(MaterialPageRoute( diff --git a/example/lib/navigator_example.dart b/example/lib/navigator_example.dart new file mode 100644 index 000000000..5728a84c3 --- /dev/null +++ b/example/lib/navigator_example.dart @@ -0,0 +1,189 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart'; +import 'package:mapbox_maps_example/utils.dart'; +import 'package:permission_handler/permission_handler.dart'; + +import 'example.dart'; + +class NavigatorExample extends StatefulWidget implements Example { + @override + final Widget leading = const Icon(Icons.map); + @override + final String title = 'Navigation Component'; + @override + final String? subtitle = null; + + @override + State createState() => NavigatorExampleState(); +} + +class NavigatorExampleState extends State + with TickerProviderStateMixin { + NavigatorExampleState(); + + late MapboxMap mapboxMap; + + NavigationCameraState navigationCameraState = NavigationCameraState.IDLE; + + Timer? timer; + Animation? animation; + AnimationController? controller; + + bool styleLoaded = false; + + bool firstLocationUpdateReceived = false; + + _onMapCreated(MapboxMap mapboxMap) { + this.mapboxMap = mapboxMap; + } + + _onStyleLoadedCallback(StyleLoadedEventData data) async { + print("Style loaded"); + styleLoaded = true; + //await mapboxMap.navigation.startTripSession(true); + print("Trip navigation started"); + await _start(); + print("Trip started"); + } + + _onNavigationRouteReadyListener() async { + print("Navigation route ready"); + } + + _onNavigationRouteFailedListener() async { + print("Navigation route failed"); + } + + _onNewLocationListener(NavigationLocation location) async { + print("Location updated: (${location.latitude},${location.longitude})"); + + if (firstLocationUpdateReceived) { + if (navigationCameraState == NavigationCameraState.FOLLOWING) { + // RouteProgress observer might cause a camera issue, that is why we mgiht have to correct camera our own + await updateCamera(location); + } + + return; + } + + firstLocationUpdateReceived = true; + + if (!styleLoaded) { + return; + } + + await _start(); + } + + _onFollowingClicked() async { + await mapboxMap.navigation.requestNavigationCameraToFollowing(); + } + + _onOverviewClicked() async { + await mapboxMap.navigation.requestNavigationCameraToOverview(); + } + + _onNavigationCameraStateListener(NavigationCameraState state) { + print("Navigation camera state: $state"); + navigationCameraState = state; + } + + Future updateCamera(NavigationLocation location) async { + await mapboxMap.easeTo( + CameraOptions( + center: Point( + coordinates: Position(location.longitude!, location.latitude!)), + zoom: 17, + pitch: 45, + padding: MbxEdgeInsets(top: 300.0, left: 0, bottom: 0, right: 0), + bearing: location.bearing), + MapAnimationOptions(duration: 100)); + } + + Future _start() async { + await Permission.location.request(); + print("Permissions requested"); + + //final ByteData bytes = await rootBundle.load('assets/puck_icon.png'); + //final Uint8List list = bytes.buffer.asUint8List(); + // await mapboxMap.location.updateSettings(LocationComponentSettings( + // enabled: true, + // puckBearingEnabled: true)); + + print("Puck enabled"); + //var myCoordinate = await mapboxMap.style.getPuckPosition(); + //if (myCoordinate == null) { + print("Puck location was not defined"); + var lastLocation = await mapboxMap.navigation.lastLocation(); + if (lastLocation == null) { + print("Current location is not defined"); + return; + } + + var myCoordinate = + Position(lastLocation.longitude!, lastLocation.latitude!); + //} + + await mapboxMap + .setCamera(CameraOptions(center: Point(coordinates: myCoordinate))); + print("Camera centered to the current user location"); + + final destinationCoordinate = createRandomPositionAround(myCoordinate); + + await mapboxMap.navigation.setRoute(RouteOptions(waypoints: [ + Waypoint(point: Point(coordinates: myCoordinate)), + Waypoint(point: Point(coordinates: destinationCoordinate)) + ])); + + await mapboxMap.navigation.startTripSession(true); + } + + @override + Widget build(BuildContext context) { + final MapWidget mapWidget = MapWidget( + key: ValueKey("mapWidget"), + styleUri: MapboxStyles.MAPBOX_STREETS, + onMapCreated: _onMapCreated, + onStyleLoadedListener: _onStyleLoadedCallback, + onNewLocationListener: _onNewLocationListener, + onNavigationRouteRenderedListener: _onNavigationRouteReadyListener, + onNavigationRouteFailedListener: _onNavigationRouteFailedListener, + onNavigationCameraStateListener: _onNavigationCameraStateListener, + onRouteProgressListener: (routeProgress) => + {print("Distance remaining: ${routeProgress.distanceRemaining}")}, + ); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Stack(children: [ + Center( + child: SizedBox( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height - 180, + child: mapWidget), + ), + Padding( + padding: const EdgeInsets.all(8), + child: FloatingActionButton( + elevation: 4, + heroTag: "following", + onPressed: _onFollowingClicked, + child: const Icon(Icons.mode_standby), + )), + Padding( + padding: const EdgeInsets.fromLTRB(72, 8, 8, 8), + child: FloatingActionButton( + elevation: 5, + heroTag: "overview", + onPressed: _onOverviewClicked, + child: const Icon(Icons.route), + )), + ]), + ], + ); + } +} diff --git a/example/lib/utils.dart b/example/lib/utils.dart index 8ce147a31..53b4946ad 100644 --- a/example/lib/utils.dart +++ b/example/lib/utils.dart @@ -99,6 +99,18 @@ extension PuckPosition on StyleManager { } } +extension PuckBearing on StyleManager { + Future getPuckBearing() async { + Layer? layer; + if (Platform.isAndroid) { + layer = await getLayer("mapbox-location-indicator-layer"); + } else { + layer = await getLayer("puck"); + } + return (layer as LocationIndicatorLayer).bearing; + } +} + extension PolylineCreation on PolylineAnnotationManager { addAnnotation(List coordinates) { return PolylineAnnotationOptions( diff --git a/example/pubspec.lock b/example/pubspec.lock index ed05e304d..d7c51d6c3 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" cupertino_icons: dependency: "direct main" description: @@ -174,18 +174,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.7" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.8" leak_tracker_testing: dependency: transitive description: @@ -357,7 +357,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: @@ -370,10 +370,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.0" stream_channel: dependency: transitive description: @@ -386,10 +386,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" sweepline_intersections: dependency: transitive description: @@ -418,10 +418,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.3" turf: dependency: "direct dev" description: @@ -466,18 +466,18 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "14.3.0" webdriver: dependency: transitive description: name: webdriver - sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" + sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.4" xdg_directories: dependency: transitive description: diff --git a/ios/Classes/EnumsMapping.swift b/ios/Classes/EnumsMapping.swift index 9b1382c09..173c91418 100644 --- a/ios/Classes/EnumsMapping.swift +++ b/ios/Classes/EnumsMapping.swift @@ -1,5 +1,6 @@ // This file is generated import MapboxMaps +import MapboxNavigationCore extension MapboxMaps.FillTranslateAnchor { @@ -475,3 +476,25 @@ extension MapboxMaps.Anchor { } } } +extension MapboxNavigationCore.NavigationCameraState { + + init?(_ fltValue: NavigationCameraState?) { + guard let fltValue else { return nil } + + switch fltValue { + case .iDLE: self = .idle + case .fOLLOWING: self = .following + case .oVERVIEW: self = .overview + case .tRANSITIONTOFOLLOWING: self = .following + case .tRANSITIONTOOVERVIEW: self = .overview + } + } + + func toFLTNavigationCameraState() -> NavigationCameraState? { + switch self { + case .idle: return .iDLE + case .following: return .fOLLOWING + case .overview: return .oVERVIEW + } + } +} diff --git a/ios/Classes/Extensions.swift b/ios/Classes/Extensions.swift index 6fe760a8e..ecf9ffb4b 100644 --- a/ios/Classes/Extensions.swift +++ b/ios/Classes/Extensions.swift @@ -11,6 +11,8 @@ extension FlutterError: @retroactive Error { } extension FlutterError: Error { } #endif +import MapboxNavigationCore + // FLT to Mapbox extension [_MapWidgetDebugOptions] { @@ -1094,3 +1096,50 @@ func executeOnMainThread(_ execute: @escaping (T) -> Void) -> (T) -> Void { } } } + +extension CoreLocation.CLLocation { + func toFLTNavigationLocation() -> NavigationLocation { + + let timestamp = Int64(self.timestamp.timeIntervalSince1970) + + return NavigationLocation( + latitude: self.coordinate.latitude, + longitude: self.coordinate.longitude, + timestamp: timestamp, + monotonicTimestamp: timestamp, + altitude: self.altitude, + horizontalAccuracy: self.horizontalAccuracy, + verticalAccuracy: self.verticalAccuracy, + speed: self.speed, + speedAccuracy: self.speedAccuracy, + bearing: nil, + bearingAccuracy: nil, + floor: nil, + source: nil + ) + } +} + +extension MapboxNavigationCore.RouteProgress { + func toFLTRouteProgress() -> RouteProgress { + + return RouteProgress( + navigationRouteJson: nil, + bannerInstructionsJson: nil, + voiceInstructionsJson: nil, + currentState: .uNCERTAIN, + inTunnel: false, + distanceRemaining: self.distanceRemaining, + distanceTraveled: self.distanceTraveled, + durationRemaining: self.durationRemaining, + fractionTraveled: self.fractionTraveled, + remainingWaypoints: Int64(self.remainingWaypoints.count), + upcomingRoadObjects: nil, + stale: nil, + routeAlternativeId: nil, + currentRouteGeometryIndex: nil, + inParkingAisle: nil + ) + } +} + diff --git a/ios/Classes/Generated/NavigationMessager.swift b/ios/Classes/Generated/NavigationMessager.swift new file mode 100644 index 000000000..77580ae0e --- /dev/null +++ b/ios/Classes/Generated/NavigationMessager.swift @@ -0,0 +1,750 @@ +// Autogenerated from Pigeon (v22.6.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +import Foundation +import Turf + +#if os(iOS) + import Flutter +#elseif os(macOS) + import FlutterMacOS +#else + #error("Unsupported platform.") +#endif + +/// Error class for passing custom error details to Dart side. +final class NavigationMessagerError: Error { + let code: String + let message: String? + let details: Any? + + init(code: String, message: String?, details: Any?) { + self.code = code + self.message = message + self.details = details + } + + var localizedDescription: String { + return + "NavigationMessagerError(code: \(code), message: \(message ?? ""), details: \(details ?? "")" + } +} + +private func wrapResult(_ result: Any?) -> [Any?] { + return [result] +} + +private func wrapError(_ error: Any) -> [Any?] { + if let pigeonError = error as? NavigationMessagerError { + return [ + pigeonError.code, + pigeonError.message, + pigeonError.details, + ] + } + if let flutterError = error as? FlutterError { + return [ + flutterError.code, + flutterError.message, + flutterError.details, + ] + } + return [ + "\(error)", + "\(type(of: error))", + "Stacktrace: \(Thread.callStackSymbols)", + ] +} + +private func createConnectionError(withChannelName channelName: String) -> NavigationMessagerError { + return NavigationMessagerError(code: "channel-error", message: "Unable to establish connection on channel: '\(channelName)'.", details: "") +} + +private func isNullish(_ value: Any?) -> Bool { + return value is NSNull || value == nil +} + +private func nilOrValue(_ value: Any?) -> T? { + if value is NSNull { return nil } + return value as! T? +} + +enum RouteProgressState: Int { + case iNITIALIZED = 0 + case tRACKING = 1 + case cOMPLETE = 2 + case oFFROUTE = 3 + case uNCERTAIN = 4 +} + +enum RoadObjectLocationType: Int { + case gANTRY = 0 + case oPENLRLINE = 1 + case oPENLRPOINT = 2 + case pOINT = 3 + case pOLYGON = 4 + case pOLYLINE = 5 + case rOUTEALERT = 6 + case sUBGRAPH = 7 +} + +enum NavigationCameraState: Int { + case iDLE = 0 + case tRANSITIONTOFOLLOWING = 1 + case fOLLOWING = 2 + case tRANSITIONTOOVERVIEW = 3 + case oVERVIEW = 4 +} + +/// Generated class from Pigeon that represents data sent in messages. +struct NavigationLocation { + var latitude: Double? = nil + var longitude: Double? = nil + var timestamp: Int64? = nil + var monotonicTimestamp: Int64? = nil + var altitude: Double? = nil + var horizontalAccuracy: Double? = nil + var verticalAccuracy: Double? = nil + var speed: Double? = nil + var speedAccuracy: Double? = nil + var bearing: Double? = nil + var bearingAccuracy: Double? = nil + var floor: Int64? = nil + var source: String? = nil + + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> NavigationLocation? { + let latitude: Double? = nilOrValue(pigeonVar_list[0]) + let longitude: Double? = nilOrValue(pigeonVar_list[1]) + let timestamp: Int64? = nilOrValue(pigeonVar_list[2]) + let monotonicTimestamp: Int64? = nilOrValue(pigeonVar_list[3]) + let altitude: Double? = nilOrValue(pigeonVar_list[4]) + let horizontalAccuracy: Double? = nilOrValue(pigeonVar_list[5]) + let verticalAccuracy: Double? = nilOrValue(pigeonVar_list[6]) + let speed: Double? = nilOrValue(pigeonVar_list[7]) + let speedAccuracy: Double? = nilOrValue(pigeonVar_list[8]) + let bearing: Double? = nilOrValue(pigeonVar_list[9]) + let bearingAccuracy: Double? = nilOrValue(pigeonVar_list[10]) + let floor: Int64? = nilOrValue(pigeonVar_list[11]) + let source: String? = nilOrValue(pigeonVar_list[12]) + + return NavigationLocation( + latitude: latitude, + longitude: longitude, + timestamp: timestamp, + monotonicTimestamp: monotonicTimestamp, + altitude: altitude, + horizontalAccuracy: horizontalAccuracy, + verticalAccuracy: verticalAccuracy, + speed: speed, + speedAccuracy: speedAccuracy, + bearing: bearing, + bearingAccuracy: bearingAccuracy, + floor: floor, + source: source + ) + } + func toList() -> [Any?] { + return [ + latitude, + longitude, + timestamp, + monotonicTimestamp, + altitude, + horizontalAccuracy, + verticalAccuracy, + speed, + speedAccuracy, + bearing, + bearingAccuracy, + floor, + source, + ] + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct RoadObject { + var id: String? = nil + var objectType: RoadObjectLocationType? = nil + var length: Double? = nil + var provider: String? = nil + var isUrban: Bool? = nil + + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> RoadObject? { + let id: String? = nilOrValue(pigeonVar_list[0]) + let objectType: RoadObjectLocationType? = nilOrValue(pigeonVar_list[1]) + let length: Double? = nilOrValue(pigeonVar_list[2]) + let provider: String? = nilOrValue(pigeonVar_list[3]) + let isUrban: Bool? = nilOrValue(pigeonVar_list[4]) + + return RoadObject( + id: id, + objectType: objectType, + length: length, + provider: provider, + isUrban: isUrban + ) + } + func toList() -> [Any?] { + return [ + id, + objectType, + length, + provider, + isUrban, + ] + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct RoadObjectDistanceInfo { + var distanceToStart: Double? = nil + + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> RoadObjectDistanceInfo? { + let distanceToStart: Double? = nilOrValue(pigeonVar_list[0]) + + return RoadObjectDistanceInfo( + distanceToStart: distanceToStart + ) + } + func toList() -> [Any?] { + return [ + distanceToStart + ] + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct UpcomingRoadObject { + var roadObject: RoadObject? = nil + var distanceToStart: Double? = nil + var distanceInfo: RoadObjectDistanceInfo? = nil + + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> UpcomingRoadObject? { + let roadObject: RoadObject? = nilOrValue(pigeonVar_list[0]) + let distanceToStart: Double? = nilOrValue(pigeonVar_list[1]) + let distanceInfo: RoadObjectDistanceInfo? = nilOrValue(pigeonVar_list[2]) + + return UpcomingRoadObject( + roadObject: roadObject, + distanceToStart: distanceToStart, + distanceInfo: distanceInfo + ) + } + func toList() -> [Any?] { + return [ + roadObject, + distanceToStart, + distanceInfo, + ] + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct RouteProgress { + var navigationRouteJson: String? = nil + var bannerInstructionsJson: String? = nil + var voiceInstructionsJson: String? = nil + var currentState: RouteProgressState? = nil + var inTunnel: Bool? = nil + var distanceRemaining: Double? = nil + var distanceTraveled: Double? = nil + var durationRemaining: Double? = nil + var fractionTraveled: Double? = nil + var remainingWaypoints: Int64? = nil + var upcomingRoadObjects: [UpcomingRoadObject]? = nil + var stale: Bool? = nil + var routeAlternativeId: String? = nil + var currentRouteGeometryIndex: Int64? = nil + var inParkingAisle: Bool? = nil + + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> RouteProgress? { + let navigationRouteJson: String? = nilOrValue(pigeonVar_list[0]) + let bannerInstructionsJson: String? = nilOrValue(pigeonVar_list[1]) + let voiceInstructionsJson: String? = nilOrValue(pigeonVar_list[2]) + let currentState: RouteProgressState? = nilOrValue(pigeonVar_list[3]) + let inTunnel: Bool? = nilOrValue(pigeonVar_list[4]) + let distanceRemaining: Double? = nilOrValue(pigeonVar_list[5]) + let distanceTraveled: Double? = nilOrValue(pigeonVar_list[6]) + let durationRemaining: Double? = nilOrValue(pigeonVar_list[7]) + let fractionTraveled: Double? = nilOrValue(pigeonVar_list[8]) + let remainingWaypoints: Int64? = nilOrValue(pigeonVar_list[9]) + let upcomingRoadObjects: [UpcomingRoadObject]? = nilOrValue(pigeonVar_list[10]) + let stale: Bool? = nilOrValue(pigeonVar_list[11]) + let routeAlternativeId: String? = nilOrValue(pigeonVar_list[12]) + let currentRouteGeometryIndex: Int64? = nilOrValue(pigeonVar_list[13]) + let inParkingAisle: Bool? = nilOrValue(pigeonVar_list[14]) + + return RouteProgress( + navigationRouteJson: navigationRouteJson, + bannerInstructionsJson: bannerInstructionsJson, + voiceInstructionsJson: voiceInstructionsJson, + currentState: currentState, + inTunnel: inTunnel, + distanceRemaining: distanceRemaining, + distanceTraveled: distanceTraveled, + durationRemaining: durationRemaining, + fractionTraveled: fractionTraveled, + remainingWaypoints: remainingWaypoints, + upcomingRoadObjects: upcomingRoadObjects, + stale: stale, + routeAlternativeId: routeAlternativeId, + currentRouteGeometryIndex: currentRouteGeometryIndex, + inParkingAisle: inParkingAisle + ) + } + func toList() -> [Any?] { + return [ + navigationRouteJson, + bannerInstructionsJson, + voiceInstructionsJson, + currentState, + inTunnel, + distanceRemaining, + distanceTraveled, + durationRemaining, + fractionTraveled, + remainingWaypoints, + upcomingRoadObjects, + stale, + routeAlternativeId, + currentRouteGeometryIndex, + inParkingAisle, + ] + } +} + +struct Waypoint { + var point: Point? = nil + var name: String? = nil + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> Waypoint? { + let point: Point? = nilOrValue(pigeonVar_list[0]) + let name: String? = nilOrValue(pigeonVar_list[1]) + + return Waypoint( + point: point, + name: name + ) + } + func toList() -> [Any?] { + return [ + point, + name + ] + } +} + +struct RouteOptions { + var waypoints: [Waypoint]? = nil + var steps: Bool? = nil + var alternatives: Bool? = nil + var coordinates: [Point]? = nil + var voiceInstructions: Bool? = nil + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> RouteOptions? { + let waypoints: [Waypoint]? = nilOrValue(pigeonVar_list[0]) + let steps: Bool? = nilOrValue(pigeonVar_list[1]) + let alternatives: Bool? = nilOrValue(pigeonVar_list[2]) + let coordinates: [Point]? = nilOrValue(pigeonVar_list[3]) + let voiceInstructions: Bool? = nilOrValue(pigeonVar_list[4]) + + return RouteOptions( + waypoints: waypoints, + steps: steps, + alternatives:alternatives, + coordinates:coordinates, + voiceInstructions:voiceInstructions + ) + } + func toList() -> [Any?] { + return [ + waypoints, + steps, + alternatives, + coordinates, + voiceInstructions + ] + } +} + + +private class NavigationMessagerPigeonCodecReader: FlutterStandardReader { + override func readValue(ofType type: UInt8) -> Any? { + switch type { + case 151: + return Point.fromList(self.readValue() as! [Any?]) + case 152: + return Feature.fromList(self.readValue() as! [Any?]) + case 191: + return NavigationLocation.fromList(self.readValue() as! [Any?]) + case 192: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return RouteProgressState(rawValue: enumResultAsInt) + } + return nil + case 193: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return RoadObjectLocationType(rawValue: enumResultAsInt) + } + return nil + case 194: + return RoadObject.fromList(self.readValue() as! [Any?]) + case 195: + return RoadObjectDistanceInfo.fromList(self.readValue() as! [Any?]) + case 196: + return UpcomingRoadObject.fromList(self.readValue() as! [Any?]) + case 197: + return RouteProgress.fromList(self.readValue() as! [Any?]) + case 198: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return NavigationCameraState(rawValue: enumResultAsInt) + } + return nil + case 199: + return Waypoint.fromList(self.readValue() as! [Any?]) + case 200: + return RouteOptions.fromList(self.readValue() as! [Any?]) + default: + return super.readValue(ofType: type) + } + } +} + +private class NavigationMessagerPigeonCodecWriter: FlutterStandardWriter { + override func writeValue(_ value: Any) { + if let value = value as? Point { + super.writeByte(151) + super.writeValue(value.toList()) + } else if let value = value as? Feature { + super.writeByte(152) + super.writeValue(value.toList()) + } else if let value = value as? NavigationLocation { + super.writeByte(191) + super.writeValue(value.toList()) + } else if let value = value as? RouteProgressState { + super.writeByte(192) + super.writeValue(value.rawValue) + } else if let value = value as? RoadObjectLocationType { + super.writeByte(193) + super.writeValue(value.rawValue) + } else if let value = value as? RoadObject { + super.writeByte(194) + super.writeValue(value.toList()) + } else if let value = value as? RoadObjectDistanceInfo { + super.writeByte(195) + super.writeValue(value.toList()) + } else if let value = value as? UpcomingRoadObject { + super.writeByte(196) + super.writeValue(value.toList()) + } else if let value = value as? RouteProgress { + super.writeByte(197) + super.writeValue(value.toList()) + } else if let value = value as? NavigationCameraState { + super.writeByte(198) + super.writeValue(value.rawValue) + } else if let value = value as? Waypoint { + super.writeByte(199) + super.writeValue(value.toList()) + } else if let value = value as? RouteOptions { + super.writeByte(200) + super.writeValue(value.toList()) + } else { + super.writeValue(value) + } + } +} + +private class NavigationMessagerPigeonCodecReaderWriter: FlutterStandardReaderWriter { + override func reader(with data: Data) -> FlutterStandardReader { + return NavigationMessagerPigeonCodecReader(data: data) + } + + override func writer(with data: NSMutableData) -> FlutterStandardWriter { + return NavigationMessagerPigeonCodecWriter(data: data) + } +} + +class NavigationMessagerPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { + static let shared = NavigationMessagerPigeonCodec(readerWriter: NavigationMessagerPigeonCodecReaderWriter()) +} + + +/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift. +protocol NavigationListenerProtocol { + func onNavigationRouteReady(completion: @escaping (Result) -> Void) + func onNavigationRouteFailed(completion: @escaping (Result) -> Void) + func onNavigationRouteCancelled(completion: @escaping (Result) -> Void) + func onNavigationRouteRendered(completion: @escaping (Result) -> Void) + func onNewLocation(location locationArg: NavigationLocation, completion: @escaping (Result) -> Void) + func onRouteProgress(routeProgress routeProgressArg: RouteProgress, completion: @escaping (Result) -> Void) + func onNavigationCameraStateChanged(state stateArg: NavigationCameraState, completion: @escaping (Result) -> Void) +} +class NavigationListener: NavigationListenerProtocol { + private let binaryMessenger: FlutterBinaryMessenger + private let messageChannelSuffix: String + init(binaryMessenger: FlutterBinaryMessenger, messageChannelSuffix: String = "") { + self.binaryMessenger = binaryMessenger + self.messageChannelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + } + var codec: NavigationMessagerPigeonCodec { + return NavigationMessagerPigeonCodec.shared + } + func onNavigationRouteReady(completion: @escaping (Result) -> Void) { + let channelName: String = "dev.flutter.pigeon.mapbox_maps_flutter.NavigationListener.onNavigationRouteReady\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage(nil) { response in + guard let listResponse = response as? [Any?] else { + completion(.failure(createConnectionError(withChannelName: channelName))) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + completion(.failure(NavigationMessagerError(code: code, message: message, details: details))) + } else { + completion(.success(Void())) + } + } + } + func onNavigationRouteFailed(completion: @escaping (Result) -> Void) { + let channelName: String = "dev.flutter.pigeon.mapbox_maps_flutter.NavigationListener.onNavigationRouteFailed\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage(nil) { response in + guard let listResponse = response as? [Any?] else { + completion(.failure(createConnectionError(withChannelName: channelName))) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + completion(.failure(NavigationMessagerError(code: code, message: message, details: details))) + } else { + completion(.success(Void())) + } + } + } + func onNavigationRouteCancelled(completion: @escaping (Result) -> Void) { + let channelName: String = "dev.flutter.pigeon.mapbox_maps_flutter.NavigationListener.onNavigationRouteCancelled\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage(nil) { response in + guard let listResponse = response as? [Any?] else { + completion(.failure(createConnectionError(withChannelName: channelName))) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + completion(.failure(NavigationMessagerError(code: code, message: message, details: details))) + } else { + completion(.success(Void())) + } + } + } + func onNavigationRouteRendered(completion: @escaping (Result) -> Void) { + let channelName: String = "dev.flutter.pigeon.mapbox_maps_flutter.NavigationListener.onNavigationRouteRendered\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage(nil) { response in + guard let listResponse = response as? [Any?] else { + completion(.failure(createConnectionError(withChannelName: channelName))) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + completion(.failure(NavigationMessagerError(code: code, message: message, details: details))) + } else { + completion(.success(Void())) + } + } + } + func onNewLocation(location locationArg: NavigationLocation, completion: @escaping (Result) -> Void) { + let channelName: String = "dev.flutter.pigeon.mapbox_maps_flutter.NavigationListener.onNewLocation\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage([locationArg] as [Any?]) { response in + guard let listResponse = response as? [Any?] else { + completion(.failure(createConnectionError(withChannelName: channelName))) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + completion(.failure(NavigationMessagerError(code: code, message: message, details: details))) + } else { + completion(.success(Void())) + } + } + } + func onRouteProgress(routeProgress routeProgressArg: RouteProgress, completion: @escaping (Result) -> Void) { + let channelName: String = "dev.flutter.pigeon.mapbox_maps_flutter.NavigationListener.onRouteProgress\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage([routeProgressArg] as [Any?]) { response in + guard let listResponse = response as? [Any?] else { + completion(.failure(createConnectionError(withChannelName: channelName))) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + completion(.failure(NavigationMessagerError(code: code, message: message, details: details))) + } else { + completion(.success(Void())) + } + } + } + func onNavigationCameraStateChanged(state stateArg: NavigationCameraState, completion: @escaping (Result) -> Void) { + let channelName: String = "dev.flutter.pigeon.mapbox_maps_flutter.NavigationListener.onNavigationCameraStateChanged\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage([stateArg] as [Any?]) { response in + guard let listResponse = response as? [Any?] else { + completion(.failure(createConnectionError(withChannelName: channelName))) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + completion(.failure(NavigationMessagerError(code: code, message: message, details: details))) + } else { + completion(.success(Void())) + } + } + } +} +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol NavigationInterface { + func setRoute(options: RouteOptions, completion: @escaping (Result) -> Void) + func stopTripSession(completion: @escaping (Result) -> Void) + func startTripSession(withForegroundService: Bool, completion: @escaping (Result) -> Void) + func requestNavigationCameraToFollowing(completion: @escaping (Result) -> Void) + func requestNavigationCameraToOverview(completion: @escaping (Result) -> Void) + func lastLocation(completion: @escaping (Result) -> Void) +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class NavigationInterfaceSetup { + static var codec: FlutterStandardMessageCodec { NavigationMessagerPigeonCodec.shared } + /// Sets up an instance of `NavigationInterface` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: NavigationInterface?, messageChannelSuffix: String = "") { + let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + let setRouteChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.mapbox_maps_flutter.NavigationInterface.setRoute\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setRouteChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let routeOptionsArg = args[0] as! RouteOptions + api.setRoute(options: routeOptionsArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setRouteChannel.setMessageHandler(nil) + } + let stopTripSessionChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.mapbox_maps_flutter.NavigationInterface.stopTripSession\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + stopTripSessionChannel.setMessageHandler { _, reply in + api.stopTripSession { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + stopTripSessionChannel.setMessageHandler(nil) + } + let startTripSessionChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.mapbox_maps_flutter.NavigationInterface.startTripSession\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + startTripSessionChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let withForegroundServiceArg = args[0] as! Bool + api.startTripSession(withForegroundService: withForegroundServiceArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + startTripSessionChannel.setMessageHandler(nil) + } + let requestNavigationCameraToFollowingChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.mapbox_maps_flutter.NavigationInterface.requestNavigationCameraToFollowing\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + requestNavigationCameraToFollowingChannel.setMessageHandler { _, reply in + api.requestNavigationCameraToFollowing { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + requestNavigationCameraToFollowingChannel.setMessageHandler(nil) + } + let requestNavigationCameraToOverviewChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.mapbox_maps_flutter.NavigationInterface.requestNavigationCameraToOverview\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + requestNavigationCameraToOverviewChannel.setMessageHandler { _, reply in + api.requestNavigationCameraToOverview { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + requestNavigationCameraToOverviewChannel.setMessageHandler(nil) + } + let lastLocationChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.mapbox_maps_flutter.NavigationInterface.lastLocation\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + lastLocationChannel.setMessageHandler { _, reply in + api.lastLocation { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + lastLocationChannel.setMessageHandler(nil) + } + } +} diff --git a/ios/Classes/Generated/Settings.swift b/ios/Classes/Generated/Settings.swift index fb0357238..8c704eb05 100644 --- a/ios/Classes/Generated/Settings.swift +++ b/ios/Classes/Generated/Settings.swift @@ -1180,4 +1180,4 @@ class LogoSettingsInterfaceSetup { updateSettingsChannel.setMessageHandler(nil) } } -} +} \ No newline at end of file diff --git a/ios/Classes/MapboxMapController.swift b/ios/Classes/MapboxMapController.swift index f5e88d8ca..cc160a01c 100644 --- a/ios/Classes/MapboxMapController.swift +++ b/ios/Classes/MapboxMapController.swift @@ -1,23 +1,29 @@ import Flutter @_spi(Experimental) import MapboxMaps import UIKit +import MapboxNavigationCore struct SuffixBinaryMessenger { let messenger: FlutterBinaryMessenger let suffix: String } +@MainActor final class MapboxMapController: NSObject, FlutterPlatformView { private let mapView: MapView private let mapboxMap: MapboxMap private let channel: FlutterMethodChannel private let annotationController: AnnotationController? private let gesturesController: GesturesController? + private let navigationController: NavigationController? private let eventHandler: MapboxEventHandler private let binaryMessenger: SuffixBinaryMessenger + private static var navigationProvider: MapboxNavigationProvider? + + private var navigationMapView: NavigationMapView! func view() -> UIView { - return mapView + return navigationMapView } init( @@ -31,8 +37,37 @@ final class MapboxMapController: NSObject, FlutterPlatformView { binaryMessenger = SuffixBinaryMessenger(messenger: registrar.messenger(), suffix: String(channelSuffix)) _ = SettingsServiceFactory.getInstanceFor(.nonPersistent) .set(key: "com.mapbox.common.telemetry.internal.custom_user_agent_fragment", value: "FlutterPlugin/\(pluginVersion)") + + if(MapboxMapController.navigationProvider == nil) + { + MapboxMapController.navigationProvider = MapboxNavigationProvider(coreConfig: CoreConfig( + credentials: .init(navigation: ApiConfiguration(accessToken: MapboxOptions.accessToken), + map: ApiConfiguration(accessToken: MapboxOptions.accessToken)), // You can pass a custom token if you need to, + locationSource: .live, + disableBackgroundTrackingLocation: false + )) + } + + let mapboxNavigation = MapboxMapController.navigationProvider!.mapboxNavigation + let navigationMapView = NavigationMapView( + location: mapboxNavigation.navigation() + .locationMatching.map(\.enhancedLocation) + .eraseToAnyPublisher(), + routeProgress: mapboxNavigation.navigation() + .routeProgress.map(\.?.routeProgress) + .eraseToAnyPublisher(), + predictiveCacheManager: MapboxMapController.navigationProvider!.predictiveCacheManager, + frame: frame, + mapInitOptions: mapInitOptions + ) + + navigationMapView.viewportPadding = UIEdgeInsets(top: 20, left: 20, bottom: 80, right: 20) + navigationMapView.puckType = .puck2D(.navigationDefault) + navigationMapView.translatesAutoresizingMaskIntoConstraints = true - mapView = MapView(frame: frame, mapInitOptions: mapInitOptions) + self.navigationMapView = navigationMapView + + mapView = self.navigationMapView.mapView mapboxMap = mapView.mapboxMap channel = FlutterMethodChannel( @@ -82,6 +117,9 @@ final class MapboxMapController: NSObject, FlutterPlatformView { annotationController = AnnotationController(withMapView: mapView) annotationController!.setup(binaryMessenger: binaryMessenger) + navigationController = NavigationController(withMapView: navigationMapView, navigationProvider: mapboxNavigation) + NavigationInterfaceSetup.setUp(binaryMessenger: binaryMessenger.messenger, api: navigationController, messageChannelSuffix: binaryMessenger.suffix) + super.init() channel.setMethodCallHandler { [weak self] in self?.onMethodCall(methodCall: $0, result: $1) } @@ -109,6 +147,12 @@ final class MapboxMapController: NSObject, FlutterPlatformView { } catch { result(FlutterError(code: "2342345", message: error.localizedDescription, details: nil)) } + case "navigation#add_listeners": + navigationController!.addListeners(messenger: binaryMessenger) + result(nil) + case "navigation#remove_listeners": + navigationController!.removeListeners() + result(nil) default: result(FlutterMethodNotImplemented) } @@ -128,5 +172,6 @@ final class MapboxMapController: NSObject, FlutterPlatformView { CompassSettingsInterfaceSetup.setUp(binaryMessenger: binaryMessenger.messenger, api: nil, messageChannelSuffix: binaryMessenger.suffix) ScaleBarSettingsInterfaceSetup.setUp(binaryMessenger: binaryMessenger.messenger, api: nil, messageChannelSuffix: binaryMessenger.suffix) annotationController?.tearDown(messenger: binaryMessenger) + NavigationInterfaceSetup.setUp(binaryMessenger: binaryMessenger.messenger, api: nil, messageChannelSuffix: binaryMessenger.suffix) } } diff --git a/ios/Classes/MapboxMapFactory.swift b/ios/Classes/MapboxMapFactory.swift index 2eacebc6c..11223b607 100644 --- a/ios/Classes/MapboxMapFactory.swift +++ b/ios/Classes/MapboxMapFactory.swift @@ -3,6 +3,7 @@ import MapboxMaps import MapboxCommon import MapboxCommon_Private +@MainActor final class MapboxMapFactory: NSObject, FlutterPlatformViewFactory { private static let mapCounter = FeatureTelemetryCounter.create(forName: "maps-mobile/flutter/map") diff --git a/ios/Classes/MapboxMapsPlugin.swift b/ios/Classes/MapboxMapsPlugin.swift index 42b27b5e0..9e07f70ef 100644 --- a/ios/Classes/MapboxMapsPlugin.swift +++ b/ios/Classes/MapboxMapsPlugin.swift @@ -2,6 +2,7 @@ import Flutter import UIKit import MapboxMaps +@MainActor public class MapboxMapsPlugin: NSObject, FlutterPlugin { public static func register(with registrar: FlutterPluginRegistrar) { let instance = MapboxMapFactory(withRegistrar: registrar) diff --git a/ios/Classes/NavigationController.swift b/ios/Classes/NavigationController.swift new file mode 100644 index 000000000..512bd75c0 --- /dev/null +++ b/ios/Classes/NavigationController.swift @@ -0,0 +1,177 @@ +import Combine +import CoreLocation +import MapboxDirections +import MapboxNavigationCore +import MapboxMaps +import _MapboxNavigationHelpers + +@MainActor +final class NavigationController: NSObject, NavigationInterface { + + @Published private(set) var isInActiveNavigation: Bool = false + @Published private(set) var routes: MapboxNavigationCore.NavigationRoutes? + @Published private(set) var routeProgress: MapboxNavigationCore.RouteProgress? + @Published private(set) var currentLocation: CLLocation? + @Published var cameraState: MapboxNavigationCore.NavigationCameraState = .idle + @Published var profileIdentifier: ProfileIdentifier = .automobileAvoidingTraffic + @Published var shouldRequestMapMatching = false + + private var waypoints: [MapboxNavigationCore.Waypoint] = [] + private let core: MapboxNavigation + + private var cancelables: Set = [] + private var onNavigationListener: NavigationListener? + + private let navigationMapView: NavigationMapView + + init(withMapView: NavigationMapView, navigationProvider: MapboxNavigation) { + self.navigationMapView = withMapView + self.core = navigationProvider + + super.init() + observeMap() + } + + func observeMap(){ + self.navigationMapView.navigationCamera.cameraStates.sink { state in + self.onNavigationListener?.onNavigationCameraStateChanged(state: state.toFLTNavigationCameraState()!) { _ in } + } + core.navigation().locationMatching.sink { locationMatching in + self.onNavigationListener?.onNewLocation(location: locationMatching.enhancedLocation.toFLTNavigationLocation()) { _ in } + } + core.navigation().routeProgress.sink { routeProgress in + if(routeProgress == nil){ return } + self.onNavigationListener?.onRouteProgress(routeProgress: routeProgress!.routeProgress.toFLTRouteProgress()) { _ in } + } + } + + func startFreeDrive() { + core.tripSession().startFreeDrive() + } + + func cancelPreview() { + waypoints = [] + routes = nil + self.navigationMapView.removeRoutes() + } + + func startActiveNavigation() { + guard let previewRoutes = routes else { return } + core.tripSession().startActiveGuidance(with: previewRoutes, startLegIndex: 0) + } + + func stopActiveNavigation() { + core.tripSession().startFreeDrive() + self.navigationMapView.navigationCamera.stop() + } + + func requestRoutes(points: [Waypoint]) async throws { + + self.waypoints = points.map { + MapboxNavigationCore.Waypoint( + coordinate: LocationCoordinate2D(latitude: $0.point!.coordinates.latitude, longitude: $0.point!.coordinates.longitude)) + } + + let provider = core.routingProvider() + if shouldRequestMapMatching { + let mapMatchingOptions = NavigationMatchOptions( + waypoints: waypoints, + profileIdentifier: profileIdentifier + ) + let previewRoutes = try await provider.calculateRoutes(options: mapMatchingOptions).value + routes = previewRoutes + self.onNavigationListener?.onNavigationRouteReady() { _ in } + } else { + let routeOptions = NavigationRouteOptions( + waypoints: waypoints, + profileIdentifier: profileIdentifier + ) + let previewRoutes = try await provider.calculateRoutes(options: routeOptions).value + routes = previewRoutes + self.onNavigationListener?.onNavigationRouteReady() { _ in } + } + self.navigationMapView.showcase(routes!) + } + + func addListeners(messenger: SuffixBinaryMessenger) { + removeListeners() + onNavigationListener = NavigationListener(binaryMessenger: messenger.messenger, messageChannelSuffix: messenger.suffix) + } + + func removeListeners() { + cancelables = [] + } + + func setRoute(options: RouteOptions, completion: @escaping (Result) -> Void) { + Task { + do { + try await self.requestRoutes(points: options.waypoints!) + completion(.success(Void())) + } + catch { + completion(.failure(error)) + } + } + } + + func stopTripSession(completion: @escaping (Result) -> Void) { + + stopActiveNavigation() + completion(.success(Void())) + } + + func startTripSession(withForegroundService: Bool, completion: @escaping (Result) -> Void) { + guard let previewRoutes = routes else { return } + core.tripSession().startActiveGuidance(with: previewRoutes, startLegIndex: 0) + routes = nil + waypoints = [] + completion(.success(Void())) + } + + func requestNavigationCameraToFollowing(completion: @escaping (Result) -> Void) { + navigationMapView.update(navigationCameraState: .following) + completion(.success(Void())) + } + + func requestNavigationCameraToOverview(completion: @escaping (Result) -> Void) { + navigationMapView.update(navigationCameraState: .overview) + completion(.success(Void())) + } + + func lastLocation(completion: @escaping (Result) -> Void) { + var mapView = self.navigationMapView.mapView + if(self.currentLocation != nil) + { + completion(.success(self.currentLocation!.toFLTNavigationLocation())) + } + else if(mapView.location.latestLocation != nil) + { + let timestamp = Int64(mapView.location.latestLocation!.timestamp.timeIntervalSince1970) + + completion(.success(NavigationLocation( + latitude: mapView.location.latestLocation!.coordinate.latitude, + longitude: mapView.location.latestLocation!.coordinate.longitude, + timestamp: timestamp, + monotonicTimestamp: timestamp, + altitude: mapView.location.latestLocation!.altitude, + horizontalAccuracy: mapView.location.latestLocation!.horizontalAccuracy, + verticalAccuracy: mapView.location.latestLocation!.verticalAccuracy, + speed: mapView.location.latestLocation!.speed, + speedAccuracy: mapView.location.latestLocation!.speedAccuracy, + bearing: nil, + bearingAccuracy: nil, + floor: nil, + source: nil + ))) + } + else { + var locationManager = NavigationLocationManager() + if(locationManager.location != nil){ + completion(.success(locationManager.location!.toFLTNavigationLocation())) + } + else{ + completion(.success(nil)) + } + } + } +} diff --git a/ios/mapbox_maps_flutter.podspec b/ios/mapbox_maps_flutter.podspec index 25f6536b3..d92736092 100644 --- a/ios/mapbox_maps_flutter.podspec +++ b/ios/mapbox_maps_flutter.podspec @@ -2,6 +2,11 @@ # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. # Run `pod lib lint mapbox_maps_flutter.podspec` to validate before publishing. # +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint mapbox_maps_flutter.podspec` to validate before publishing. +# + Pod::Spec.new do |s| s.name = 'mapbox_maps_flutter' s.version = '2.4.0' @@ -15,12 +20,14 @@ Pod::Spec.new do |s| s.source_files = 'Classes/**/*' s.dependency 'Flutter' - s.platform = :ios, '12.0' - - s.dependency 'MapboxMaps', '~> 11.8.0' - s.dependency 'Turf', '3.0.0' + s.platform = :ios, '14.0' # Flutter.framework does not contain a i386 slice. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } s.swift_version = '5.8' + + s.dependency 'MapboxMaps', '11.8.0' + s.dependency 'Turf', '3.0.0' + + s.dependency 'MapboxNavigationCoreUnofficial', '3.5.3' end diff --git a/lib/mapbox_maps_flutter.dart b/lib/mapbox_maps_flutter.dart index edc84b644..fe1b7b936 100644 --- a/lib/mapbox_maps_flutter.dart +++ b/lib/mapbox_maps_flutter.dart @@ -32,6 +32,7 @@ part 'src/pigeons/polyline_annotation_messenger.dart'; part 'src/pigeons/map_interfaces.dart'; part 'src/pigeons/settings.dart'; part 'src/pigeons/gesture_listeners.dart'; +part 'src/pigeons/navigation.dart'; part 'src/snapshotter/snapshotter_messenger.dart'; part 'src/pigeons/log_backend.dart'; part 'src/style/layer/background_layer.dart'; @@ -50,6 +51,8 @@ part 'src/style/layer/slot_layer.dart'; part 'src/style/layer/raster_particle_layer.dart'; part 'src/style/layer/clip_layer.dart'; part 'src/style/mapbox_styles.dart'; +part 'src/style/directions_criteria.dart'; +part 'src/style/navigation_styles.dart'; part 'src/style/source/geojson_source.dart'; part 'src/style/source/image_source.dart'; part 'src/style/source/raster_source.dart'; diff --git a/lib/src/annotation/annotation_manager.dart b/lib/src/annotation/annotation_manager.dart index ebe2527d7..da97317fa 100644 --- a/lib/src/annotation/annotation_manager.dart +++ b/lib/src/annotation/annotation_manager.dart @@ -76,7 +76,8 @@ class AnnotationManager { /// The super class for all AnnotationManagers. class BaseAnnotationManager { BaseAnnotationManager._( - {required String id, required BinaryMessenger messenger}) + {required String id, + required BinaryMessenger messenger}) : this.id = id, _messenger = messenger; final String id; diff --git a/lib/src/callbacks.dart b/lib/src/callbacks.dart index c01a9b337..d6bb9559a 100644 --- a/lib/src/callbacks.dart +++ b/lib/src/callbacks.dart @@ -72,3 +72,11 @@ typedef void OnTileRegionLoadProgressListener(TileRegionLoadProgress progress); // TileRegionEstimate load progress callback. typedef void OnTileRegionEstimateProgressListenter( TileRegionEstimateProgress progress); + +typedef void OnNavigationRouteListener(); + +typedef void OnNewLocationListener(NavigationLocation location); + +typedef void OnRouteProgressListener(RouteProgress routeProgress); + +typedef void OnNavigationCameraStateListener(NavigationCameraState state); diff --git a/lib/src/location_settings.dart b/lib/src/location_settings.dart index 640dd4b5a..66d240055 100644 --- a/lib/src/location_settings.dart +++ b/lib/src/location_settings.dart @@ -26,7 +26,7 @@ class LocationSettings { await MapboxMapsOptions._getFlutterAssetPath( settings.locationPuck?.locationPuck3D?.modelUri); } - _api.updateSettings(settings, + await _api.updateSettings(settings, settings.locationPuck?.locationPuck2D is DefaultLocationPuck2D); } } diff --git a/lib/src/map_widget.dart b/lib/src/map_widget.dart index 9aaece9ca..586c08d9a 100644 --- a/lib/src/map_widget.dart +++ b/lib/src/map_widget.dart @@ -41,35 +41,42 @@ enum AndroidPlatformViewHostingMode { /// Warning: Please note that you are responsible for getting permission to use the map data, /// and for ensuring your use adheres to the relevant terms of use. class MapWidget extends StatefulWidget { - MapWidget({ - Key? key, - this.mapOptions, - this.cameraOptions, - // FIXME Flutter 3.x has memory leak on Android using in SurfaceView mode, see https://github.com/flutter/flutter/issues/118384 - // As a workaround default is true. - this.textureView = true, - this.androidHostingMode = AndroidPlatformViewHostingMode.VD, - this.styleUri = MapboxStyles.STANDARD, - this.gestureRecognizers, - this.onMapCreated, - this.onStyleLoadedListener, - this.onCameraChangeListener, - this.onMapIdleListener, - this.onMapLoadedListener, - this.onMapLoadErrorListener, - this.onRenderFrameStartedListener, - this.onRenderFrameFinishedListener, - this.onSourceAddedListener, - this.onSourceDataLoadedListener, - this.onSourceRemovedListener, - this.onStyleDataLoadedListener, - this.onStyleImageMissingListener, - this.onStyleImageUnusedListener, - this.onResourceRequestListener, - this.onTapListener, - this.onLongTapListener, - this.onScrollListener, - }) : super(key: key) { + MapWidget( + {Key? key, + this.mapOptions, + this.cameraOptions, + // FIXME Flutter 3.x has memory leak on Android using in SurfaceView mode, see https://github.com/flutter/flutter/issues/118384 + // As a workaround default is true. + this.textureView = true, + this.androidHostingMode = AndroidPlatformViewHostingMode.HC, + this.styleUri = MapboxStyles.STANDARD, + this.gestureRecognizers, + this.onMapCreated, + this.onStyleLoadedListener, + this.onCameraChangeListener, + this.onMapIdleListener, + this.onMapLoadedListener, + this.onMapLoadErrorListener, + this.onRenderFrameStartedListener, + this.onRenderFrameFinishedListener, + this.onSourceAddedListener, + this.onSourceDataLoadedListener, + this.onSourceRemovedListener, + this.onStyleDataLoadedListener, + this.onStyleImageMissingListener, + this.onStyleImageUnusedListener, + this.onResourceRequestListener, + this.onTapListener, + this.onLongTapListener, + this.onScrollListener, + this.onNewLocationListener, + this.onNavigationRouteReadyListener, + this.onNavigationRouteFailedListener, + this.onNavigationRouteCancelledListener, + this.onNavigationRouteRenderedListener, + this.onRouteProgressListener, + this.onNavigationCameraStateListener}) + : super(key: key) { LogConfiguration._setupDebugLoggingIfNeeded(); } @@ -161,6 +168,14 @@ class MapWidget extends StatefulWidget { final OnMapLongTapListener? onLongTapListener; final OnMapScrollListener? onScrollListener; + final OnNewLocationListener? onNewLocationListener; + final OnNavigationRouteListener? onNavigationRouteReadyListener; + final OnNavigationRouteListener? onNavigationRouteFailedListener; + final OnNavigationRouteListener? onNavigationRouteCancelledListener; + final OnNavigationRouteListener? onNavigationRouteRenderedListener; + final OnRouteProgressListener? onRouteProgressListener; + final OnNavigationCameraStateListener? onNavigationCameraStateListener; + @override State createState() { return _mapWidgetState; @@ -243,11 +258,20 @@ class _MapWidgetState extends State { Future onPlatformViewCreated(int id) async { final MapboxMap controller = MapboxMap._( - mapboxMapsPlatform: _mapboxMapsPlatform, - onMapTapListener: widget.onTapListener, - onMapLongTapListener: widget.onLongTapListener, - onMapScrollListener: widget.onScrollListener, - ); + mapboxMapsPlatform: _mapboxMapsPlatform, + onMapTapListener: widget.onTapListener, + onMapLongTapListener: widget.onLongTapListener, + onMapScrollListener: widget.onScrollListener, + onNavigationRouteReadyListener: widget.onNavigationRouteReadyListener, + onNavigationRouteFailedListener: widget.onNavigationRouteFailedListener, + onNavigationRouteCancelledListener: + widget.onNavigationRouteCancelledListener, + onNavigationRouteRenderedListener: + widget.onNavigationRouteRenderedListener, + onNewLocationListener: widget.onNewLocationListener, + onRouteProgressListener: widget.onRouteProgressListener, + onNavigationCameraStateListener: + widget.onNavigationCameraStateListener); if (widget.onMapCreated != null) { widget.onMapCreated!(controller); } diff --git a/lib/src/mapbox_map.dart b/lib/src/mapbox_map.dart index 0d4c51d95..e69df942c 100644 --- a/lib/src/mapbox_map.dart +++ b/lib/src/mapbox_map.dart @@ -137,14 +137,22 @@ extension on _MapWidgetDebugOptions { /// Controller for a single MapboxMap instance running on the host platform. class MapboxMap extends ChangeNotifier { - MapboxMap._({ - required _MapboxMapsPlatform mapboxMapsPlatform, - this.onMapTapListener, - this.onMapLongTapListener, - this.onMapScrollListener, - }) : _mapboxMapsPlatform = mapboxMapsPlatform { + MapboxMap._( + {required _MapboxMapsPlatform mapboxMapsPlatform, + this.onMapTapListener, + this.onMapLongTapListener, + this.onMapScrollListener, + this.onNewLocationListener, + this.onNavigationRouteReadyListener, + this.onNavigationRouteFailedListener, + this.onNavigationRouteCancelledListener, + this.onNavigationRouteRenderedListener, + this.onRouteProgressListener, + this.onNavigationCameraStateListener}) + : _mapboxMapsPlatform = mapboxMapsPlatform { annotations = AnnotationManager._(mapboxMapsPlatform: _mapboxMapsPlatform); _setupGestures(); + _setupNavigation(); } final _MapboxMapsPlatform _mapboxMapsPlatform; @@ -204,10 +212,23 @@ class MapboxMap extends ChangeNotifier { binaryMessenger: _mapboxMapsPlatform.binaryMessenger, messageChannelSuffix: _mapboxMapsPlatform.channelSuffix.toString()); + /// The interface to access navigation methods. + late NavigationInterface navigation = NavigationInterface( + binaryMessenger: _mapboxMapsPlatform.binaryMessenger, + messageChannelSuffix: _mapboxMapsPlatform.channelSuffix.toString()); + OnMapTapListener? onMapTapListener; OnMapLongTapListener? onMapLongTapListener; OnMapScrollListener? onMapScrollListener; + OnNewLocationListener? onNewLocationListener; + OnNavigationRouteListener? onNavigationRouteReadyListener; + OnNavigationRouteListener? onNavigationRouteFailedListener; + OnNavigationRouteListener? onNavigationRouteCancelledListener; + OnNavigationRouteListener? onNavigationRouteRenderedListener; + OnRouteProgressListener? onRouteProgressListener; + OnNavigationCameraStateListener? onNavigationCameraStateListener; + @override void dispose() { _mapboxMapsPlatform.dispose(); @@ -215,6 +236,10 @@ class MapboxMap extends ChangeNotifier { binaryMessenger: _mapboxMapsPlatform.binaryMessenger, messageChannelSuffix: _mapboxMapsPlatform.channelSuffix.toString()); + NavigationListener.setUp(null, + binaryMessenger: _mapboxMapsPlatform.binaryMessenger, + messageChannelSuffix: _mapboxMapsPlatform.channelSuffix.toString()); + super.dispose(); } @@ -614,6 +639,26 @@ class MapboxMap extends ChangeNotifier { } } + void _setupNavigation() { + if (onNewLocationListener != null || + onNavigationRouteReadyListener != null) { + NavigationListener.setUp( + _NavigationListener( + onNewLocationListener: onNewLocationListener, + onNavigationRouteReadyListener: onNavigationRouteReadyListener, + onNavigationRouteFailedListener: onNavigationRouteFailedListener, + onNavigationRouteCancelledListener: + onNavigationRouteCancelledListener, + onNavigationRouteRenderedListener: + onNavigationRouteRenderedListener, + onRouteProgressListener: onRouteProgressListener, + onNavigationCameraStateListener: onNavigationCameraStateListener), + binaryMessenger: _mapboxMapsPlatform.binaryMessenger, + messageChannelSuffix: _mapboxMapsPlatform.channelSuffix.toString()); + _mapboxMapsPlatform.addNavigationListeners(); + } + } + void setOnMapTapListener(OnMapTapListener? onMapTapListener) { this.onMapTapListener = onMapTapListener; _setupGestures(); @@ -629,6 +674,17 @@ class MapboxMap extends ChangeNotifier { _setupGestures(); } + void setOnNavigationRouteReadyListener( + OnNavigationRouteListener? onNavigationRouteReadyListener) { + this.onNavigationRouteReadyListener = onNavigationRouteReadyListener; + _setupNavigation(); + } + + void setOnNewLocationListener(OnNewLocationListener? onNewLocationListener) { + this.onNewLocationListener = onNewLocationListener; + _setupNavigation(); + } + /// Returns a snapshot of the map. /// The snapshot is taken from the current state of the map. Future snapshot() => _mapboxMapsPlatform.snapshot(); @@ -671,3 +727,57 @@ class _GestureListener extends GestureListener { onMapScrollListener?.call(context); } } + +class _NavigationListener extends NavigationListener { + _NavigationListener( + {this.onNavigationRouteReadyListener, + this.onNavigationRouteFailedListener, + this.onNavigationRouteCancelledListener, + this.onNavigationRouteRenderedListener, + this.onNewLocationListener, + this.onRouteProgressListener, + this.onNavigationCameraStateListener}); + + final OnNavigationRouteListener? onNavigationRouteReadyListener; + final OnNavigationRouteListener? onNavigationRouteFailedListener; + final OnNavigationRouteListener? onNavigationRouteCancelledListener; + final OnNavigationRouteListener? onNavigationRouteRenderedListener; + final OnNewLocationListener? onNewLocationListener; + final OnRouteProgressListener? onRouteProgressListener; + final OnNavigationCameraStateListener? onNavigationCameraStateListener; + + @override + void onNavigationRouteReady() { + onNavigationRouteReadyListener?.call(); + } + + @override + void onNavigationRouteFailed() { + onNavigationRouteFailedListener?.call(); + } + + @override + void onNavigationRouteCancelled() { + onNavigationRouteCancelledListener?.call(); + } + + @override + void onNavigationRouteRendered() { + onNavigationRouteRenderedListener?.call(); + } + + @override + void onNewLocation(NavigationLocation location) { + onNewLocationListener?.call(location); + } + + @override + void onRouteProgress(RouteProgress routeProgress) { + onRouteProgressListener?.call(routeProgress); + } + + @override + void onNavigationCameraStateChanged(NavigationCameraState state) { + onNavigationCameraStateListener?.call(state); + } +} diff --git a/lib/src/mapbox_maps_platform.dart b/lib/src/mapbox_maps_platform.dart index 9e8c6236d..630d4f0c6 100644 --- a/lib/src/mapbox_maps_platform.dart +++ b/lib/src/mapbox_maps_platform.dart @@ -156,6 +156,22 @@ class _MapboxMapsPlatform { } } + Future addNavigationListeners() async { + try { + return _channel.invokeMethod('navigation#add_listeners'); + } on PlatformException catch (e) { + return new Future.error(e); + } + } + + Future removeNavigationListeners() async { + try { + return _channel.invokeMethod('navigation#remove_listeners'); + } on PlatformException catch (e) { + return new Future.error(e); + } + } + Future snapshot() async { try { final List data = await _channel.invokeMethod('map#snapshot'); diff --git a/lib/src/pigeons/map_interfaces.dart b/lib/src/pigeons/map_interfaces.dart index 9eb1bcacb..7df7ee47c 100644 --- a/lib/src/pigeons/map_interfaces.dart +++ b/lib/src/pigeons/map_interfaces.dart @@ -2005,6 +2005,27 @@ class MapInterfaces_PigeonCodec extends StandardMessageCodec { } else if (value is StylePropertyValue) { buffer.putUint8(190); writeValue(buffer, value.encode()); + } else if (value is NavigationLocation) { + buffer.putUint8(191); + writeValue(buffer, value.encode()); + } else if (value is RouteProgressState) { + buffer.putUint8(192); + writeValue(buffer, value.index); + } else if (value is RoadObjectLocationType) { + buffer.putUint8(193); + writeValue(buffer, value.index); + } else if (value is RoadObject) { + buffer.putUint8(194); + writeValue(buffer, value.encode()); + } else if (value is RoadObjectDistanceInfo) { + buffer.putUint8(195); + writeValue(buffer, value.encode()); + } else if (value is UpcomingRoadObject) { + buffer.putUint8(196); + writeValue(buffer, value.encode()); + } else if (value is RouteProgress) { + buffer.putUint8(197); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -2159,6 +2180,22 @@ class MapInterfaces_PigeonCodec extends StandardMessageCodec { return CanonicalTileID.decode(readValue(buffer)!); case 190: return StylePropertyValue.decode(readValue(buffer)!); + case 191: + return NavigationLocation.decode(readValue(buffer)!); + case 192: + final int? value = readValue(buffer) as int?; + return value == null ? null : RouteProgressState.values[value]; + case 193: + final int? value = readValue(buffer) as int?; + return value == null ? null : RoadObjectLocationType.values[value]; + case 194: + return RoadObject.decode(readValue(buffer)!); + case 195: + return RoadObjectDistanceInfo.decode(readValue(buffer)!); + case 196: + return UpcomingRoadObject.decode(readValue(buffer)!); + case 197: + return RouteProgress.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } diff --git a/lib/src/pigeons/navigation.dart b/lib/src/pigeons/navigation.dart new file mode 100644 index 000000000..107833f6c --- /dev/null +++ b/lib/src/pigeons/navigation.dart @@ -0,0 +1,806 @@ +// Autogenerated from Pigeon (v22.6.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers +part of mapbox_maps_flutter; + +enum NavigationCameraState { + IDLE, + TRANSITION_TO_FOLLOWING, + FOLLOWING, + TRANSITION_TO_OVERVIEW, + OVERVIEW, +} + +enum RouteProgressState { + INITIALIZED, + TRACKING, + COMPLETE, + OFF_ROUTE, + UNCERTAIN, +} + +enum RoadObjectLocationType { + GANTRY, + OPEN_LR_LINE, + OPEN_LR_POINT, + POINT, + POLYGON, + POLYLINE, + ROUTE_ALERT, + SUBGRAPH, +} + +class RoadObject { + RoadObject({ + this.id, + this.objectType, + this.length, + this.provider, + this.isUrban, + }); + + String? id; + + RoadObjectLocationType? objectType; + + double? length; + + String? provider; + + bool? isUrban; + + Object encode() { + return [ + id, + objectType, + length, + provider, + isUrban, + ]; + } + + static RoadObject decode(Object result) { + result as List; + return RoadObject( + id: result[0] as String?, + objectType: result[1] as RoadObjectLocationType?, + length: result[2] as double?, + provider: result[3] as String?, + isUrban: result[4] as bool?, + ); + } +} + +class RoadObjectDistanceInfo { + RoadObjectDistanceInfo({ + this.distanceToStart, + }); + + double? distanceToStart; + + Object encode() { + return [ + distanceToStart, + ]; + } + + static RoadObjectDistanceInfo decode(Object result) { + result as List; + return RoadObjectDistanceInfo( + distanceToStart: result[0] as double?, + ); + } +} + +class UpcomingRoadObject { + UpcomingRoadObject({ + this.roadObject, + this.distanceToStart, + this.distanceInfo, + }); + + RoadObject? roadObject; + + double? distanceToStart; + + RoadObjectDistanceInfo? distanceInfo; + + Object encode() { + return [ + roadObject, + distanceToStart, + distanceInfo, + ]; + } + + static UpcomingRoadObject decode(Object result) { + result as List; + return UpcomingRoadObject( + roadObject: result[0] as RoadObject?, + distanceToStart: result[1] as double?, + distanceInfo: result[2] as RoadObjectDistanceInfo?, + ); + } +} + +class RouteProgress { + RouteProgress({ + this.navigationRouteJson, + this.bannerInstructionsJson, + this.voiceInstructionsJson, + this.currentState, + this.inTunnel, + this.distanceRemaining, + this.distanceTraveled, + this.durationRemaining, + this.fractionTraveled, + this.remainingWaypoints, + this.upcomingRoadObjects, + this.stale, + this.routeAlternativeId, + this.currentRouteGeometryIndex, + this.inParkingAisle, + }); + + String? navigationRouteJson; + + String? bannerInstructionsJson; + + String? voiceInstructionsJson; + + RouteProgressState? currentState; + + bool? inTunnel; + + double? distanceRemaining; + + double? distanceTraveled; + + double? durationRemaining; + + double? fractionTraveled; + + int? remainingWaypoints; + + List? upcomingRoadObjects; + + bool? stale; + + String? routeAlternativeId; + + int? currentRouteGeometryIndex; + + bool? inParkingAisle; + + Object encode() { + return [ + navigationRouteJson, + bannerInstructionsJson, + voiceInstructionsJson, + currentState, + inTunnel, + distanceRemaining, + distanceTraveled, + durationRemaining, + fractionTraveled, + remainingWaypoints, + upcomingRoadObjects, + stale, + routeAlternativeId, + currentRouteGeometryIndex, + inParkingAisle, + ]; + } + + static RouteProgress decode(Object result) { + result as List; + return RouteProgress( + navigationRouteJson: result[0] as String?, + bannerInstructionsJson: result[1] as String?, + voiceInstructionsJson: result[2] as String?, + currentState: result[3] as RouteProgressState?, + inTunnel: result[4] as bool?, + distanceRemaining: result[5] as double?, + distanceTraveled: result[6] as double?, + durationRemaining: result[7] as double?, + fractionTraveled: result[8] as double?, + remainingWaypoints: result[9] as int?, + upcomingRoadObjects: + (result[10] as List?)?.cast(), + stale: result[11] as bool?, + routeAlternativeId: result[12] as String?, + currentRouteGeometryIndex: result[13] as int?, + inParkingAisle: result[14] as bool?, + ); + } +} + +class NavigationLocation { + NavigationLocation({ + this.latitude, + this.longitude, + this.timestamp, + this.monotonicTimestamp, + this.altitude, + this.horizontalAccuracy, + this.verticalAccuracy, + this.speed, + this.speedAccuracy, + this.bearing, + this.bearingAccuracy, + this.floor, + this.source, + }); + + double? latitude; + + double? longitude; + + int? timestamp; + + int? monotonicTimestamp; + + double? altitude; + + double? horizontalAccuracy; + + double? verticalAccuracy; + + double? speed; + + double? speedAccuracy; + + double? bearing; + + double? bearingAccuracy; + + int? floor; + + String? source; + + Object encode() { + return [ + latitude, + longitude, + timestamp, + monotonicTimestamp, + altitude, + horizontalAccuracy, + verticalAccuracy, + speed, + speedAccuracy, + bearing, + bearingAccuracy, + floor, + source, + ]; + } + + static NavigationLocation decode(Object result) { + result as List; + return NavigationLocation( + latitude: result[0] as double?, + longitude: result[1] as double?, + timestamp: result[2] as int?, + monotonicTimestamp: result[3] as int?, + altitude: result[4] as double?, + horizontalAccuracy: result[5] as double?, + verticalAccuracy: result[6] as double?, + speed: result[7] as double?, + speedAccuracy: result[8] as double?, + bearing: result[9] as double?, + bearingAccuracy: result[10] as double?, + floor: result[11] as int?, + source: result[12] as String?, + ); + } +} + +class Waypoint { + Waypoint({this.point, this.name}); + + Point? point; + + String? name; + + Object encode() { + return [ + point, + name, + ]; + } + + static Waypoint decode(Object result) { + result as List; + return Waypoint( + point: result[0] as Point?, + name: result[1] as String?, + ); + } +} + +class RouteOptions { + RouteOptions( + {this.waypoints, + this.steps, + this.alternatives, + this.coordinates, + this.voiceInstructions}); + + List? waypoints; + bool? steps; + bool? alternatives; + List? coordinates; + bool? voiceInstructions; + + Object encode() { + return [ + waypoints, + steps, + alternatives, + coordinates, + voiceInstructions + ]; + } + + static RouteOptions decode(Object result) { + result as List; + return RouteOptions( + waypoints: result[0] as List?, + steps: result[1] as bool?, + alternatives: result[2] as bool?, + coordinates: result[3] as List?, + voiceInstructions: result[4] as bool?, + ); + } +} + +class Navigation_PigeonCodec extends StandardMessageCodec { + const Navigation_PigeonCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is int) { + buffer.putUint8(4); + buffer.putInt64(value); + } else if (value is Point) { + buffer.putUint8(151); + writeValue(buffer, value.encode()); + } else if (value is Feature) { + buffer.putUint8(152); + writeValue(buffer, value.encode()); + } else if (value is NavigationLocation) { + buffer.putUint8(191); + writeValue(buffer, value.encode()); + } else if (value is RouteProgressState) { + buffer.putUint8(192); + writeValue(buffer, value.index); + } else if (value is RoadObjectLocationType) { + buffer.putUint8(193); + writeValue(buffer, value.index); + } else if (value is RoadObject) { + buffer.putUint8(194); + writeValue(buffer, value.encode()); + } else if (value is RoadObjectDistanceInfo) { + buffer.putUint8(195); + writeValue(buffer, value.encode()); + } else if (value is UpcomingRoadObject) { + buffer.putUint8(196); + writeValue(buffer, value.encode()); + } else if (value is RouteProgress) { + buffer.putUint8(197); + writeValue(buffer, value.encode()); + } else if (value is NavigationCameraState) { + buffer.putUint8(198); + writeValue(buffer, value.index); + } else if (value is Waypoint) { + buffer.putUint8(199); + writeValue(buffer, value.encode()); + } else if (value is RouteOptions) { + buffer.putUint8(200); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 151: + return Point.decode(readValue(buffer)!); + case 152: + return Feature.decode(readValue(buffer)!); + case 191: + return NavigationLocation.decode(readValue(buffer)!); + case 192: + final int? value = readValue(buffer) as int?; + return value == null ? null : RouteProgressState.values[value]; + case 193: + final int? value = readValue(buffer) as int?; + return value == null ? null : RoadObjectLocationType.values[value]; + case 194: + return RoadObject.decode(readValue(buffer)!); + case 195: + return RoadObjectDistanceInfo.decode(readValue(buffer)!); + case 196: + return UpcomingRoadObject.decode(readValue(buffer)!); + case 197: + return RouteProgress.decode(readValue(buffer)!); + case 198: + final int? value = readValue(buffer) as int?; + return value == null ? null : NavigationCameraState.values[value]; + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class NavigationListener { + static const MessageCodec pigeonChannelCodec = + Navigation_PigeonCodec(); + + void onNavigationRouteReady(); + + void onNavigationRouteFailed(); + + void onNavigationRouteCancelled(); + + void onNavigationRouteRendered(); + + void onNewLocation(NavigationLocation location); + + void onRouteProgress(RouteProgress routeProgress); + + void onNavigationCameraStateChanged(NavigationCameraState state); + + static void setUp( + NavigationListener? api, { + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) { + messageChannelSuffix = + messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + { + final BasicMessageChannel< + Object?> pigeonVar_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.mapbox_maps_flutter.NavigationListener.onNavigationRouteReady$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + try { + api.onNavigationRouteReady(); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel< + Object?> pigeonVar_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.mapbox_maps_flutter.NavigationListener.onNavigationRouteFailed$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + try { + api.onNavigationRouteFailed(); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel< + Object?> pigeonVar_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.mapbox_maps_flutter.NavigationListener.onNavigationRouteCancelled$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + try { + api.onNavigationRouteCancelled(); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel< + Object?> pigeonVar_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.mapbox_maps_flutter.NavigationListener.onNavigationRouteRendered$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + try { + api.onNavigationRouteRendered(); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel< + Object?> pigeonVar_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.mapbox_maps_flutter.NavigationListener.onNewLocation$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.mapbox_maps_flutter.NavigationListener.onNewLocation was null.'); + final List args = (message as List?)!; + final NavigationLocation? arg_location = + (args[0] as NavigationLocation?); + assert(arg_location != null, + 'Argument for dev.flutter.pigeon.mapbox_maps_flutter.NavigationListener.onNewLocation was null, expected non-null NavigationLocation.'); + try { + api.onNewLocation(arg_location!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel< + Object?> pigeonVar_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.mapbox_maps_flutter.NavigationListener.onRouteProgress$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.mapbox_maps_flutter.NavigationListener.onRouteProgress was null.'); + final List args = (message as List?)!; + final RouteProgress? arg_routeProgress = (args[0] as RouteProgress?); + assert(arg_routeProgress != null, + 'Argument for dev.flutter.pigeon.mapbox_maps_flutter.NavigationListener.onRouteProgress was null, expected non-null RouteProgress.'); + try { + api.onRouteProgress(arg_routeProgress!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel< + Object?> pigeonVar_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.mapbox_maps_flutter.NavigationListener.onNavigationCameraStateChanged$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.mapbox_maps_flutter.NavigationListener.onNavigationCameraStateChanged was null.'); + final List args = (message as List?)!; + final NavigationCameraState? arg_state = + (args[0] as NavigationCameraState?); + assert(arg_state != null, + 'Argument for dev.flutter.pigeon.mapbox_maps_flutter.NavigationListener.onNavigationCameraStateChanged was null, expected non-null NavigationCameraState.'); + try { + api.onNavigationCameraStateChanged(arg_state!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + } +} + +class NavigationInterface { + /// Constructor for [NavigationInterface]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + NavigationInterface( + {BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = + messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = + Navigation_PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + Future setRoute(RouteOptions options) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.mapbox_maps_flutter.NavigationInterface.setRoute$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([options]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future stopTripSession() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.mapbox_maps_flutter.NavigationInterface.stopTripSession$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future startTripSession(bool withForegroundService) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.mapbox_maps_flutter.NavigationInterface.startTripSession$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = await pigeonVar_channel + .send([withForegroundService]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future requestNavigationCameraToFollowing() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.mapbox_maps_flutter.NavigationInterface.requestNavigationCameraToFollowing$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future requestNavigationCameraToOverview() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.mapbox_maps_flutter.NavigationInterface.requestNavigationCameraToOverview$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future lastLocation() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.mapbox_maps_flutter.NavigationInterface.lastLocation$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return (pigeonVar_replyList[0] as NavigationLocation?); + } + } +} diff --git a/lib/src/style/directions_criteria.dart b/lib/src/style/directions_criteria.dart new file mode 100644 index 000000000..d72a587d0 --- /dev/null +++ b/lib/src/style/directions_criteria.dart @@ -0,0 +1,7 @@ +part of mapbox_maps_flutter; + +/// Constants and properties used to customize the directions request. + +class DirectionsCriteria { + static const String PROFILE_DEFAULT_USER = "mapbox"; +} diff --git a/lib/src/style/navigation_styles.dart b/lib/src/style/navigation_styles.dart new file mode 100644 index 000000000..7fdead14c --- /dev/null +++ b/lib/src/style/navigation_styles.dart @@ -0,0 +1,36 @@ +part of mapbox_maps_flutter; + +/// An object that links to default navigation styles. +/// We recommend using these map styles for an optimized map appearance for navigation use-cases. +class NavigationStyles { + static const String NAVIGATION_DAY_STYLE_USER_ID = + DirectionsCriteria.PROFILE_DEFAULT_USER; + + // + // Style ID for day mode + // + static const String NAVIGATION_DAY_STYLE_ID = "navigation-day-v1"; + + // + // Default navigation style for day mode + // + static const String NAVIGATION_DAY_STYLE = + "mapbox://styles/$NAVIGATION_DAY_STYLE_USER_ID/$NAVIGATION_DAY_STYLE_ID"; + + // + // User ID for night mode + // + static const String NAVIGATION_NIGHT_STYLE_USER_ID = + DirectionsCriteria.PROFILE_DEFAULT_USER; + + // + // Style ID for night mode + // + static const String NAVIGATION_NIGHT_STYLE_ID = "navigation-night-v1"; + + // + // Default navigation style for night mode + // + static const String NAVIGATION_NIGHT_STYLE = + "mapbox://styles/$NAVIGATION_NIGHT_STYLE_USER_ID/$NAVIGATION_NIGHT_STYLE_ID"; +} diff --git a/scripts/checkout-navigation.sh b/scripts/checkout-navigation.sh new file mode 100644 index 000000000..04d3fde1c --- /dev/null +++ b/scripts/checkout-navigation.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +git clone --no-checkout https://github.com/mapbox/mapbox-navigation-ios.git ./ios/Classes/Navigation + +cd ./ios/Classes/Navigation + +git tag + +git checkout tags/v3.5.0 \ No newline at end of file